From: David Bremner Date: Sat, 26 Sep 2015 13:31:50 +0000 (-0300) Subject: Merge branch 'release' X-Git-Tag: 0.21_rc0~22 X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=commitdiff_plain;h=33c8777a967ece2dd4bbda7e83a4e07c195abf51;hp=754d84edad2534099b53c15cc5b443a672621622 Merge branch 'release' bring debian specific changes into master --- diff --git a/INSTALL b/INSTALL index eaccd93d..b1b9cd55 100644 --- a/INSTALL +++ b/INSTALL @@ -20,7 +20,7 @@ configure stage. Dependencies ------------ -Notmuch depends on four libraries: Xapian, GMime 2.4 or 2.6, +Notmuch depends on four libraries: Xapian, GMime 2.6, Talloc, and zlib which are each described below: Xapian @@ -39,8 +39,8 @@ Talloc, and zlib which are each described below: reading mail while notmuch would wait for Xapian when removing the "inbox" and "unread" tags from messages in a thread. - GMime 2.4 or 2.6 - ---------------- + GMime 2.6 + ---------- GMime provides decoding of MIME email messages for Notmuch. Without GMime, Notmuch would not be able to extract and index diff --git a/Makefile.local b/Makefile.local index 6d547423..d58cea04 100644 --- a/Makefile.local +++ b/Makefile.local @@ -59,7 +59,7 @@ endif FINAL_LIBNOTMUCH_LDFLAGS = $(LDFLAGS) $(AS_NEEDED_LDFLAGS) $(CONFIGURE_LDFLAGS) .PHONY: all -all: notmuch notmuch-shared build-man +all: notmuch notmuch-shared build-man ruby-bindings ifeq ($(MAKECMDGOALS),) ifeq ($(shell cat .first-build-message 2>/dev/null),) @NOTMUCH_FIRST_BUILD=1 $(MAKE) --no-print-directory all @@ -271,6 +271,7 @@ dataclean: distclean notmuch_client_srcs = \ command-line-arguments.c\ debugger.c \ + status.c \ gmime-filter-reply.c \ hooks.c \ notmuch.c \ diff --git a/NEWS b/NEWS index cc8d7edf..3593ded7 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,32 @@ +Notmuch 0.21 (UNRELEASED) +========================= + +General +------- + +Notmuch now requires gmime >= 2.6.7. The gmime 2.4 series is no longer +supported. + +Emacs Interface +--------------- + +`notmuch-emacs-version` is used in `User-Agent` header + + The value of recently introduced variable `notmuch-emacs-version` is + now used as a part of `User-Agent` header when sending emails. + +Removed `notmuch-version` function by renaming it to `notmuch-cli-version` + + With existing variable `notmuch-emacs-version` the accompanied + function which retrieves the version of `notmuch-command` is + better named as `notmuch-cli-version`. + +Library +------- + +The use of absolute paths is now enforced when calling +`notmuch_database_{open, create}`. + Notmuch 0.20.2 (2015-06-27) =========================== @@ -12,7 +41,7 @@ Notmuch 0.20.1 (2015-06-01) Test Suite ---------- -Work around apparent gdb bug on arm64 +Work around apparent gdb bug on arm64. Notmuch 0.20 (2015-05-31) ========================= @@ -2767,7 +2796,7 @@ New 'G' key binding to trigger mail refresh (G == "Get new mail") The 'G' key works wherever '=' works. Before refreshing the screen it calls an external program that can be used to poll email servers, - run notmuch new and setup specific tags for the new emails. The + run notmuch new and set up specific tags for the new emails. The script to be called should be configured with the "Notmuch Poll Script" setting in the customize interface. This script will typically invoke "notmuch new" and then perhaps several "notmuch diff --git a/bindings/Makefile b/bindings/Makefile new file mode 100644 index 00000000..de492a7c --- /dev/null +++ b/bindings/Makefile @@ -0,0 +1,7 @@ +# See Makefile.local for the list of files to be compiled in this +# directory. +all: + $(MAKE) -C .. all + +.DEFAULT: + $(MAKE) -C .. $@ diff --git a/bindings/Makefile.local b/bindings/Makefile.local new file mode 100644 index 00000000..4ecf839d --- /dev/null +++ b/bindings/Makefile.local @@ -0,0 +1,21 @@ +# -*- makefile -*- + +dir := bindings + +# force the shared library to be built +ruby-bindings: lib/$(LINKER_NAME) +ifeq ($(HAVE_RUBY_DEV),1) + cd $(dir)/ruby && \ + EXTRA_LDFLAGS="$(NO_UNDEFINED_LDFLAGS)" \ + LIBNOTMUCH="../../lib/$(LINKER_NAME)" \ + ruby extconf.rb --vendor + $(MAKE) -C $(dir)/ruby +else + @echo Missing dependency, skipping ruby bindings +endif + +CLEAN += $(patsubst %,$(dir)/ruby/%, \ + .RUBYARCHDIR.time \ + Makefile database.o directory.o filenames.o\ + init.o message.o messages.o mkmf.log notmuch.so query.o \ + status.o tags.o thread.o threads.o) diff --git a/bindings/ruby/README b/bindings/ruby/README new file mode 100644 index 00000000..a2946b66 --- /dev/null +++ b/bindings/ruby/README @@ -0,0 +1,7 @@ +To build the the notmuch ruby extension, run the following commands +from the *top level* notmuch source directory: + +% ./configure +% make ruby-bindings + +The generic documentation about building notmuch also applies. diff --git a/bindings/ruby/extconf.rb b/bindings/ruby/extconf.rb index 6160db26..ddaa6841 100644 --- a/bindings/ruby/extconf.rb +++ b/bindings/ruby/extconf.rb @@ -10,22 +10,16 @@ dir = File.join('..', '..', 'lib') # includes $INCFLAGS = "-I#{dir} #{$INCFLAGS}" -# make sure there are no undefined symbols -$LDFLAGS += ' -Wl,--no-undefined' - -def have_local_library(lib, path, func, headers = nil) - checking_for checking_message(func, lib) do - lib = File.join(path, lib) - if try_func(func, lib, headers) - $LOCAL_LIBS += lib - end - end +if ENV['EXTRA_LDFLAGS'] + $LDFLAGS += " " + ENV['EXTRA_LDFLAGS'] end -if not have_local_library('libnotmuch.so', dir, 'notmuch_database_create', 'notmuch.h') +if not ENV['LIBNOTMUCH'] exit 1 end +$LOCAL_LIBS += ENV['LIBNOTMUCH'] + # Create Makefile dir_config('notmuch') create_makefile('notmuch') diff --git a/configure b/configure index 4af7ba94..440d678c 100755 --- a/configure +++ b/configure @@ -21,6 +21,7 @@ srcdir=$(dirname "$0") subdirs="util compat lib parse-time-string completion doc emacs" subdirs="${subdirs} performance-test test test/test-databases" +subdirs="${subdirs} bindings" # For a non-srcdir configure invocation (such as ../configure), create # the directory structure and copy Makefiles. @@ -37,7 +38,7 @@ if [ "$srcdir" != "." ]; then cp -a "$srcdir"/test/* test # Emacs only likes to generate compiled files next to the .el files - # by default so copy these as well (which is not ideal0. + # by default so copy these as well (which is not ideal). cp -a "$srcdir"/emacs/*.el emacs fi @@ -47,9 +48,11 @@ CC=${CC:-cc} CXX=${CXX:-c++} CFLAGS=${CFLAGS:--g -O2} CPPFLAGS=${CPPFLAGS:-} +CXXFLAGS_for_sh=${CXXFLAGS:-${CFLAGS}} CXXFLAGS=${CXXFLAGS:-\$(CFLAGS)} LDFLAGS=${LDFLAGS:-} XAPIAN_CONFIG=${XAPIAN_CONFIG:-xapian-config} +PYTHON=${PYTHON:-} # We don't allow the EMACS or GZIP Makefile variables inherit values # from the environment as we do with CC and CXX above. The reason is @@ -62,20 +65,12 @@ XAPIAN_CONFIG=${XAPIAN_CONFIG:-xapian-config} # options. PREFIX=/usr/local LIBDIR= +WITH_DOCS=1 WITH_EMACS=1 WITH_BASH=1 +WITH_RUBY=1 WITH_ZSH=1 -# Compatible GMime versions (with constraints). -# If using GMime 2.6, we need to have a version >= 2.6.5 to avoid a -# crypto bug. We need 2.6.7 for permissive "From " header handling. -GMIME_24_VERSION_CTR='' -GMIME_24_VERSION="gmime-2.4 $GMIME_24_VERSION_CTR" -GMIME_26_VERSION_CTR='>= 2.6.7' -GMIME_26_VERSION="gmime-2.6 $GMIME_26_VERSION_CTR" - -WITH_GMIME_VERSIONS="$GMIME_26_VERSION;$GMIME_24_VERSION" - usage () { cat < minimal.c + +printf "Sanity checking C compilation environment... " +if ${CC} ${CFLAGS} ${CPPFLAGS} minimal.c ${LDFLAGS} -o minimal > /dev/null 2>&1 +then + printf "OK.\n" +else + printf "Fail.\n" + errors=$((errors + 1)) +fi + +printf "Sanity checking C++ compilation environment... " +if ${CXX} ${CXXFLAGS_for_sh} ${CPPFLAGS} minimal.c ${LDFLAGS} -o minimal > /dev/null 2>&1 +then + printf "OK.\n" +else + printf "Fail.\n" + errors=$((errors + 1)) +fi + +if [ $errors -gt 0 ]; then + cat < _libversion.c < +#include "lib/notmuch.h" +int main(void) { + printf("libnotmuch_version_major=%d\n", + LIBNOTMUCH_MAJOR_VERSION); + printf("libnotmuch_version_minor=%d\n", + LIBNOTMUCH_MINOR_VERSION); + printf("libnotmuch_version_release=%d\n", + LIBNOTMUCH_MICRO_VERSION); + return 0; +} +EOF +if ${CC} ${CFLAGS} _libversion.c -o _libversion > /dev/null 2>&1 && \ + ./_libversion > _libversion.sh && . ./_libversion.sh +then + printf "OK.\n" +else + cat < /dev/null 2>&1; then have_pkg_config=1 @@ -298,30 +361,29 @@ have_xapian_compact=0 if [ ${have_xapian} = "1" ]; then printf "Checking for Xapian compaction support... " case "${xapian_version}" in - 0.*|1.[01].*|1.2.[0-5]) - printf "No (only available with Xapian > 1.2.6).\n" ;; - [1-9]*.[0-9]*.[0-9]*) - have_xapian_compact=1 - printf "Yes.\n" ;; - *) - printf "Unknown version.\n" ;; + 0.*|1.[01].*|1.2.[0-5]) + printf "No (only available with Xapian > 1.2.6).\n" ;; + [1-9]*.[0-9]*.[0-9]*) + have_xapian_compact=1 + printf "Yes.\n" ;; + *) + printf "Unknown version.\n" ;; esac fi + +# we need to have a version >= 2.6.5 to avoid a crypto bug. We need +# 2.6.7 for permissive "From " header handling. +GMIME_MINVER=2.6.7 + printf "Checking for GMime development files... " -have_gmime=0 -IFS=';' -for gmimepc in $WITH_GMIME_VERSIONS; do - if pkg-config --exists $gmimepc; then - printf "Yes ($gmimepc).\n" - have_gmime=1 - gmime_cflags=$(pkg-config --cflags $gmimepc) - gmime_ldflags=$(pkg-config --libs $gmimepc) - break - fi -done -IFS=$DEFAULT_IFS -if [ "$have_gmime" = "0" ]; then +if pkg-config --exists "gmime-2.6 >= $GMIME_MINVER"; then + printf "Yes.\n" + have_gmime=1 + gmime_cflags=$(pkg-config --cflags gmime-2.6) + gmime_ldflags=$(pkg-config --libs gmime-2.6) +else + have_gmime=0 printf "No.\n" errors=$((errors + 1)) fi @@ -377,7 +439,7 @@ fi printf "Checking for python... " have_python=0 -for name in python python2 python3; do +for name in ${PYTHON} python python2 python3; do if command -v $name > /dev/null; then have_python=1 python=$name @@ -434,22 +496,37 @@ else have_emacs=0 fi -printf "Checking if doxygen is available... " -if command -v doxygen > /dev/null; then - printf "Yes.\n" - have_doxygen=1 -else - printf "No (so will not install api docs)\n" - have_doxygen=0 +have_doxygen=0 +if [ $WITH_DOCS = "1" ] ; then + printf "Checking if doxygen is available... " + if command -v doxygen > /dev/null; then + printf "Yes.\n" + have_doxygen=1 + else + printf "No (so will not install api docs)\n" + fi fi -printf "Checking if sphinx is available and supports nroff output... " -if command -v sphinx-build > /dev/null && ${python} -m sphinx.writers.manpage > /dev/null 2>&1 ; then - printf "Yes.\n" - have_sphinx=1 -else - printf "No (so will not install man pages).\n" - have_sphinx=0 +have_ruby_dev=0 +if [ $WITH_RUBY = "1" ] ; then + printf "Checking for ruby development files... " + if ruby -e "require 'mkmf'"> /dev/null 2>&1; then + printf "Yes.\n" + have_ruby_dev=1 + else + printf "No (skipping ruby bindings)\n" + fi +fi + +have_sphinx=0 +if [ $WITH_DOCS = "1" ] ; then + printf "Checking if sphinx is available and supports nroff output... " + if command -v sphinx-build > /dev/null && ${python} -m sphinx.writers.manpage > /dev/null 2>&1 ; then + printf "Yes.\n" + have_sphinx=1 + else + printf "No (so will not install man pages).\n" + fi fi libdir_in_ldconfig=0 @@ -542,7 +619,7 @@ EOF echo fi if [ $have_gmime -eq 0 ]; then - echo " Either GMime 2.4 library" $GMIME_24_VERSION_CTR "or GMime 2.6 library" $GMIME_26_VERSION_CTR + echo " GMime 2.6 library >= $GMIME_MINVER" echo " (including development files such as headers)" echo " http://spruce.sourceforge.net/gmime/" echo @@ -690,8 +767,6 @@ else fi rm -f compat/check_asctime -printf "int main(void){return 0;}\n" > minimal.c - printf "Checking for rpath support... " if ${CC} -Wl,--enable-new-dtags -Wl,-rpath,/tmp/ -o minimal minimal.c >/dev/null 2>&1 then @@ -712,6 +787,16 @@ else as_needed_ldflags="" fi +printf "Checking for -Wl,--no-undefined... " +if ${CC} -Wl,--no-undefined -o minimal minimal.c >/dev/null 2>&1 +then + printf "Yes.\n" + no_undefined_ldflags="-Wl,--no-undefined" +else + printf "No (nothing to worry about).\n" + no_undefined_ldflags="" +fi + WARN_CXXFLAGS="" printf "Checking for available C++ compiler warning flags... " for flag in -Wall -Wextra -Wwrite-strings; do @@ -732,7 +817,7 @@ for flag in -Wmissing-declarations; do done printf "\n\t${WARN_CFLAGS}\n" -rm -f minimal minimal.c +rm -f minimal minimal.c _libversion.c _libversion _libversion.sh # construct the Makefile.config cat > Makefile.config < Read input from given file, instead of from stdin. Implies ``--batch``. diff --git a/doc/man1/notmuch.rst b/doc/man1/notmuch.rst index b33738ed..3acfbdb4 100644 --- a/doc/man1/notmuch.rst +++ b/doc/man1/notmuch.rst @@ -39,15 +39,28 @@ OPTIONS Supported global options for ``notmuch`` include - ``--help`` - Print a synopsis of available commands and exit. + ``--help`` [command-name] + Print a synopsis of available commands and exit. + With an optional command name, show the man page + for that subcommand. ``--version`` - Print the installed version of notmuch, and exit. + Print the installed version of notmuch, and exit. ``--config=FILE`` - Specify the configuration file to use. This overrides any - configuration file specified by ${NOTMUCH\_CONFIG}. + Specify the configuration file to use. This overrides any + configuration file specified by ${NOTMUCH\_CONFIG}. + + ``--uuid=HEX`` + Enforce that the database UUID (a unique identifier which + persists until e.g. the database is compacted) + is HEX; exit with an error if it is not. This is useful to + detect rollover in modification counts on messages. You can + find this UUID using e.g. ``notmuch count --lastmod`` + +All global options except ``--config`` can also be specified after the +command. For example, ``notmuch subcommand --uuid=HEX`` is +equivalent to ``notmuch --uuid=HEX subcommand``. COMMANDS ======== diff --git a/doc/man7/notmuch-search-terms.rst b/doc/man7/notmuch-search-terms.rst index 1d27ac1e..11a3da3d 100644 --- a/doc/man7/notmuch-search-terms.rst +++ b/doc/man7/notmuch-search-terms.rst @@ -54,6 +54,8 @@ indicate user-supplied values): - date:.. +- lastmod:.. + The **from:** prefix is used to match the name or address of the sender of an email message. @@ -124,6 +126,12 @@ The time range can also be specified using timestamps with a syntax of: Each timestamp is a number representing the number of seconds since 1970-01-01 00:00:00 UTC. +The **lastmod:** prefix can be used to restrict the result by the +database revision number of when messages were last modified (tags +were added/removed or filenames changed). This is usually used in +conjunction with the **--uuid** argument to **notmuch search** +to find messages that have changed since an earlier query. + Operators --------- @@ -270,6 +278,13 @@ In this case, is taken as the earliest time it could describe could describe (the end of yesterday). Similarly, date:january..february matches from the beginning of January to the end of February. +date:..! can be used as a shorthand for date:... The +expansion takes place before interpretation, and thus, for example, +date:monday..! matches from the beginning of Monday until the end of +Monday. (Note that entering date: without "..", for example +date:yesterday, won't work, as it's not interpreted as a range +expression at all. Again, use date:yesterday..!) + Currently, we do not support spaces in range expressions. You can replace the spaces with '\_', or (in most cases) '-', or (in some cases) leave the spaces out altogether. Examples in this man page use spaces @@ -280,11 +295,6 @@ to specify date:.. or date:.. to not limit the start or end time, respectively. Pre-1.2.1 Xapian does not report an error on open ended ranges, but it does not work as expected either. -Entering date:expr without ".." (for example date:yesterday) won't work, -as it's not interpreted as a range expression at all. You can achieve -the expected result by duplicating the expr both sides of ".." (for -example date:yesterday..yesterday). - Relative date and time ---------------------- diff --git a/emacs/notmuch-hello.el b/emacs/notmuch-hello.el index 65d06276..7bfa752d 100644 --- a/emacs/notmuch-hello.el +++ b/emacs/notmuch-hello.el @@ -628,7 +628,7 @@ with `notmuch-hello-query-counts'." (defun notmuch-hello-versions () "Display the notmuch version(s)" (interactive) - (let ((notmuch-cli-version (notmuch-version))) + (let ((notmuch-cli-version (notmuch-cli-version))) (message "notmuch version %s" (if (string= notmuch-emacs-version notmuch-cli-version) notmuch-cli-version diff --git a/emacs/notmuch-lib.el b/emacs/notmuch-lib.el index f8e51658..201d7ec8 100644 --- a/emacs/notmuch-lib.el +++ b/emacs/notmuch-lib.el @@ -25,6 +25,10 @@ (require 'mm-decode) (require 'cl) +(unless (require 'notmuch-version nil t) + (defconst notmuch-emacs-version "unknown" + "Placeholder variable when notmuch-version.el[c] is not available.")) + (autoload 'notmuch-jump-search "notmuch-jump" "Jump to a saved search by shortcut key." t) @@ -192,8 +196,8 @@ Otherwise the output will be returned" "Perhaps you haven't run \"notmuch setup\" yet? Try running this on the command line, and then retry your notmuch command"))) -(defun notmuch-version () - "Return a string with the notmuch version number." +(defun notmuch-cli-version () + "Return a string with the notmuch cli command version number." (let ((long-string ;; Trim off the trailing newline. (substring (notmuch-command-to-string "--version") 0 -1))) diff --git a/emacs/notmuch-maildir-fcc.el b/emacs/notmuch-maildir-fcc.el index 07eedba2..c2f2f4cb 100644 --- a/emacs/notmuch-maildir-fcc.el +++ b/emacs/notmuch-maildir-fcc.el @@ -59,23 +59,19 @@ yet when sending a mail." :require 'notmuch-fcc-initialization :group 'notmuch-send) -(defun notmuch-fcc-initialization () - "If notmuch-fcc-directories is set, - hook them into the message-fcc-handler-function" - ;; Set up the message-fcc-handler to move mails to the maildir in Fcc - ;; The parameter is set to mark messages as "seen" - (setq message-fcc-handler-function - (lambda (destdir) - (notmuch-maildir-fcc-write-buffer-to-maildir destdir t))) - ;; add a hook to actually insert the Fcc header when sending - (add-hook 'message-header-setup-hook 'notmuch-fcc-header-setup)) +(defun notmuch-fcc-handler (destdir) + "Write buffer to `destdir', marking it as sent + +Intended to be dynamically bound to `message-fcc-handler-function'" + (notmuch-maildir-fcc-write-buffer-to-maildir destdir t)) (defun notmuch-fcc-header-setup () "Add an Fcc header to the current message buffer. -Can be added to `message-send-hook' and will set the Fcc header -based on the values of `notmuch-fcc-dirs'. An existing Fcc header -will NOT be removed or replaced." +Sets the Fcc header based on the values of `notmuch-fcc-dirs'. + +Originally intended to be use a hook function, but now called directly +by notmuch-mua-mail" (let ((subdir (cond @@ -213,6 +209,5 @@ return t if successful, and nil otherwise." (delete-file (concat destdir "/tmp/" msg-id)))) t))) -(notmuch-fcc-initialization) (provide 'notmuch-maildir-fcc) diff --git a/emacs/notmuch-mua.el b/emacs/notmuch-mua.el index 33f13998..57465b20 100644 --- a/emacs/notmuch-mua.el +++ b/emacs/notmuch-mua.el @@ -50,7 +50,7 @@ window/frame that will be destroyed when the buffer is killed. You may want to customize `message-kill-buffer-on-exit' accordingly." (when (< emacs-major-version 24) - " Due to a known bug in Emacs 23, you should not set + " Due to a known bug in Emacs 23, you should not set this to `new-window' if `message-kill-buffer-on-exit' is disabled: this would result in an incorrect behavior.")) :group 'notmuch-send @@ -118,7 +118,10 @@ Note that these functions use `mail-citation-hook' if that is non-nil." (defun notmuch-mua-user-agent-notmuch () "Generate a `User-Agent:' string suitable for notmuch." - (concat "Notmuch/" (notmuch-version) " (http://notmuchmail.org)")) + (let ((notmuch-version (if (string= notmuch-emacs-version "unknown") + (notmuch-cli-version) + notmuch-emacs-version))) + (concat "Notmuch/" notmuch-version " (http://notmuchmail.org)"))) (defun notmuch-mua-user-agent-emacs () "Generate a `User-Agent:' string suitable for notmuch." @@ -265,6 +268,12 @@ Note that these functions use `mail-citation-hook' if that is non-nil." (message-goto-body) (set-buffer-modified-p nil)) +(define-derived-mode notmuch-message-mode message-mode "Message[Notmuch]" + "Notmuch message composition mode. Mostly like `message-mode'") + +(define-key notmuch-message-mode-map (kbd "C-c C-c") #'notmuch-mua-send-and-exit) +(define-key notmuch-message-mode-map (kbd "C-c C-s") #'notmuch-mua-send) + (defun notmuch-mua-mail (&optional to subject other-headers &rest other-args) "Invoke the notmuch mail composition window. @@ -281,6 +290,8 @@ OTHER-ARGS are passed through to `message-mail'." (notmuch-user-name) " <" (notmuch-user-primary-email) ">")) other-headers)) (apply #'message-mail to subject other-headers other-args) + (notmuch-message-mode) + (notmuch-fcc-header-setup) (message-sort-headers) (message-hide-headers) (set-buffer-modified-p nil) @@ -394,7 +405,13 @@ will be addressed to all recipients of the source message." (defun notmuch-mua-send-and-exit (&optional arg) (interactive "P") - (message-send-and-exit arg)) + (let ((message-fcc-handler-function #'notmuch-fcc-handler)) + (message-send-and-exit arg))) + +(defun notmuch-mua-send (&optional arg) + (interactive "P") + (let ((message-fcc-handler-function #'notmuch-fcc-handler)) + (message-send arg))) (defun notmuch-mua-kill-buffer () (interactive) diff --git a/emacs/notmuch-show.el b/emacs/notmuch-show.el index 2a53461e..4dee34bf 100644 --- a/emacs/notmuch-show.el +++ b/emacs/notmuch-show.el @@ -47,6 +47,7 @@ (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) +(declare-function notmuch-read-query "notmuch" (prompt)) (defcustom notmuch-message-headers '("Subject" "To" "Cc" "Date") "Headers that should be shown in a message, in this order. @@ -1281,6 +1282,16 @@ This includes: ")") notmuch-show-thread-id)) +(defun notmuch-show-goto-message (msg-id) + "Go to message with msg-id." + (goto-char (point-min)) + (unless (loop if (string= msg-id (notmuch-show-get-message-id)) + return t + until (not (notmuch-show-goto-message-next))) + (goto-char (point-min)) + (message "Message-id not found.")) + (notmuch-show-message-adjust)) + (defun notmuch-show-apply-state (state) "Apply STATE to the current buffer. @@ -1298,13 +1309,7 @@ This includes: until (not (notmuch-show-goto-message-next))) ;; Go to the previously open message. - (goto-char (point-min)) - (unless (loop if (string= current (notmuch-show-get-message-id)) - return t - until (not (notmuch-show-goto-message-next))) - (goto-char (point-min)) - (message "Previously current message not found.")) - (notmuch-show-message-adjust))) + (notmuch-show-goto-message current))) (defun notmuch-show-refresh-view (&optional reset-state) "Refresh the current view. @@ -1368,6 +1373,7 @@ reset based on the original query." (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 "l" 'notmuch-show-filter-thread) (define-key map "r" 'notmuch-show-reply-sender) (define-key map "R" 'notmuch-show-reply) (define-key map "|" 'notmuch-show-pipe-message) @@ -1656,6 +1662,16 @@ user decision and we should not override it." (save-excursion (funcall notmuch-show-mark-read-function (window-start) (window-end))))) +(defun notmuch-show-filter-thread (query) + "Filter or LIMIT the current thread based on a new query string. + +Reshows the current thread with matches defined by the new query-string." + (interactive (list (notmuch-read-query "Filter thread: "))) + (let ((msg-id (notmuch-show-get-message-id))) + (setq notmuch-show-query-context (if (string= query "") nil query)) + (notmuch-show-refresh-view t) + (notmuch-show-goto-message msg-id))) + ;; Functions for getting attributes of several messages in the current ;; thread. diff --git a/emacs/notmuch-tag.el b/emacs/notmuch-tag.el index f54aa9d6..c7f62c90 100644 --- a/emacs/notmuch-tag.el +++ b/emacs/notmuch-tag.el @@ -265,7 +265,7 @@ changed (the normal case) are shown using formats from (defcustom notmuch-before-tag-hook nil "Hooks that are run before tags of a message are modified. -'tags' will contain the tags that are about to be added or removed as +'tag-changes' will contain the tags that are about to be added or removed as a list of strings of the form \"+TAG\" or \"-TAG\". 'query' will be a string containing the search query that determines the messages that are about to be tagged" @@ -277,7 +277,7 @@ the messages that are about to be tagged" (defcustom notmuch-after-tag-hook nil "Hooks that are run after tags of a message are modified. -'tags' will contain the tags that were added or removed as +'tag-changes' will contain the tags that were added or removed as a list of strings of the form \"+TAG\" or \"-TAG\". 'query' will be a string containing the search query that determines the messages that were tagged" diff --git a/emacs/notmuch.el b/emacs/notmuch.el index ab004543..463b9262 100644 --- a/emacs/notmuch.el +++ b/emacs/notmuch.el @@ -61,10 +61,6 @@ (require 'notmuch-message) (require 'notmuch-parser) -(unless (require 'notmuch-version nil t) - (defconst notmuch-emacs-version "unknown" - "Placeholder variable when notmuch-version.el[c] is not available.")) - (defcustom notmuch-search-result-format `(("date" . "%12s ") ("count" . "%-7s ") @@ -181,6 +177,7 @@ there will be called at other points of notmuch execution." (defvar notmuch-search-stash-map (let ((map (make-sparse-keymap))) (define-key map "i" 'notmuch-search-stash-thread-id) + (define-key map "q" 'notmuch-stash-query) (define-key map "?" 'notmuch-subkeymap-help) map) "Submap for stash commands") @@ -191,6 +188,11 @@ there will be called at other points of notmuch execution." (interactive) (notmuch-common-do-stash (notmuch-search-find-thread-id))) +(defun notmuch-stash-query () + "Copy current query to kill-ring." + (interactive) + (notmuch-common-do-stash (notmuch-search-get-query))) + (defvar notmuch-search-query-string) (defvar notmuch-search-target-thread) (defvar notmuch-search-target-line) @@ -855,13 +857,15 @@ See `notmuch-tag' for information on the format of TAG-CHANGES." "Read a notmuch-query from the minibuffer with completion. PROMPT is the string to prompt with." - (lexical-let - ((completions - (append (list "folder:" "path:" "thread:" "id:" "date:" "from:" "to:" - "subject:" "attachment:" "mimetype:") - (mapcar (lambda (tag) - (concat "tag:" (notmuch-escape-boolean-term tag))) - (process-lines notmuch-command "search" "--output=tags" "*"))))) + (lexical-let* + ((all-tags + (mapcar (lambda (tag) (notmuch-escape-boolean-term tag)) + (process-lines notmuch-command "search" "--output=tags" "*"))) + (completions + (append (list "folder:" "path:" "thread:" "id:" "date:" "from:" "to:" + "subject:" "attachment:" "mimetype:") + (mapcar (lambda (tag) (concat "tag:" tag)) all-tags) + (mapcar (lambda (tag) (concat "is:" tag)) all-tags)))) (let ((keymap (copy-keymap minibuffer-local-map)) (current-query (case major-mode (notmuch-search-mode (notmuch-search-get-query)) @@ -974,18 +978,28 @@ default sort order is defined by `notmuch-search-oldest-first'." (set 'notmuch-search-oldest-first (not notmuch-search-oldest-first)) (notmuch-search-refresh-view)) +(defun notmuch-group-disjunctive-query-string (query-string) + "Group query if it contains a complex expression. + +Enclose QUERY-STRING in parentheses if it matches +`notmuch-search-disjunctive-regexp'." + (if (string-match-p notmuch-search-disjunctive-regexp query-string) + (concat "( " query-string " )") + query-string)) + (defun notmuch-search-filter (query) "Filter the current search results based on an additional query string. Runs a new search matching only messages that match both the current search results AND the additional query string provided." (interactive (list (notmuch-read-query "Filter search: "))) - (let ((grouped-query (if (string-match-p notmuch-search-disjunctive-regexp query) - (concat "( " query " )") - query))) - (notmuch-search (if (string= notmuch-search-query-string "*") + (let ((grouped-query (notmuch-group-disjunctive-query-string query)) + (grouped-original-query (notmuch-group-disjunctive-query-string + notmuch-search-query-string))) + (notmuch-search (if (string= grouped-original-query "*") grouped-query - (concat notmuch-search-query-string " and " grouped-query)) notmuch-search-oldest-first))) + (concat grouped-original-query " and " grouped-query)) + notmuch-search-oldest-first))) (defun notmuch-search-filter-by-tag (tag) "Filter the current search results based on a single tag. diff --git a/lib/Makefile.local b/lib/Makefile.local index f9ecd50e..3a070907 100644 --- a/lib/Makefile.local +++ b/lib/Makefile.local @@ -1,26 +1,5 @@ # -*- makefile -*- -# The major version of the library interface. This will control the soname. -# As such, this number must be incremented for any incompatible change to -# the library interface, (such as the deletion of an API or a major -# semantic change that breaks formerly functioning code). -# -LIBNOTMUCH_VERSION_MAJOR = 4 - -# The minor version of the library interface. This should be incremented at -# the time of release for any additions to the library interface, -# (and when it is incremented, the release version of the library should -# be reset to 0). -LIBNOTMUCH_VERSION_MINOR = 2 - -# The release version the library interface. This should be incremented at -# the time of release if there have been no changes to the interface, (but -# simply compatible changes to the implementation). -LIBNOTMUCH_VERSION_RELEASE = 0 - -# Note: Don't forget to change the VERSION macros in notmuch.h when -# any of the above change. - ifeq ($(PLATFORM),MACOSX) LIBRARY_SUFFIX = dylib # On OS X, library version numbers go before suffix. @@ -33,7 +12,7 @@ LIBRARY_SUFFIX = so LINKER_NAME = libnotmuch.$(LIBRARY_SUFFIX) SONAME = $(LINKER_NAME).$(LIBNOTMUCH_VERSION_MAJOR) LIBNAME = $(SONAME).$(LIBNOTMUCH_VERSION_MINOR).$(LIBNOTMUCH_VERSION_RELEASE) -LIBRARY_LINK_FLAG = -shared -Wl,--version-script=notmuch.sym,-soname=$(SONAME) -Wl,--no-undefined +LIBRARY_LINK_FLAG = -shared -Wl,--version-script=notmuch.sym,-soname=$(SONAME) $(NO_UNDEFINED_LDFLAGS) ifeq ($(PLATFORM),OPENBSD) LIBRARY_LINK_FLAG += -lc endif diff --git a/lib/database-private.h b/lib/database-private.h index 24243db2..3fb10f7a 100644 --- a/lib/database-private.h +++ b/lib/database-private.h @@ -100,6 +100,12 @@ enum _notmuch_features { * * Introduced: version 3. */ NOTMUCH_FEATURE_INDEXED_MIMETYPES = 1 << 5, + + /* If set, messages store the revision number of the last + * modification in NOTMUCH_VALUE_LAST_MOD. + * + * Introduced: version 3. */ + NOTMUCH_FEATURE_LAST_MOD = 1 << 6, }; /* In C++, a named enum is its own type, so define bitwise operators @@ -145,6 +151,8 @@ struct _notmuch_database { notmuch_database_mode_t mode; int atomic_nesting; + /* TRUE if changes have been made in this atomic section */ + notmuch_bool_t atomic_dirty; Xapian::Database *xapian_db; /* Bit mask of features used by this database. This is a @@ -158,10 +166,17 @@ struct _notmuch_database { * next library call. May be NULL */ char *status_string; + /* Highest committed revision number. Modifications are recorded + * under a higher revision number, which can be generated with + * notmuch_database_new_revision. */ + unsigned long revision; + const char *uuid; + Xapian::QueryParser *query_parser; Xapian::TermGenerator *term_gen; Xapian::ValueRangeProcessor *value_range_processor; Xapian::ValueRangeProcessor *date_range_processor; + Xapian::ValueRangeProcessor *last_mod_range_processor; }; /* Prior to database version 3, features were implied by the database @@ -179,7 +194,8 @@ struct _notmuch_database { * will have it). */ #define NOTMUCH_FEATURES_CURRENT \ (NOTMUCH_FEATURE_FILE_TERMS | NOTMUCH_FEATURE_DIRECTORY_DOCS | \ - NOTMUCH_FEATURE_BOOL_FOLDER | NOTMUCH_FEATURE_GHOSTS) + NOTMUCH_FEATURE_BOOL_FOLDER | NOTMUCH_FEATURE_GHOSTS | \ + NOTMUCH_FEATURE_LAST_MOD) /* Return the list of terms from the given iterator matching a prefix. * The prefix will be stripped from the strings in the returned list. diff --git a/lib/database.cc b/lib/database.cc index cffab62c..6d0e5a63 100644 --- a/lib/database.cc +++ b/lib/database.cc @@ -101,6 +101,9 @@ typedef struct { * * SUBJECT: The value of the "Subject" header * + * LAST_MOD: The revision number as of the last tag or + * filename change. + * * In addition, terms from the content of the message are added with * "from", "to", "attachment", and "subject" prefixes for use by the * user in searching. Similarly, terms from the path of the mail @@ -310,6 +313,8 @@ static const struct { * them. */ { NOTMUCH_FEATURE_INDEXED_MIMETYPES, "indexed MIME types", "w"}, + { NOTMUCH_FEATURE_LAST_MOD, + "modification tracking", "w"}, }; const char * @@ -342,6 +347,8 @@ notmuch_status_to_string (notmuch_status_t status) return "Unsupported operation"; case NOTMUCH_STATUS_UPGRADE_REQUIRED: return "Operation requires a database upgrade"; + case NOTMUCH_STATUS_PATH_ERROR: + return "Path supplied is illegal for this function"; default: case NOTMUCH_STATUS_LAST_STATUS: return "Unknown error status value"; @@ -657,6 +664,12 @@ notmuch_database_create_verbose (const char *path, goto DONE; } + if (path[0] != '/') { + message = strdup ("Error: Database path must be absolute.\n"); + status = NOTMUCH_STATUS_PATH_ERROR; + goto DONE; + } + err = stat (path, &st); if (err) { IGNORE_RESULT (asprintf (&message, "Error: Cannot create database at %s: %s.\n", @@ -729,6 +742,23 @@ _notmuch_database_ensure_writable (notmuch_database_t *notmuch) return NOTMUCH_STATUS_SUCCESS; } +/* Allocate a revision number for the next change. */ +unsigned long +_notmuch_database_new_revision (notmuch_database_t *notmuch) +{ + unsigned long new_revision = notmuch->revision + 1; + + /* If we're in an atomic section, hold off on updating the + * committed revision number until we commit the atomic section. + */ + if (notmuch->atomic_nesting) + notmuch->atomic_dirty = TRUE; + else + notmuch->revision = new_revision; + + return new_revision; +} + /* Parse a database features string from the given database version. * Returns the feature bit set. * @@ -847,6 +877,12 @@ notmuch_database_open_verbose (const char *path, goto DONE; } + if (path[0] != '/') { + message = strdup ("Error: Database path must be absolute.\n"); + status = NOTMUCH_STATUS_PATH_ERROR; + goto DONE; + } + if (! (notmuch_path = talloc_asprintf (local, "%s/%s", path, ".notmuch"))) { message = strdup ("Out of memory\n"); status = NOTMUCH_STATUS_OUT_OF_MEMORY; @@ -890,6 +926,7 @@ notmuch_database_open_verbose (const char *path, notmuch->atomic_nesting = 0; try { string last_thread_id; + string last_mod; if (mode == NOTMUCH_DATABASE_MODE_READ_WRITE) { notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path, @@ -948,11 +985,22 @@ notmuch_database_open_verbose (const char *path, INTERNAL_ERROR ("Malformed database last_thread_id: %s", str); } + /* Get current highest revision number. */ + last_mod = notmuch->xapian_db->get_value_upper_bound ( + NOTMUCH_VALUE_LAST_MOD); + if (last_mod.empty ()) + notmuch->revision = 0; + else + notmuch->revision = Xapian::sortable_unserialise (last_mod); + notmuch->uuid = talloc_strdup ( + notmuch, notmuch->xapian_db->get_uuid ().c_str ()); + notmuch->query_parser = new Xapian::QueryParser; notmuch->term_gen = new Xapian::TermGenerator; notmuch->term_gen->set_stemmer (Xapian::Stem ("english")); notmuch->value_range_processor = new Xapian::NumberValueRangeProcessor (NOTMUCH_VALUE_TIMESTAMP); notmuch->date_range_processor = new ParseTimeValueRangeProcessor (NOTMUCH_VALUE_TIMESTAMP); + notmuch->last_mod_range_processor = new Xapian::NumberValueRangeProcessor (NOTMUCH_VALUE_LAST_MOD, "lastmod:"); notmuch->query_parser->set_default_op (Xapian::Query::OP_AND); notmuch->query_parser->set_database (*notmuch->xapian_db); @@ -960,6 +1008,7 @@ notmuch_database_open_verbose (const char *path, notmuch->query_parser->set_stemming_strategy (Xapian::QueryParser::STEM_SOME); notmuch->query_parser->add_valuerangeprocessor (notmuch->value_range_processor); notmuch->query_parser->add_valuerangeprocessor (notmuch->date_range_processor); + notmuch->query_parser->add_valuerangeprocessor (notmuch->last_mod_range_processor); for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++) { prefix_t *prefix = &BOOLEAN_PREFIX_EXTERNAL[i]; @@ -1038,6 +1087,8 @@ notmuch_database_close (notmuch_database_t *notmuch) notmuch->value_range_processor = NULL; delete notmuch->date_range_processor; notmuch->date_range_processor = NULL; + delete notmuch->last_mod_range_processor; + notmuch->last_mod_range_processor = NULL; return status; } @@ -1336,7 +1387,7 @@ notmuch_database_upgrade (notmuch_database_t *notmuch, return NOTMUCH_STATUS_SUCCESS; if (progress_notify) { - /* Setup our handler for SIGALRM */ + /* Set up our handler for SIGALRM */ memset (&action, 0, sizeof (struct sigaction)); action.sa_handler = handle_sigalrm; sigemptyset (&action.sa_mask); @@ -1355,7 +1406,8 @@ notmuch_database_upgrade (notmuch_database_t *notmuch, /* Figure out how much total work we need to do. */ if (new_features & - (NOTMUCH_FEATURE_FILE_TERMS | NOTMUCH_FEATURE_BOOL_FOLDER)) { + (NOTMUCH_FEATURE_FILE_TERMS | NOTMUCH_FEATURE_BOOL_FOLDER | + NOTMUCH_FEATURE_LAST_MOD)) { notmuch_query_t *query = notmuch_query_create (notmuch, ""); total += notmuch_query_count_messages (query); notmuch_query_destroy (query); @@ -1382,12 +1434,15 @@ notmuch_database_upgrade (notmuch_database_t *notmuch, /* Perform per-message upgrades. */ if (new_features & - (NOTMUCH_FEATURE_FILE_TERMS | NOTMUCH_FEATURE_BOOL_FOLDER)) { + (NOTMUCH_FEATURE_FILE_TERMS | NOTMUCH_FEATURE_BOOL_FOLDER | + NOTMUCH_FEATURE_LAST_MOD)) { notmuch_query_t *query = notmuch_query_create (notmuch, ""); notmuch_messages_t *messages; notmuch_message_t *message; char *filename; + /* XXX: this should use the _st version, but needs an error + path */ for (messages = notmuch_query_search_messages (query); notmuch_messages_valid (messages); notmuch_messages_move_to_next (messages)) @@ -1419,6 +1474,14 @@ notmuch_database_upgrade (notmuch_database_t *notmuch, if (new_features & NOTMUCH_FEATURE_BOOL_FOLDER) _notmuch_message_upgrade_folder (message); + /* Prior to NOTMUCH_FEATURE_LAST_MOD, messages did not + * track modification revisions. Give all messages the + * next available revision; since we just started tracking + * revisions for this database, that will be 1. + */ + if (new_features & NOTMUCH_FEATURE_LAST_MOD) + _notmuch_message_upgrade_last_mod (message); + _notmuch_message_sync (message); notmuch_message_destroy (message); @@ -1601,11 +1664,25 @@ notmuch_database_end_atomic (notmuch_database_t *notmuch) return NOTMUCH_STATUS_XAPIAN_EXCEPTION; } + if (notmuch->atomic_dirty) { + ++notmuch->revision; + notmuch->atomic_dirty = FALSE; + } + DONE: notmuch->atomic_nesting--; return NOTMUCH_STATUS_SUCCESS; } +unsigned long +notmuch_database_get_revision (notmuch_database_t *notmuch, + const char **uuid) +{ + if (uuid) + *uuid = notmuch->uuid; + return notmuch->revision; +} + /* We allow the user to use arbitrarily long paths for directories. But * we have a term-length limit. So if we exceed that, we'll use the * SHA-1 of the path for the database term. @@ -2576,7 +2653,7 @@ notmuch_database_get_all_tags (notmuch_database_t *db) } const char * -notmuch_database_status_string (notmuch_database_t *notmuch) +notmuch_database_status_string (const notmuch_database_t *notmuch) { return notmuch->status_string; } diff --git a/lib/gen-version-script.sh b/lib/gen-version-script.sh index 64a73749..84770011 100644 --- a/lib/gen-version-script.sh +++ b/lib/gen-version-script.sh @@ -1,3 +1,4 @@ +set -eu # we go through a bit of work to get the unmangled names of the # typeinfo symbols because of diff --git a/lib/message-file.c b/lib/message-file.c index 8ac96e8e..ee305202 100644 --- a/lib/message-file.c +++ b/lib/message-file.c @@ -37,27 +37,6 @@ struct _notmuch_message_file { GMimeMessage *message; }; -static int -strcase_equal (const void *a, const void *b) -{ - return strcasecmp (a, b) == 0; -} - -static unsigned int -strcase_hash (const void *ptr) -{ - const char *s = ptr; - - /* This is the djb2 hash. */ - unsigned int hash = 5381; - while (s && *s) { - hash = ((hash << 5) + hash) + tolower (*s); - s++; - } - - return hash; -} - static int _notmuch_message_file_destructor (notmuch_message_file_t *message) { diff --git a/lib/message.cc b/lib/message.cc index 5bc7aff1..26b5e76e 100644 --- a/lib/message.cc +++ b/lib/message.cc @@ -43,6 +43,9 @@ struct visible _notmuch_message { * if each flag has been initialized. */ unsigned long lazy_flags; + /* Message document modified since last sync */ + notmuch_bool_t modified; + Xapian::Document doc; Xapian::termcount termpos; }; @@ -539,6 +542,7 @@ _notmuch_message_remove_terms (notmuch_message_t *message, const char *prefix) try { message->doc.remove_term ((*i)); + message->modified = TRUE; } catch (const Xapian::InvalidArgumentError) { /* Ignore failure to remove non-existent term. */ } @@ -793,6 +797,7 @@ void _notmuch_message_clear_data (notmuch_message_t *message) { message->doc.set_data (""); + message->modified = TRUE; } static void @@ -990,6 +995,17 @@ _notmuch_message_set_header_values (notmuch_message_t *message, Xapian::sortable_serialise (time_value)); message->doc.add_value (NOTMUCH_VALUE_FROM, from); message->doc.add_value (NOTMUCH_VALUE_SUBJECT, subject); + message->modified = TRUE; +} + +/* Upgrade a message to support NOTMUCH_FEATURE_LAST_MOD. The caller + * must call _notmuch_message_sync. */ +void +_notmuch_message_upgrade_last_mod (notmuch_message_t *message) +{ + /* _notmuch_message_sync will update the last modification + * revision; we just have to ask it to. */ + message->modified = TRUE; } /* Synchronize changes made to message->doc out into the database. */ @@ -1001,8 +1017,24 @@ _notmuch_message_sync (notmuch_message_t *message) if (message->notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY) return; + if (! message->modified) + return; + + /* Update the last modification of this message. */ + if (message->notmuch->features & NOTMUCH_FEATURE_LAST_MOD) + /* sortable_serialise gives a reasonably compact encoding, + * which directly translates to reduced IO when scanning the + * value stream. Since it's built for doubles, we only get 53 + * effective bits, but that's still enough for the database to + * last a few centuries at 1 million revisions per second. */ + message->doc.add_value (NOTMUCH_VALUE_LAST_MOD, + Xapian::sortable_serialise ( + _notmuch_database_new_revision ( + message->notmuch))); + db = static_cast (message->notmuch->xapian_db); db->replace_document (message->doc_id, message->doc); + message->modified = FALSE; } /* Delete a message document from the database. */ @@ -1077,6 +1109,7 @@ _notmuch_message_add_term (notmuch_message_t *message, return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG; message->doc.add_term (term, 0); + message->modified = TRUE; talloc_free (term); @@ -1145,6 +1178,7 @@ _notmuch_message_remove_term (notmuch_message_t *message, try { message->doc.remove_term (term); + message->modified = TRUE; } catch (const Xapian::InvalidArgumentError) { /* We'll let the philosopher's try to wrestle with the * question of whether failing to remove that which was not diff --git a/lib/notmuch-private.h b/lib/notmuch-private.h index cc9ce12c..5dd4770e 100644 --- a/lib/notmuch-private.h +++ b/lib/notmuch-private.h @@ -50,6 +50,7 @@ NOTMUCH_BEGIN_DECLS #include "xutil.h" #include "error_util.h" +#include "string-util.h" #pragma GCC visibility push(hidden) @@ -107,7 +108,8 @@ typedef enum { NOTMUCH_VALUE_TIMESTAMP = 0, NOTMUCH_VALUE_MESSAGE_ID, NOTMUCH_VALUE_FROM, - NOTMUCH_VALUE_SUBJECT + NOTMUCH_VALUE_SUBJECT, + NOTMUCH_VALUE_LAST_MOD, } notmuch_value_t; /* Xapian (with flint backend) complains if we provide a term longer @@ -194,6 +196,9 @@ void _notmuch_database_log (notmuch_database_t *notmuch, const char *format, ...); +unsigned long +_notmuch_database_new_revision (notmuch_database_t *notmuch); + const char * _notmuch_database_relative_path (notmuch_database_t *notmuch, const char *path); @@ -305,6 +310,10 @@ _notmuch_message_set_header_values (notmuch_message_t *message, const char *date, const char *from, const char *subject); + +void +_notmuch_message_upgrade_last_mod (notmuch_message_t *message); + void _notmuch_message_sync (notmuch_message_t *message); diff --git a/lib/notmuch.h b/lib/notmuch.h index 20c4e019..87756838 100644 --- a/lib/notmuch.h +++ b/lib/notmuch.h @@ -56,9 +56,11 @@ NOTMUCH_BEGIN_DECLS * version in Makefile.local. */ #define LIBNOTMUCH_MAJOR_VERSION 4 -#define LIBNOTMUCH_MINOR_VERSION 2 +#define LIBNOTMUCH_MINOR_VERSION 3 #define LIBNOTMUCH_MICRO_VERSION 0 +#define NOTMUCH_DEPRECATED(major,minor) \ + __attribute__ ((deprecated ("function deprecated as of libnotmuch " #major "." #minor))) #endif /* __DOXYGEN__ */ /** @@ -163,6 +165,11 @@ typedef enum _notmuch_status { * The operation requires a database upgrade. */ NOTMUCH_STATUS_UPGRADE_REQUIRED, + /** + * There is a problem with the proposed path, e.g. a relative path + * passed to a function expecting an absolute path. + */ + NOTMUCH_STATUS_PATH_ERROR, /** * Not an actual status value. Just a way to find out how many * valid status values there are. @@ -306,7 +313,7 @@ notmuch_database_open_verbose (const char *path, * */ const char * -notmuch_database_status_string (notmuch_database_t *notmuch); +notmuch_database_status_string (const notmuch_database_t *notmuch); /** * Commit changes and close the given notmuch database. @@ -460,6 +467,24 @@ notmuch_database_begin_atomic (notmuch_database_t *notmuch); notmuch_status_t notmuch_database_end_atomic (notmuch_database_t *notmuch); +/** + * Return the committed database revision and UUID. + * + * The database revision number increases monotonically with each + * commit to the database. Hence, all messages and message changes + * committed to the database (that is, visible to readers) have a last + * modification revision <= the committed database revision. Any + * messages committed in the future will be assigned a modification + * revision > the committed database revision. + * + * The UUID is a NUL-terminated opaque string that uniquely identifies + * this database. Two revision numbers are only comparable if they + * have the same database UUID. + */ +unsigned long +notmuch_database_get_revision (notmuch_database_t *notmuch, + const char **uuid); + /** * Retrieve a directory object from the database for 'path'. * @@ -703,7 +728,13 @@ typedef enum { * Return the query_string of this query. See notmuch_query_create. */ const char * -notmuch_query_get_query_string (notmuch_query_t *query); +notmuch_query_get_query_string (const notmuch_query_t *query); + +/** + * Return the notmuch database of this query. See notmuch_query_create. + */ +notmuch_database_t * +notmuch_query_get_database (const notmuch_query_t *query); /** * Exclude values for notmuch_query_set_omit_excluded. The strange @@ -760,7 +791,7 @@ notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort); * notmuch_query_set_sort. */ notmuch_sort_t -notmuch_query_get_sort (notmuch_query_t *query); +notmuch_query_get_sort (const notmuch_query_t *query); /** * Add a tag that will be excluded from the query results by default. @@ -807,19 +838,24 @@ notmuch_query_add_tag_exclude (notmuch_query_t *query, const char *tag); * notmuch_threads_destroy function, but there's no good reason * to call it if the query is about to be destroyed). * - * If a Xapian exception occurs this function will return NULL. - * For better error reporting, use the _st variant. - */ -notmuch_threads_t * -notmuch_query_search_threads (notmuch_query_t *query); - -/** - * Like notmuch_query_search_threads, but with a status return. */ notmuch_status_t notmuch_query_search_threads_st (notmuch_query_t *query, notmuch_threads_t **out); +/** + * Like notmuch_query_search_threads_st, but without a status return. + * + * If a Xapian exception occurs this function will return NULL. + * + * @deprecated Deprecated as of libnotmuch 4.3 (notmuch 0.21). Please + * use notmuch_query_search_threads_st instead. + * + */ +NOTMUCH_DEPRECATED(4,3) +notmuch_threads_t * +notmuch_query_search_threads (notmuch_query_t *query); + /** * Execute a query for messages, returning a notmuch_messages_t object * which can be used to iterate over the results. The returned @@ -858,17 +894,23 @@ notmuch_query_search_threads_st (notmuch_query_t *query, * reason to call it if the query is about to be destroyed). * * If a Xapian exception occurs this function will return NULL. - * For better error reporting, use the _st variant. - */ -notmuch_messages_t * -notmuch_query_search_messages (notmuch_query_t *query); - -/** - * Like notmuch_query_search_messages, but with a status return. + * */ notmuch_status_t notmuch_query_search_messages_st (notmuch_query_t *query, notmuch_messages_t **out); +/** + * Like notmuch_query_search_messages, but without a status return. + * + * If a Xapian exception occurs this function will return NULL. + * + * @deprecated Deprecated as of libnotmuch 4.3 (notmuch 0.21). Please use + * notmuch_query_search_messages_st instead. + * + */ +NOTMUCH_DEPRECATED(4,3) +notmuch_messages_t * +notmuch_query_search_messages (notmuch_query_t *query); /** * Destroy a notmuch_query_t along with any associated resources. diff --git a/lib/parse-time-vrp.cc b/lib/parse-time-vrp.cc index 33f07db3..03804cf5 100644 --- a/lib/parse-time-vrp.cc +++ b/lib/parse-time-vrp.cc @@ -31,6 +31,7 @@ Xapian::valueno ParseTimeValueRangeProcessor::operator() (std::string &begin, std::string &end) { time_t t, now; + std::string b; /* Require date: prefix in start of the range... */ if (STRNCMP_LITERAL (begin.c_str (), PREFIX)) @@ -38,6 +39,7 @@ ParseTimeValueRangeProcessor::operator() (std::string &begin, std::string &end) /* ...and remove it. */ begin.erase (0, sizeof (PREFIX) - 1); + b = begin; /* Use the same 'now' for begin and end. */ if (time (&now) == (time_t) -1) @@ -51,6 +53,9 @@ ParseTimeValueRangeProcessor::operator() (std::string &begin, std::string &end) } if (!end.empty ()) { + if (end == "!" && ! b.empty ()) + end = b; + if (parse_time_string (end.c_str (), &t, &now, PARSE_TIME_ROUND_UP_INCLUSIVE)) return Xapian::BAD_VALUENO; diff --git a/lib/query.cc b/lib/query.cc index 9cedb6a8..8cf0a077 100644 --- a/lib/query.cc +++ b/lib/query.cc @@ -98,7 +98,7 @@ notmuch_query_create (notmuch_database_t *notmuch, } const char * -notmuch_query_get_query_string (notmuch_query_t *query) +notmuch_query_get_query_string (const notmuch_query_t *query) { return query->query_string; } @@ -117,7 +117,7 @@ notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort) } notmuch_sort_t -notmuch_query_get_sort (notmuch_query_t *query) +notmuch_query_get_sort (const notmuch_query_t *query) { return query->sort; } @@ -618,10 +618,14 @@ notmuch_query_count_threads (notmuch_query_t *query) GHashTable *hash; unsigned int count; notmuch_sort_t sort; + notmuch_status_t status; sort = query->sort; query->sort = NOTMUCH_SORT_UNSORTED; - messages = notmuch_query_search_messages (query); + status = notmuch_query_search_messages_st (query, &messages); + if (status) + return 0; + query->sort = sort; if (messages == NULL) return 0; @@ -654,3 +658,9 @@ notmuch_query_count_threads (notmuch_query_t *query) return count; } + +notmuch_database_t * +notmuch_query_get_database (const notmuch_query_t *query) +{ + return query->notmuch; +} diff --git a/lib/thread.cc b/lib/thread.cc index 9847cf8b..c8e58c33 100644 --- a/lib/thread.cc +++ b/lib/thread.cc @@ -504,6 +504,8 @@ _notmuch_thread_create (void *ctx, * oldest or newest subject is desired. */ notmuch_query_set_sort (thread_id_query, NOTMUCH_SORT_OLDEST_FIRST); + /* XXX: this should use the _st version, but it needs an error path + */ for (messages = notmuch_query_search_messages (thread_id_query); notmuch_messages_valid (messages); notmuch_messages_move_to_next (messages)) diff --git a/mime-node.c b/mime-node.c index fd9e4a45..e96e6639 100644 --- a/mime-node.c +++ b/mime-node.c @@ -129,8 +129,6 @@ DONE: return status; } -#ifdef GMIME_ATLEAST_26 - /* Signature list destructor (GMime 2.6) */ static int _signature_list_free (GMimeSignatureList **proxy) @@ -205,87 +203,6 @@ node_decrypt_and_verify (mime_node_t *node, GMimeObject *part, g_error_free (err); } -#else /* GMIME_ATLEAST_26 */ - -/* Signature validity destructor (GMime 2.4) */ -static int -_signature_validity_free (GMimeSignatureValidity **proxy) -{ - g_mime_signature_validity_free (*proxy); - return 0; -} - -/* Set up signature validity destructor (GMime 2.4) */ -static void -set_signature_validity_destructor (mime_node_t *node, - GMimeSignatureValidity *sig_validity) -{ - GMimeSignatureValidity **proxy = talloc (node, GMimeSignatureValidity *); - if (proxy) { - *proxy = sig_validity; - talloc_set_destructor (proxy, _signature_validity_free); - } -} - -/* Verify a signed mime node (GMime 2.4) */ -static void -node_verify (mime_node_t *node, GMimeObject *part, - notmuch_crypto_context_t *cryptoctx) -{ - GError *err = NULL; - GMimeSignatureValidity *sig_validity; - - node->verify_attempted = TRUE; - sig_validity = g_mime_multipart_signed_verify - (GMIME_MULTIPART_SIGNED (part), cryptoctx, &err); - node->sig_validity = sig_validity; - if (sig_validity) { - set_signature_validity_destructor (node, sig_validity); - } else { - fprintf (stderr, "Failed to verify signed part: %s\n", - err ? err->message : "no error explanation given"); - } - - if (err) - g_error_free (err); -} - -/* Decrypt and optionally verify an encrypted mime node (GMime 2.4) */ -static void -node_decrypt_and_verify (mime_node_t *node, GMimeObject *part, - notmuch_crypto_context_t *cryptoctx) -{ - GError *err = NULL; - GMimeMultipartEncrypted *encrypteddata = GMIME_MULTIPART_ENCRYPTED (part); - - node->decrypt_attempted = TRUE; - node->decrypted_child = g_mime_multipart_encrypted_decrypt - (encrypteddata, cryptoctx, &err); - if (! node->decrypted_child) { - fprintf (stderr, "Failed to decrypt part: %s\n", - err ? err->message : "no error explanation given"); - goto DONE; - } - - node->decrypt_success = TRUE; - node->verify_attempted = TRUE; - - /* The GMimeSignatureValidity returned here is a const, unlike the - * one returned by g_mime_multipart_signed_verify() in - * node_verify() above, so the destructor is not needed. - */ - node->sig_validity = g_mime_multipart_encrypted_get_signature_validity (encrypteddata); - if (! node->sig_validity) - fprintf (stderr, "Failed to verify encrypted signed part: %s\n", - err ? err->message : "no error explanation given"); - - DONE: - if (err) - g_error_free (err); -} - -#endif /* GMIME_ATLEAST_26 */ - static mime_node_t * _mime_node_create (mime_node_t *parent, GMimeObject *part) { diff --git a/notmuch-client.h b/notmuch-client.h index fb3021cc..3bd2903e 100644 --- a/notmuch-client.h +++ b/notmuch-client.h @@ -30,16 +30,7 @@ #include -/* GMIME_CHECK_VERSION in gmime 2.4 is not usable from the - * preprocessor (it calls a runtime function). But since - * GMIME_MAJOR_VERSION and friends were added in gmime 2.6, we can use - * these to check the version number. */ -#ifdef GMIME_MAJOR_VERSION -#define GMIME_ATLEAST_26 typedef GMimeCryptoContext notmuch_crypto_context_t; -#else -typedef GMimeCipherContext notmuch_crypto_context_t; -#endif #include "notmuch.h" @@ -57,6 +48,7 @@ typedef GMimeCipherContext notmuch_crypto_context_t; #include #include #include +#include #include "talloc-extra.h" @@ -394,17 +386,10 @@ struct mime_node { /* True if signature verification on this part was attempted. */ notmuch_bool_t verify_attempted; -#ifdef GMIME_ATLEAST_26 + /* The list of signatures for signed or encrypted containers. If * there are no signatures, this will be NULL. */ GMimeSignatureList* sig_list; -#else - /* For signed or encrypted containers, the validity of the - * signature. May be NULL if signature verification failed. If - * there are simply no signatures, this will be non-NULL with an - * empty signers list. */ - const GMimeSignatureValidity *sig_validity; -#endif /* Internal: Context inherited from the root iterator. */ struct mime_node_context *ctx; @@ -465,5 +450,22 @@ notmuch_database_dump (notmuch_database_t *notmuch, dump_format_t output_format, notmuch_bool_t gzip_output); +/* If status is non-zero (i.e. error) print appropriate + messages to stderr. +*/ + +notmuch_status_t +print_status_query (const char *loc, + const notmuch_query_t *query, + notmuch_status_t status); + #include "command-line-arguments.h" + +extern char *notmuch_requested_db_uuid; +extern const notmuch_opt_desc_t notmuch_shared_options []; +void notmuch_exit_if_unmatched_db_uuid (notmuch_database_t *notmuch); + +void notmuch_process_shared_options (const char* subcommand_name); +int notmuch_minimal_options (const char* subcommand_name, + int argc, char **argv); #endif diff --git a/notmuch-compact.c b/notmuch-compact.c index 2fc012a9..93737216 100644 --- a/notmuch-compact.c +++ b/notmuch-compact.c @@ -38,12 +38,21 @@ notmuch_compact_command (notmuch_config_t *config, int argc, char *argv[]) notmuch_opt_desc_t options[] = { { NOTMUCH_OPT_STRING, &backup_path, "backup", 0, 0 }, { NOTMUCH_OPT_BOOLEAN, &quiet, "quiet", 'q', 0 }, + { NOTMUCH_OPT_INHERIT, (void *) ¬much_shared_options, NULL, 0, 0 }, + { 0, 0, 0, 0, 0} }; opt_index = parse_arguments (argc, argv, options, 1); if (opt_index < 0) return EXIT_FAILURE; + if (notmuch_requested_db_uuid) { + fprintf (stderr, "Error: --uuid not implemented for compact\n"); + return EXIT_FAILURE; + } + + notmuch_process_shared_options (argv[0]); + if (! quiet) printf ("Compacting database...\n"); ret = notmuch_database_compact (path, backup_path, diff --git a/notmuch-config.c b/notmuch-config.c index 2d5c297b..d252bb25 100644 --- a/notmuch-config.c +++ b/notmuch-config.c @@ -872,8 +872,19 @@ int notmuch_config_command (notmuch_config_t *config, int argc, char *argv[]) { int ret; + int opt_index; - argc--; argv++; /* skip subcommand argument */ + opt_index = notmuch_minimal_options ("config", argc, argv); + if (opt_index < 0) + return EXIT_FAILURE; + + if (notmuch_requested_db_uuid) + fprintf (stderr, "Warning: ignoring --uuid=%s\n", + notmuch_requested_db_uuid); + + /* skip at least subcommand argument */ + argc-= opt_index; + argv+= opt_index; if (argc < 1) { fprintf (stderr, "Error: notmuch config requires at least one argument.\n"); diff --git a/notmuch-count.c b/notmuch-count.c index 6058f7c9..09613e6a 100644 --- a/notmuch-count.c +++ b/notmuch-count.c @@ -33,17 +33,19 @@ enum { EXCLUDE_FALSE, }; -static unsigned int +/* Return the number of files matching the query, or -1 for an error */ +static int count_files (notmuch_query_t *query) { notmuch_messages_t *messages; notmuch_message_t *message; notmuch_filenames_t *filenames; - unsigned int count = 0; + notmuch_status_t status; + int count = 0; - messages = notmuch_query_search_messages (query); - if (messages == NULL) - return 0; + status = notmuch_query_search_messages_st (query, &messages); + if (print_status_query ("notmuch count", query, status)) + return -1; for (; notmuch_messages_valid (messages); @@ -67,10 +69,14 @@ count_files (notmuch_query_t *query) static int print_count (notmuch_database_t *notmuch, const char *query_str, - const char **exclude_tags, size_t exclude_tags_length, int output) + const char **exclude_tags, size_t exclude_tags_length, int output, int print_lastmod) { notmuch_query_t *query; size_t i; + int count; + unsigned long revision; + const char *uuid; + int ret = 0; query = notmuch_query_create (notmuch, query_str); if (query == NULL) { @@ -83,34 +89,48 @@ print_count (notmuch_database_t *notmuch, const char *query_str, switch (output) { case OUTPUT_MESSAGES: - printf ("%u\n", notmuch_query_count_messages (query)); + printf ("%u", notmuch_query_count_messages (query)); break; case OUTPUT_THREADS: - printf ("%u\n", notmuch_query_count_threads (query)); + printf ("%u", notmuch_query_count_threads (query)); break; case OUTPUT_FILES: - printf ("%u\n", count_files (query)); + count = count_files (query); + if (count >= 0) { + printf ("%u", count); + } else { + ret = -1; + goto DONE; + } break; } + if (print_lastmod) { + revision = notmuch_database_get_revision (notmuch, &uuid); + printf ("\t%s\t%lu\n", uuid, revision); + } else { + fputs ("\n", stdout); + } + + DONE: notmuch_query_destroy (query); - return 0; + return ret; } static int count_file (notmuch_database_t *notmuch, FILE *input, const char **exclude_tags, - size_t exclude_tags_length, int output) + size_t exclude_tags_length, int output, int print_lastmod) { char *line = NULL; ssize_t line_len; size_t line_size; int ret = 0; - while (!ret && (line_len = getline (&line, &line_size, input)) != -1) { + while (! ret && (line_len = getline (&line, &line_size, input)) != -1) { chomp_newline (line); ret = print_count (notmuch, line, exclude_tags, exclude_tags_length, - output); + output, print_lastmod); } if (line) @@ -130,6 +150,7 @@ notmuch_count_command (notmuch_config_t *config, int argc, char *argv[]) const char **search_exclude_tags = NULL; size_t search_exclude_tags_length = 0; notmuch_bool_t batch = FALSE; + notmuch_bool_t print_lastmod = FALSE; FILE *input = stdin; char *input_file_name = NULL; int ret; @@ -144,8 +165,10 @@ notmuch_count_command (notmuch_config_t *config, int argc, char *argv[]) (notmuch_keyword_t []){ { "true", EXCLUDE_TRUE }, { "false", EXCLUDE_FALSE }, { 0, 0 } } }, + { NOTMUCH_OPT_BOOLEAN, &print_lastmod, "lastmod", 'l', 0 }, { NOTMUCH_OPT_BOOLEAN, &batch, "batch", 0, 0 }, { NOTMUCH_OPT_STRING, &input_file_name, "input", 'i', 0 }, + { NOTMUCH_OPT_INHERIT, (void *) ¬much_shared_options, NULL, 0, 0 }, { 0, 0, 0, 0, 0 } }; @@ -153,6 +176,8 @@ notmuch_count_command (notmuch_config_t *config, int argc, char *argv[]) if (opt_index < 0) return EXIT_FAILURE; + notmuch_process_shared_options (argv[0]); + if (input_file_name) { batch = TRUE; input = fopen (input_file_name, "r"); @@ -172,7 +197,9 @@ notmuch_count_command (notmuch_config_t *config, int argc, char *argv[]) NOTMUCH_DATABASE_MODE_READ_ONLY, ¬much)) return EXIT_FAILURE; - query_str = query_string_from_args (config, argc-opt_index, argv+opt_index); + notmuch_exit_if_unmatched_db_uuid (notmuch); + + query_str = query_string_from_args (config, argc - opt_index, argv + opt_index); if (query_str == NULL) { fprintf (stderr, "Out of memory.\n"); return EXIT_FAILURE; @@ -185,10 +212,10 @@ notmuch_count_command (notmuch_config_t *config, int argc, char *argv[]) if (batch) ret = count_file (notmuch, input, search_exclude_tags, - search_exclude_tags_length, output); + search_exclude_tags_length, output, print_lastmod); else ret = print_count (notmuch, query_str, search_exclude_tags, - search_exclude_tags_length, output); + search_exclude_tags_length, output, print_lastmod); notmuch_database_destroy (notmuch); diff --git a/notmuch-dump.c b/notmuch-dump.c index 9c6ad7f4..829781f8 100644 --- a/notmuch-dump.c +++ b/notmuch-dump.c @@ -48,8 +48,13 @@ database_dump_file (notmuch_database_t *notmuch, gzFile output, char *buffer = NULL; size_t buffer_size = 0; + notmuch_status_t status; - for (messages = notmuch_query_search_messages (query); + status = notmuch_query_search_messages_st (query, &messages); + if (print_status_query ("notmuch dump", query, status)) + return EXIT_FAILURE; + + for (; notmuch_messages_valid (messages); notmuch_messages_move_to_next (messages)) { int first = 1; @@ -215,6 +220,8 @@ notmuch_dump_command (notmuch_config_t *config, int argc, char *argv[]) NOTMUCH_DATABASE_MODE_READ_WRITE, ¬much)) return EXIT_FAILURE; + notmuch_exit_if_unmatched_db_uuid (notmuch); + char *output_file_name = NULL; int opt_index; @@ -228,6 +235,7 @@ notmuch_dump_command (notmuch_config_t *config, int argc, char *argv[]) { 0, 0 } } }, { NOTMUCH_OPT_STRING, &output_file_name, "output", 'o', 0 }, { NOTMUCH_OPT_BOOLEAN, &gzip_output, "gzip", 'z', 0 }, + { NOTMUCH_OPT_INHERIT, (void *) ¬much_shared_options, NULL, 0, 0 }, { 0, 0, 0, 0, 0 } }; @@ -235,6 +243,8 @@ notmuch_dump_command (notmuch_config_t *config, int argc, char *argv[]) if (opt_index < 0) return EXIT_FAILURE; + notmuch_process_shared_options (argv[0]); + if (opt_index < argc) { query_str = query_string_from_args (notmuch, argc - opt_index, argv + opt_index); if (query_str == NULL) { diff --git a/notmuch-insert.c b/notmuch-insert.c index 90fe3bad..5205c17a 100644 --- a/notmuch-insert.c +++ b/notmuch-insert.c @@ -466,6 +466,7 @@ notmuch_insert_command (notmuch_config_t *config, int argc, char *argv[]) { NOTMUCH_OPT_BOOLEAN, &create_folder, "create-folder", 0, 0 }, { NOTMUCH_OPT_BOOLEAN, &keep, "keep", 0, 0 }, { NOTMUCH_OPT_BOOLEAN, &no_hooks, "no-hooks", 'n', 0 }, + { NOTMUCH_OPT_INHERIT, (void *) ¬much_shared_options, NULL, 0, 0 }, { NOTMUCH_OPT_END, 0, 0, 0, 0 } }; @@ -473,6 +474,8 @@ notmuch_insert_command (notmuch_config_t *config, int argc, char *argv[]) if (opt_index < 0) return EXIT_FAILURE; + notmuch_process_shared_options (argv[0]); + db_path = notmuch_config_get_database_path (config); new_tags = notmuch_config_get_new_tags (config, &new_tags_length); synchronize_flags = notmuch_config_get_maildir_synchronize_flags (config); @@ -521,7 +524,7 @@ notmuch_insert_command (notmuch_config_t *config, int argc, char *argv[]) return EXIT_FAILURE; } - /* Setup our handler for SIGINT. We do not set SA_RESTART so that copying + /* Set up our handler for SIGINT. We do not set SA_RESTART so that copying * from standard input may be interrupted. */ memset (&action, 0, sizeof (struct sigaction)); action.sa_handler = handle_sigint; @@ -533,6 +536,8 @@ notmuch_insert_command (notmuch_config_t *config, int argc, char *argv[]) NOTMUCH_DATABASE_MODE_READ_WRITE, ¬much)) return EXIT_FAILURE; + notmuch_exit_if_unmatched_db_uuid (notmuch); + /* Write the message to the Maildir new directory. */ newpath = maildir_write_new (config, STDIN_FILENO, maildir); if (! newpath) { diff --git a/notmuch-new.c b/notmuch-new.c index e6c283eb..33645349 100644 --- a/notmuch-new.c +++ b/notmuch-new.c @@ -528,6 +528,10 @@ add_files (notmuch_database_t *notmuch, "%s/%s", path, notmuch_filenames_get (db_files)); + if (state->debug) + printf ("(D) add_files_recursive, pass 2: queuing passed file %s for deletion from database\n", + absolute); + _filename_list_add (state->removed_files, absolute); notmuch_filenames_move_to_next (db_files); @@ -542,6 +546,9 @@ add_files (notmuch_database_t *notmuch, { char *absolute = talloc_asprintf (state->removed_directories, "%s/%s", path, filename); + if (state->debug) + printf ("(D) add_files_recursive, pass 2: queuing passed directory %s for deletion from database\n", + absolute); _filename_list_add (state->removed_directories, absolute); } @@ -610,6 +617,9 @@ add_files (notmuch_database_t *notmuch, char *absolute = talloc_asprintf (state->removed_files, "%s/%s", path, notmuch_filenames_get (db_files)); + if (state->debug) + printf ("(D) add_files_recursive, pass 3: queuing leftover file %s for deletion from database\n", + absolute); _filename_list_add (state->removed_files, absolute); @@ -622,6 +632,10 @@ add_files (notmuch_database_t *notmuch, "%s/%s", path, notmuch_filenames_get (db_subdirs)); + if (state->debug) + printf ("(D) add_files_recursive, pass 3: queuing leftover directory %s for deletion from database\n", + absolute); + _filename_list_add (state->removed_directories, absolute); notmuch_filenames_move_to_next (db_subdirs); @@ -662,7 +676,7 @@ setup_progress_printing_timer (void) struct sigaction action; struct itimerval timerval; - /* Setup our handler for SIGALRM */ + /* Set up our handler for SIGALRM */ memset (&action, 0, sizeof (struct sigaction)); action.sa_handler = handle_sigalrm; sigemptyset (&action.sa_mask); @@ -864,6 +878,15 @@ _remove_directory (void *ctx, goto DONE; } + /* + * XXX: The library does not have a function to remove a directory + * document for a path. Usually this doesn't matter except for a + * slight waste of space. However, if the directory gets added to + * the filesystem again, the old directory document is found with + * the old mtime. Reset the directory mtime to avoid problems. + */ + notmuch_directory_set_mtime (directory, 0); + DONE: notmuch_directory_destroy (directory); return status; @@ -934,6 +957,7 @@ notmuch_new_command (notmuch_config_t *config, int argc, char *argv[]) { NOTMUCH_OPT_BOOLEAN, &verbose, "verbose", 'v', 0 }, { NOTMUCH_OPT_BOOLEAN, &add_files_state.debug, "debug", 'd', 0 }, { NOTMUCH_OPT_BOOLEAN, &no_hooks, "no-hooks", 'n', 0 }, + { NOTMUCH_OPT_INHERIT, (void *) ¬much_shared_options, NULL, 0, 0 }, { 0, 0, 0, 0, 0 } }; @@ -941,6 +965,8 @@ notmuch_new_command (notmuch_config_t *config, int argc, char *argv[]) if (opt_index < 0) return EXIT_FAILURE; + notmuch_process_shared_options (argv[0]); + /* quiet trumps verbose */ if (quiet) add_files_state.verbosity = VERBOSITY_QUIET; @@ -992,10 +1018,11 @@ notmuch_new_command (notmuch_config_t *config, int argc, char *argv[]) fputs (status_string, stderr); free (status_string); } - return EXIT_FAILURE; } + notmuch_exit_if_unmatched_db_uuid (notmuch); + if (notmuch_database_needs_upgrade (notmuch)) { time_t now = time (NULL); struct tm *gm_time = gmtime (&now); @@ -1047,7 +1074,7 @@ notmuch_new_command (notmuch_config_t *config, int argc, char *argv[]) if (notmuch == NULL) return EXIT_FAILURE; - /* Setup our handler for SIGINT. We do this after having + /* Set up our handler for SIGINT. We do this after having * potentially done a database upgrade we this interrupt handler * won't support. */ memset (&action, 0, sizeof (struct sigaction)); diff --git a/notmuch-reply.c b/notmuch-reply.c index d51fdfc3..fd6a1ec1 100644 --- a/notmuch-reply.c +++ b/notmuch-reply.c @@ -606,8 +606,13 @@ notmuch_reply_format_default(void *ctx, notmuch_messages_t *messages; notmuch_message_t *message; mime_node_t *root; + notmuch_status_t status; - for (messages = notmuch_query_search_messages (query); + status = notmuch_query_search_messages_st (query, &messages); + if (print_status_query ("notmuch reply", query, status)) + return 1; + + for (; notmuch_messages_valid (messages); notmuch_messages_move_to_next (messages)) { @@ -650,13 +655,17 @@ notmuch_reply_format_sprinter(void *ctx, notmuch_messages_t *messages; notmuch_message_t *message; mime_node_t *node; + notmuch_status_t status; if (notmuch_query_count_messages (query) != 1) { fprintf (stderr, "Error: search term did not match precisely one message.\n"); return 1; } - messages = notmuch_query_search_messages (query); + status = notmuch_query_search_messages_st (query, &messages); + if (print_status_query ("notmuch reply", query, status)) + return 1; + message = notmuch_messages_get (messages); if (mime_node_open (ctx, message, &(params->crypto), &node) != NOTMUCH_STATUS_SUCCESS) return 1; @@ -698,8 +707,13 @@ notmuch_reply_format_headers_only(void *ctx, notmuch_message_t *message; const char *in_reply_to, *orig_references, *references; char *reply_headers; + notmuch_status_t status; - for (messages = notmuch_query_search_messages (query); + status = notmuch_query_search_messages_st (query, &messages); + if (print_status_query ("notmuch reply", query, status)) + return 1; + + for (; notmuch_messages_valid (messages); notmuch_messages_move_to_next (messages)) { @@ -790,6 +804,7 @@ notmuch_reply_command (notmuch_config_t *config, int argc, char *argv[]) { "sender", FALSE }, { 0, 0 } } }, { NOTMUCH_OPT_BOOLEAN, ¶ms.crypto.decrypt, "decrypt", 'd', 0 }, + { NOTMUCH_OPT_INHERIT, (void *) ¬much_shared_options, NULL, 0, 0 }, { 0, 0, 0, 0, 0 } }; @@ -797,6 +812,8 @@ notmuch_reply_command (notmuch_config_t *config, int argc, char *argv[]) if (opt_index < 0) return EXIT_FAILURE; + notmuch_process_shared_options (argv[0]); + if (format == FORMAT_HEADERS_ONLY) { reply_format_func = notmuch_reply_format_headers_only; } else if (format == FORMAT_JSON) { @@ -828,6 +845,8 @@ notmuch_reply_command (notmuch_config_t *config, int argc, char *argv[]) NOTMUCH_DATABASE_MODE_READ_ONLY, ¬much)) return EXIT_FAILURE; + notmuch_exit_if_unmatched_db_uuid (notmuch); + query = notmuch_query_create (notmuch, query_string); if (query == NULL) { fprintf (stderr, "Out of memory\n"); diff --git a/notmuch-restore.c b/notmuch-restore.c index 584d9f96..9abc64fd 100644 --- a/notmuch-restore.c +++ b/notmuch-restore.c @@ -154,6 +154,7 @@ notmuch_restore_command (notmuch_config_t *config, int argc, char *argv[]) { 0, 0 } } }, { NOTMUCH_OPT_STRING, &input_file_name, "input", 'i', 0 }, { NOTMUCH_OPT_BOOLEAN, &accumulate, "accumulate", 'a', 0 }, + { NOTMUCH_OPT_INHERIT, (void *) ¬much_shared_options, NULL, 0, 0 }, { 0, 0, 0, 0, 0 } }; @@ -163,6 +164,9 @@ notmuch_restore_command (notmuch_config_t *config, int argc, char *argv[]) goto DONE; } + notmuch_process_shared_options (argv[0]); + notmuch_exit_if_unmatched_db_uuid (notmuch); + name_for_error = input_file_name ? input_file_name : "stdin"; if (! accumulate) diff --git a/notmuch-search.c b/notmuch-search.c index b81ac013..44e582cb 100644 --- a/notmuch-search.c +++ b/notmuch-search.c @@ -36,6 +36,12 @@ typedef enum { OUTPUT_COUNT = 1 << 7, } output_t; +typedef enum { + DEDUP_NONE, + DEDUP_MAILBOX, + DEDUP_ADDRESS, +} dedup_t; + typedef enum { NOTMUCH_FORMAT_JSON, NOTMUCH_FORMAT_TEXT, @@ -55,6 +61,7 @@ typedef struct { int limit; int dupe; GHashTable *addresses; + dedup_t dedup; } search_context_t; typedef struct { @@ -111,6 +118,7 @@ do_search_threads (search_context_t *ctx) sprinter_t *format = ctx->format; time_t date; int i; + notmuch_status_t status; if (ctx->offset < 0) { ctx->offset += notmuch_query_count_threads (ctx->query); @@ -118,8 +126,8 @@ do_search_threads (search_context_t *ctx) ctx->offset = 0; } - threads = notmuch_query_search_threads (ctx->query); - if (threads == NULL) + status = notmuch_query_search_threads_st (ctx->query, &threads); + if (print_status_query("notmuch search", ctx->query, status)) return 1; format->begin_list (format); @@ -243,33 +251,87 @@ do_search_threads (search_context_t *ctx) return 0; } +static mailbox_t *new_mailbox (void *ctx, const char *name, const char *addr) +{ + mailbox_t *mailbox; + + mailbox = talloc (ctx, mailbox_t); + if (! mailbox) + return NULL; + + mailbox->name = talloc_strdup (mailbox, name); + mailbox->addr = talloc_strdup (mailbox, addr); + mailbox->count = 1; + + return mailbox; +} + +static int mailbox_compare (const void *v1, const void *v2) +{ + const mailbox_t *m1 = v1, *m2 = v2; + int ret; + + ret = strcmp_null (m1->name, m2->name); + if (! ret) + ret = strcmp (m1->addr, m2->addr); + + return ret; +} + /* Returns TRUE iff name and addr is duplicate. If not, stores the * name/addr pair in order to detect subsequent duplicates. */ static notmuch_bool_t is_duplicate (const search_context_t *ctx, const char *name, const char *addr) { - notmuch_bool_t duplicate; char *key; + GList *list, *l; mailbox_t *mailbox; - key = talloc_asprintf (ctx->format, "%s <%s>", name, addr); + list = g_hash_table_lookup (ctx->addresses, addr); + if (list) { + mailbox_t find = { + .name = name, + .addr = addr, + }; + + l = g_list_find_custom (list, &find, mailbox_compare); + if (l) { + mailbox = l->data; + mailbox->count++; + return TRUE; + } + + mailbox = new_mailbox (ctx->format, name, addr); + if (! mailbox) + return FALSE; + + /* + * XXX: It would be more efficient to prepend to the list, but + * then we'd have to store the changed list head back to the + * hash table. This check is here just to avoid the compiler + * warning for unused result. + */ + if (list != g_list_append (list, mailbox)) + INTERNAL_ERROR ("appending to list changed list head\n"); + + return FALSE; + } + + key = talloc_strdup (ctx->format, addr); if (! key) return FALSE; - duplicate = g_hash_table_lookup_extended (ctx->addresses, key, NULL, (gpointer)&mailbox); + mailbox = new_mailbox (ctx->format, name, addr); + if (! mailbox) + return FALSE; - if (! duplicate) { - mailbox = talloc (ctx->format, mailbox_t); - mailbox->name = talloc_strdup (mailbox, name); - mailbox->addr = talloc_strdup (mailbox, addr); - mailbox->count = 1; - g_hash_table_insert (ctx->addresses, key, mailbox); - } else { - mailbox->count++; - talloc_free (key); - } + list = g_list_append (NULL, mailbox); + if (! list) + return FALSE; + + g_hash_table_insert (ctx->addresses, key, list); - return duplicate; + return FALSE; } static void @@ -287,7 +349,7 @@ print_mailbox (const search_context_t *ctx, const mailbox_t *mailbox) name_addr = internet_address_to_string (ia, FALSE); if (format->is_text_printer) { - if (count > 0) { + if (ctx->output & OUTPUT_COUNT) { format->integer (format, count); format->string (format, "\t"); } @@ -301,7 +363,7 @@ print_mailbox (const search_context_t *ctx, const mailbox_t *mailbox) format->string (format, addr); format->map_key (format, "name-addr"); format->string (format, name_addr); - if (count > 0) { + if (ctx->output & OUTPUT_COUNT) { format->map_key (format, "count"); format->integer (format, count); } @@ -338,13 +400,15 @@ process_address_list (const search_context_t *ctx, mailbox_t mbx = { .name = internet_address_get_name (address), .addr = internet_address_mailbox_get_addr (mailbox), - .count = 0, }; - if (is_duplicate (ctx, mbx.name, mbx.addr)) + /* OUTPUT_COUNT only works with deduplication */ + if (ctx->dedup != DEDUP_NONE && + is_duplicate (ctx, mbx.name, mbx.addr)) continue; - if (ctx->output & OUTPUT_COUNT) + /* OUTPUT_COUNT and DEDUP_ADDRESS require a full pass. */ + if (ctx->output & OUTPUT_COUNT || ctx->dedup == DEDUP_ADDRESS) continue; print_mailbox (ctx, &mbx); @@ -378,14 +442,56 @@ _talloc_free_for_g_hash (void *ptr) } static void -print_hash_value (unused (gpointer key), gpointer value, gpointer user_data) +_list_free_for_g_hash (void *ptr) { - const mailbox_t *mailbox = value; - search_context_t *ctx = user_data; + g_list_free_full (ptr, _talloc_free_for_g_hash); +} + +/* Print the most common variant of a list of unique mailboxes, and + * conflate the counts. */ +static void +print_popular (const search_context_t *ctx, GList *list) +{ + GList *l; + mailbox_t *mailbox = NULL, *m; + int max = 0; + int total = 0; + + for (l = list; l; l = l->next) { + m = l->data; + total += m->count; + if (m->count > max) { + mailbox = m; + max = m->count; + } + } + + if (! mailbox) + INTERNAL_ERROR("Empty list in address hash table\n"); + + /* The original count is no longer needed, so overwrite. */ + mailbox->count = total; print_mailbox (ctx, mailbox); } +static void +print_list_value (void *mailbox, void *context) +{ + print_mailbox (context, mailbox); +} + +static void +print_hash_value (unused (void *key), void *list, void *context) +{ + const search_context_t *ctx = context; + + if (ctx->dedup == DEDUP_ADDRESS) + print_popular (ctx, list); + else + g_list_foreach (list, print_list_value, context); +} + static int _count_filenames (notmuch_message_t *message) { @@ -412,6 +518,7 @@ do_search_messages (search_context_t *ctx) notmuch_filenames_t *filenames; sprinter_t *format = ctx->format; int i; + notmuch_status_t status; if (ctx->offset < 0) { ctx->offset += notmuch_query_count_messages (ctx->query); @@ -419,8 +526,8 @@ do_search_messages (search_context_t *ctx) ctx->offset = 0; } - messages = notmuch_query_search_messages (ctx->query); - if (messages == NULL) + status = notmuch_query_search_messages_st (ctx->query, &messages); + if (print_status_query ("notmuch search", ctx->query, status)) return 1; format->begin_list (format); @@ -481,7 +588,8 @@ do_search_messages (search_context_t *ctx) notmuch_message_destroy (message); } - if (ctx->addresses && ctx->output & OUTPUT_COUNT) + if (ctx->addresses && + (ctx->output & OUTPUT_COUNT || ctx->dedup == DEDUP_ADDRESS)) g_hash_table_foreach (ctx->addresses, print_hash_value, ctx); notmuch_messages_destroy (messages); @@ -508,8 +616,9 @@ do_search_tags (const search_context_t *ctx) if (strcmp (notmuch_query_get_query_string (query), "*") == 0) { tags = notmuch_database_get_all_tags (notmuch); } else { - messages = notmuch_query_search_messages (query); - if (messages == NULL) + notmuch_status_t status; + status = notmuch_query_search_messages_st (query, &messages); + if (print_status_query ("notmuch search", query, status)) return 1; tags = notmuch_messages_collect_tags (messages); @@ -583,6 +692,8 @@ _notmuch_search_prepare (search_context_t *ctx, notmuch_config_t *config, int ar return EXIT_FAILURE; } + notmuch_exit_if_unmatched_db_uuid (ctx->notmuch); + query_str = query_string_from_args (ctx->notmuch, argc, argv); if (query_str == NULL) { fprintf (stderr, "Out of memory.\n"); @@ -640,6 +751,7 @@ static search_context_t search_context = { .offset = 0, .limit = -1, /* unlimited */ .dupe = -1, + .dedup = DEDUP_MAILBOX, }; static const notmuch_opt_desc_t common_options[] = { @@ -681,6 +793,7 @@ notmuch_search_command (notmuch_config_t *config, int argc, char *argv[]) { NOTMUCH_OPT_INT, &ctx->limit, "limit", 'L', 0 }, { NOTMUCH_OPT_INT, &ctx->dupe, "duplicate", 'D', 0 }, { NOTMUCH_OPT_INHERIT, (void *) &common_options, NULL, 0, 0 }, + { NOTMUCH_OPT_INHERIT, (void *) ¬much_shared_options, NULL, 0, 0 }, { 0, 0, 0, 0, 0 } }; @@ -689,6 +802,8 @@ notmuch_search_command (notmuch_config_t *config, int argc, char *argv[]) if (opt_index < 0) return EXIT_FAILURE; + notmuch_process_shared_options (argv[0]); + if (ctx->output != OUTPUT_FILES && ctx->output != OUTPUT_MESSAGES && ctx->dupe != -1) { fprintf (stderr, "Error: --duplicate=N is only supported with --output=files and --output=messages.\n"); @@ -736,7 +851,13 @@ notmuch_address_command (notmuch_config_t *config, int argc, char *argv[]) (notmuch_keyword_t []){ { "true", NOTMUCH_EXCLUDE_TRUE }, { "false", NOTMUCH_EXCLUDE_FALSE }, { 0, 0 } } }, + { NOTMUCH_OPT_KEYWORD, &ctx->dedup, "deduplicate", 'D', + (notmuch_keyword_t []){ { "no", DEDUP_NONE }, + { "mailbox", DEDUP_MAILBOX }, + { "address", DEDUP_ADDRESS }, + { 0, 0 } } }, { NOTMUCH_OPT_INHERIT, (void *) &common_options, NULL, 0, 0 }, + { NOTMUCH_OPT_INHERIT, (void *) ¬much_shared_options, NULL, 0, 0 }, { 0, 0, 0, 0, 0 } }; @@ -744,15 +865,28 @@ notmuch_address_command (notmuch_config_t *config, int argc, char *argv[]) if (opt_index < 0) return EXIT_FAILURE; + notmuch_process_shared_options (argv[0]); + if (! (ctx->output & (OUTPUT_SENDER | OUTPUT_RECIPIENTS))) ctx->output |= OUTPUT_SENDER; + if (ctx->output & OUTPUT_COUNT && ctx->dedup == DEDUP_NONE) { + fprintf (stderr, "--output=count is not applicable with --deduplicate=no\n"); + return EXIT_FAILURE; + } + if (_notmuch_search_prepare (ctx, config, argc - opt_index, argv + opt_index)) return EXIT_FAILURE; - ctx->addresses = g_hash_table_new_full (g_str_hash, g_str_equal, - _talloc_free_for_g_hash, _talloc_free_for_g_hash); + ctx->addresses = g_hash_table_new_full (strcase_hash, strcase_equal, + _talloc_free_for_g_hash, + _list_free_for_g_hash); + + /* The order is not guaranteed if a full pass is required, so go + * for fastest. */ + if (ctx->output & OUTPUT_COUNT || ctx->dedup == DEDUP_ADDRESS) + notmuch_query_set_sort (ctx->query, NOTMUCH_SORT_UNSORTED); ret = do_search_messages (ctx); diff --git a/notmuch-setup.c b/notmuch-setup.c index 36a6171a..9aaf9286 100644 --- a/notmuch-setup.c +++ b/notmuch-setup.c @@ -145,6 +145,13 @@ notmuch_setup_command (notmuch_config_t *config, chomp_newline (response); \ } while (0) + if (notmuch_minimal_options ("setup", argc, argv) < 0) + return EXIT_FAILURE; + + if (notmuch_requested_db_uuid) + fprintf (stderr, "Warning: ignoring --uuid=%s\n", + notmuch_requested_db_uuid); + if (notmuch_config_is_new (config)) welcome_message_pre_setup (); diff --git a/notmuch-show.c b/notmuch-show.c index 43bf71c8..e0548089 100644 --- a/notmuch-show.c +++ b/notmuch-show.c @@ -334,8 +334,6 @@ show_text_part_content (GMimeObject *part, GMimeStream *stream_out, g_object_unref(stream_filter); } -#ifdef GMIME_ATLEAST_26 - /* Get signature status string (GMime 2.6) */ static const char* signature_status_to_string (GMimeSignatureStatus x) @@ -427,91 +425,6 @@ format_part_sigstatus_sprinter (sprinter_t *sp, mime_node_t *node) sp->end (sp); } -#else /* GMIME_ATLEAST_26 */ - -/* Get signature status string (GMime 2.4) */ -static const char* -signer_status_to_string (GMimeSignerStatus x) -{ - switch (x) { - case GMIME_SIGNER_STATUS_NONE: - return "none"; - case GMIME_SIGNER_STATUS_GOOD: - return "good"; - case GMIME_SIGNER_STATUS_BAD: - return "bad"; - case GMIME_SIGNER_STATUS_ERROR: - return "error"; - } - return "unknown"; -} - -/* Signature status sprinter (GMime 2.4) */ -static void -format_part_sigstatus_sprinter (sprinter_t *sp, mime_node_t *node) -{ - const GMimeSignatureValidity* validity = node->sig_validity; - - sp->begin_list (sp); - - if (!validity) { - sp->end (sp); - return; - } - - const GMimeSigner *signer = g_mime_signature_validity_get_signers (validity); - while (signer) { - sp->begin_map (sp); - - /* status */ - sp->map_key (sp, "status"); - sp->string (sp, signer_status_to_string (signer->status)); - - if (signer->status == GMIME_SIGNER_STATUS_GOOD) - { - if (signer->fingerprint) { - sp->map_key (sp, "fingerprint"); - sp->string (sp, signer->fingerprint); - } - /* these dates are seconds since the epoch; should we - * provide a more human-readable format string? */ - if (signer->created) { - sp->map_key (sp, "created"); - sp->integer (sp, signer->created); - } - if (signer->expires) { - sp->map_key (sp, "expires"); - sp->integer (sp, signer->expires); - } - /* output user id only if validity is FULL or ULTIMATE. */ - /* note that gmime is using the term "trust" here, which - * is WRONG. It's actually user id "validity". */ - if ((signer->name) && (signer->trust)) { - if ((signer->trust == GMIME_SIGNER_TRUST_FULLY) || (signer->trust == GMIME_SIGNER_TRUST_ULTIMATE)) { - sp->map_key (sp, "userid"); - sp->string (sp, signer->name); - } - } - } else { - if (signer->keyid) { - sp->map_key (sp, "keyid"); - sp->string (sp, signer->keyid); - } - } - if (signer->errors != GMIME_SIGNER_ERROR_NONE) { - sp->map_key (sp, "errors"); - sp->integer (sp, signer->errors); - } - - sp->end (sp); - signer = signer->next; - } - - sp->end (sp); -} - -#endif /* GMIME_ATLEAST_26 */ - static notmuch_status_t format_part_text (const void *ctx, sprinter_t *sp, mime_node_t *node, int indent, const notmuch_show_params_t *params) @@ -982,13 +895,17 @@ do_show_single (void *ctx, { notmuch_messages_t *messages; notmuch_message_t *message; + notmuch_status_t status; if (notmuch_query_count_messages (query) != 1) { fprintf (stderr, "Error: search term did not match precisely one message.\n"); return 1; } - messages = notmuch_query_search_messages (query); + status = notmuch_query_search_messages_st (query, &messages); + if (print_status_query ("notmuch show", query, status)) + return 1; + message = notmuch_messages_get (messages); if (message == NULL) { @@ -1015,8 +932,8 @@ do_show (void *ctx, notmuch_messages_t *messages; notmuch_status_t status, res = NOTMUCH_STATUS_SUCCESS; - threads = notmuch_query_search_threads (query); - if (! threads) + status= notmuch_query_search_threads_st (query, &threads); + if (print_status_query ("notmuch show", query, status)) return 1; sp->begin_list (sp); @@ -1114,6 +1031,7 @@ notmuch_show_command (notmuch_config_t *config, int argc, char *argv[]) { NOTMUCH_OPT_BOOLEAN, ¶ms.crypto.verify, "verify", 'v', 0 }, { NOTMUCH_OPT_BOOLEAN, ¶ms.output_body, "body", 'b', 0 }, { NOTMUCH_OPT_BOOLEAN, ¶ms.include_html, "include-html", 0, 0 }, + { NOTMUCH_OPT_INHERIT, (void *) ¬much_shared_options, NULL, 0, 0 }, { 0, 0, 0, 0, 0 } }; @@ -1121,6 +1039,8 @@ notmuch_show_command (notmuch_config_t *config, int argc, char *argv[]) if (opt_index < 0) return EXIT_FAILURE; + notmuch_process_shared_options (argv[0]); + /* decryption implies verification */ if (params.crypto.decrypt) params.crypto.verify = TRUE; @@ -1210,6 +1130,8 @@ notmuch_show_command (notmuch_config_t *config, int argc, char *argv[]) NOTMUCH_DATABASE_MODE_READ_ONLY, ¬much)) return EXIT_FAILURE; + notmuch_exit_if_unmatched_db_uuid (notmuch); + query = notmuch_query_create (notmuch, query_string); if (query == NULL) { fprintf (stderr, "Out of memory\n"); diff --git a/notmuch-tag.c b/notmuch-tag.c index 5b2f1e48..c020cb6f 100644 --- a/notmuch-tag.c +++ b/notmuch-tag.c @@ -97,6 +97,8 @@ tag_query (void *ctx, notmuch_database_t *notmuch, const char *query_string, notmuch_query_t *query; notmuch_messages_t *messages; notmuch_message_t *message; + notmuch_status_t status; + int ret = NOTMUCH_STATUS_SUCCESS; if (! (flags & TAG_FLAG_REMOVE_ALL)) { @@ -119,7 +121,11 @@ tag_query (void *ctx, notmuch_database_t *notmuch, const char *query_string, /* tagging is not interested in any special sort order */ notmuch_query_set_sort (query, NOTMUCH_SORT_UNSORTED); - for (messages = notmuch_query_search_messages (query); + status = notmuch_query_search_messages_st (query, &messages); + if (print_status_query ("notmuch tag", query, status)) + return status; + + for (; notmuch_messages_valid (messages) && ! interrupted; notmuch_messages_move_to_next (messages)) { message = notmuch_messages_get (messages); @@ -195,7 +201,7 @@ notmuch_tag_command (notmuch_config_t *config, int argc, char *argv[]) int opt_index; int ret; - /* Setup our handler for SIGINT */ + /* Set up our handler for SIGINT */ memset (&action, 0, sizeof (struct sigaction)); action.sa_handler = handle_sigint; sigemptyset (&action.sa_mask); @@ -206,6 +212,7 @@ notmuch_tag_command (notmuch_config_t *config, int argc, char *argv[]) { NOTMUCH_OPT_BOOLEAN, &batch, "batch", 0, 0 }, { NOTMUCH_OPT_STRING, &input_file_name, "input", 'i', 0 }, { NOTMUCH_OPT_BOOLEAN, &remove_all, "remove-all", 0, 0 }, + { NOTMUCH_OPT_INHERIT, (void *) ¬much_shared_options, NULL, 0, 0 }, { 0, 0, 0, 0, 0 } }; @@ -213,6 +220,8 @@ notmuch_tag_command (notmuch_config_t *config, int argc, char *argv[]) if (opt_index < 0) return EXIT_FAILURE; + notmuch_process_shared_options (argv[0]); + if (input_file_name) { batch = TRUE; input = fopen (input_file_name, "r"); @@ -258,6 +267,8 @@ notmuch_tag_command (notmuch_config_t *config, int argc, char *argv[]) NOTMUCH_DATABASE_MODE_READ_WRITE, ¬much)) return EXIT_FAILURE; + notmuch_exit_if_unmatched_db_uuid (notmuch); + if (notmuch_config_get_maildir_synchronize_flags (config)) tag_flags |= TAG_FLAG_MAILDIR_SYNC; diff --git a/notmuch.c b/notmuch.c index a5b2877a..ce6c5756 100644 --- a/notmuch.c +++ b/notmuch.c @@ -43,11 +43,64 @@ notmuch_help_command (notmuch_config_t *config, int argc, char *argv[]); static int notmuch_command (notmuch_config_t *config, int argc, char *argv[]); +static int +_help_for (const char *topic); + +static notmuch_bool_t print_version = FALSE, print_help = FALSE; +char *notmuch_requested_db_uuid = NULL; + +const notmuch_opt_desc_t notmuch_shared_options [] = { + { NOTMUCH_OPT_BOOLEAN, &print_version, "version", 'v', 0 }, + { NOTMUCH_OPT_BOOLEAN, &print_help, "help", 'h', 0 }, + { NOTMUCH_OPT_STRING, ¬much_requested_db_uuid, "uuid", 'u', 0 }, + {0, 0, 0, 0, 0} +}; + +/* any subcommand wanting to support these options should call + * inherit notmuch_shared_options and call + * notmuch_process_shared_options (subcommand_name); + */ +void +notmuch_process_shared_options (const char *subcommand_name) { + if (print_version) { + printf ("notmuch " STRINGIFY(NOTMUCH_VERSION) "\n"); + exit (EXIT_SUCCESS); + } + + if (print_help) { + int ret = _help_for (subcommand_name); + exit (ret); + } +} + +/* This is suitable for subcommands that do not actually open the + * database. + */ +int notmuch_minimal_options (const char *subcommand_name, + int argc, char **argv) +{ + int opt_index; + + notmuch_opt_desc_t options[] = { + { NOTMUCH_OPT_INHERIT, (void *) ¬much_shared_options, NULL, 0, 0 }, + { 0, 0, 0, 0, 0 } + }; + + opt_index = parse_arguments (argc, argv, options, 1); + + if (opt_index < 0) + return -1; + + /* We can't use argv here as it is sometimes NULL */ + notmuch_process_shared_options (subcommand_name); + return opt_index; +} + static command_t commands[] = { { NULL, notmuch_command, TRUE, "Notmuch main command." }, { "setup", notmuch_setup_command, TRUE, - "Interactively setup notmuch for first use." }, + "Interactively set up notmuch for first use." }, { "new", notmuch_new_command, FALSE, "Find and import new messages to the notmuch database." }, { "insert", notmuch_insert_command, FALSE, @@ -167,6 +220,22 @@ be supported in the future.\n", notmuch_format_version); } } +void +notmuch_exit_if_unmatched_db_uuid (notmuch_database_t *notmuch) +{ + const char *uuid = NULL; + + if (!notmuch_requested_db_uuid) + return; + IGNORE_RESULT (notmuch_database_get_revision (notmuch, &uuid)); + + if (strcmp (notmuch_requested_db_uuid, uuid) != 0){ + fprintf (stderr, "Error: requested database revision %s does not match %s\n", + notmuch_requested_db_uuid, uuid); + exit (1); + } +} + static void exec_man (const char *page) { @@ -177,21 +246,19 @@ exec_man (const char *page) } static int -notmuch_help_command (notmuch_config_t *config, int argc, char *argv[]) +_help_for (const char *topic_name) { command_t *command; help_topic_t *topic; unsigned int i; - argc--; argv++; /* Ignore "help" */ - - if (argc == 0) { + if (!topic_name) { printf ("The notmuch mail system.\n\n"); usage (stdout); return EXIT_SUCCESS; } - if (strcmp (argv[0], "help") == 0) { + if (strcmp (topic_name, "help") == 0) { printf ("The notmuch help system.\n\n" "\tNotmuch uses the man command to display help. In case\n" "\tof difficulties check that MANPATH includes the pages\n" @@ -200,26 +267,46 @@ notmuch_help_command (notmuch_config_t *config, int argc, char *argv[]) return EXIT_SUCCESS; } - command = find_command (argv[0]); + command = find_command (topic_name); if (command) { - char *page = talloc_asprintf (config, "notmuch-%s", command->name); + char *page = talloc_asprintf (NULL, "notmuch-%s", command->name); exec_man (page); } for (i = 0; i < ARRAY_SIZE (help_topics); i++) { topic = &help_topics[i]; - if (strcmp (argv[0], topic->name) == 0) { - char *page = talloc_asprintf (config, "notmuch-%s", topic->name); + if (strcmp (topic_name, topic->name) == 0) { + char *page = talloc_asprintf (NULL, "notmuch-%s", topic->name); exec_man (page); } } fprintf (stderr, "\nSorry, %s is not a known command. There's not much I can do to help.\n\n", - argv[0]); + topic_name); return EXIT_FAILURE; } +static int +notmuch_help_command (unused (notmuch_config_t * config), int argc, char *argv[]) +{ + int opt_index; + + opt_index = notmuch_minimal_options ("help", argc, argv); + if (opt_index < 0) + return EXIT_FAILURE; + + /* skip at least subcommand argument */ + argc-= opt_index; + argv+= opt_index; + + if (argc == 0) { + return _help_for (NULL); + } + + return _help_for (argv[0]); +} + /* Handle the case of "notmuch" being invoked with no command * argument. For now we just call notmuch_setup_command, but we plan * to be more clever about this in the future. @@ -285,14 +372,12 @@ main (int argc, char *argv[]) command_t *command; char *config_file_name = NULL; notmuch_config_t *config = NULL; - notmuch_bool_t print_help=FALSE, print_version=FALSE; int opt_index; int ret; notmuch_opt_desc_t options[] = { - { NOTMUCH_OPT_BOOLEAN, &print_help, "help", 'h', 0 }, - { NOTMUCH_OPT_BOOLEAN, &print_version, "version", 'v', 0 }, { NOTMUCH_OPT_STRING, &config_file_name, "config", 'c', 0 }, + { NOTMUCH_OPT_INHERIT, (void *) ¬much_shared_options, NULL, 0, 0 }, { 0, 0, 0, 0, 0 } }; @@ -314,28 +399,11 @@ main (int argc, char *argv[]) goto DONE; } - /* Handle notmuch --help [command] and notmuch command --help. */ - if (print_help || - (opt_index + 1 < argc && strcmp (argv[opt_index + 1], "--help") == 0)) { - /* - * Pass the first positional argument as argv[1] so the help - * command can give help for it. The help command ignores the - * argv[0] passed to it. - */ - ret = notmuch_help_command (NULL, argc - opt_index + 1, - argv + opt_index - 1); - goto DONE; - } - - if (print_version) { - printf ("notmuch " STRINGIFY(NOTMUCH_VERSION) "\n"); - ret = EXIT_SUCCESS; - goto DONE; - } - if (opt_index < argc) command_name = argv[opt_index]; + notmuch_process_shared_options (command_name); + command = find_command (command_name); if (!command) { fprintf (stderr, "Error: Unknown command '%s' (see \"notmuch help\")\n", diff --git a/performance-test/M00-new.sh b/performance-test/M00-new.sh index 99c3f520..a040a97e 100755 --- a/performance-test/M00-new.sh +++ b/performance-test/M00-new.sh @@ -2,7 +2,7 @@ test_description='notmuch new' -. ./perf-test-lib.sh +. ./perf-test-lib.sh || exit 1 # ensure initial 'notmuch new' is run by memory_start uncache_database diff --git a/performance-test/M01-dump-restore.sh b/performance-test/M01-dump-restore.sh index be5894a6..8fea9824 100755 --- a/performance-test/M01-dump-restore.sh +++ b/performance-test/M01-dump-restore.sh @@ -2,7 +2,7 @@ test_description='dump and restore' -. ./perf-test-lib.sh +. ./perf-test-lib.sh || exit 1 memory_start diff --git a/performance-test/T00-new.sh b/performance-test/T00-new.sh index 553bb8b6..b9f21158 100755 --- a/performance-test/T00-new.sh +++ b/performance-test/T00-new.sh @@ -2,7 +2,7 @@ test_description='notmuch new' -. ./perf-test-lib.sh +. ./perf-test-lib.sh || exit 1 uncache_database diff --git a/performance-test/T01-dump-restore.sh b/performance-test/T01-dump-restore.sh index b2ff9400..9cfd5cd6 100755 --- a/performance-test/T01-dump-restore.sh +++ b/performance-test/T01-dump-restore.sh @@ -2,7 +2,7 @@ test_description='dump and restore' -. ./perf-test-lib.sh +. ./perf-test-lib.sh || exit 1 time_start diff --git a/performance-test/T02-tag.sh b/performance-test/T02-tag.sh index 78cecccc..dacb50b8 100755 --- a/performance-test/T02-tag.sh +++ b/performance-test/T02-tag.sh @@ -2,7 +2,7 @@ test_description='tagging' -. ./perf-test-lib.sh +. ./perf-test-lib.sh || exit 1 time_start diff --git a/performance-test/perf-test-lib.sh b/performance-test/perf-test-lib.sh index 75e3d878..00d2f1c6 100644 --- a/performance-test/perf-test-lib.sh +++ b/performance-test/perf-test-lib.sh @@ -1,4 +1,4 @@ -. ./version.sh +. ./version.sh || exit 1 corpus_size=large @@ -25,7 +25,7 @@ do echo "error: unknown performance test option '$1'" >&2; exit 1 ;; esac done -. ../test/test-lib-common.sh +. ../test/test-lib-common.sh || exit 1 set -e @@ -203,7 +203,7 @@ time_done () fi } -cd -P "$test" || error "Cannot setup test environment" +cd -P "$test" || error "Cannot set up test environment" test_failure=0 test_count=0 diff --git a/status.c b/status.c new file mode 100644 index 00000000..8fa81cbf --- /dev/null +++ b/status.c @@ -0,0 +1,21 @@ +#include "notmuch-client.h" + +notmuch_status_t +print_status_query (const char *loc, + const notmuch_query_t *query, + notmuch_status_t status) +{ + if (status) { + const char *msg; + notmuch_database_t *notmuch; + + fprintf (stderr, "%s: %s\n", loc, + notmuch_status_to_string (status)); + + notmuch = notmuch_query_get_database (query); + msg = notmuch_database_status_string (notmuch); + if (msg) + fputs (msg, stderr); + } + return status; +} diff --git a/test/Makefile.local b/test/Makefile.local index 2331ceb1..2b186914 100644 --- a/test/Makefile.local +++ b/test/Makefile.local @@ -56,7 +56,17 @@ TEST_BINARIES := $(TEST_BINARIES:.cc=) test-binaries: $(TEST_BINARIES) test: all test-binaries +ifeq ($V,) + @echo 'Use "$(MAKE) V=1" to print test headings and PASSIng results.' + @env NOTMUCH_TEST_QUIET=1 ${test_src_dir}/notmuch-test $(OPTIONS) +else +# The user has explicitly enabled quiet execution. +ifeq ($V,0) + @env NOTMUCH_TEST_QUIET=1 ${test_src_dir}/notmuch-test $(OPTIONS) +else @${test_src_dir}/notmuch-test $(OPTIONS) +endif +endif check: test diff --git a/test/README b/test/README index daf41600..e54e36b7 100644 --- a/test/README +++ b/test/README @@ -117,6 +117,13 @@ Note that some tests in the existing test suite rely on previous test items, so you cannot arbitrarily skip any test and expect the remaining tests to be unaffected. +Currently we do not consider skipped tests as build failures. For +maximum robustness, when setting up automated build processes, you +should explicitely skip tests, rather than relying on notmuch's +detection of missing prerequisites. In the future we may treat tests +unable to run because of missing prerequisites, but not explicitely +skipped by the user, as failures. + Writing Tests ------------- The test script is written as a shell script. It should start with @@ -138,7 +145,7 @@ Source 'test-lib.sh' After assigning test_description, the test script should source test-lib.sh like this: - . ./test-lib.sh + . ./test-lib.sh || exit 1 This test harness library does the following things: diff --git a/test/T000-basic.sh b/test/T000-basic.sh index ef642457..d6811bd1 100755 --- a/test/T000-basic.sh +++ b/test/T000-basic.sh @@ -14,7 +14,7 @@ then exit 1 fi -. ./test-lib.sh +. ./test-lib.sh || exit 1 ################################################################ # Test harness diff --git a/test/T010-help-test.sh b/test/T010-help-test.sh index caf8bdb0..c1732376 100755 --- a/test/T010-help-test.sh +++ b/test/T010-help-test.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash test_description="online help" -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_expect_success 'notmuch --help' 'notmuch --help' test_expect_success 'notmuch help' 'notmuch help' @@ -12,9 +12,9 @@ if [ $NOTMUCH_HAVE_MAN -eq 1 ]; then test_expect_success 'notmuch help tag' 'notmuch help tag' else test_expect_success 'notmuch --help tag (man pages not available)' \ - 'test_must_fail notmuch --help tag' + 'test_must_fail notmuch --help tag >/dev/null' test_expect_success 'notmuch help tag (man pages not available)' \ - 'test_must_fail notmuch help tag' + 'test_must_fail notmuch help tag >/dev/null' fi test_done diff --git a/test/T020-compact.sh b/test/T020-compact.sh index 507f7698..8b4dbbc4 100755 --- a/test/T020-compact.sh +++ b/test/T020-compact.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description='"notmuch compact"' -. ./test-lib.sh +. ./test-lib.sh || exit 1 add_message '[subject]=One' add_message '[subject]=Two' diff --git a/test/T030-config.sh b/test/T030-config.sh index 7d14a85f..f404908a 100755 --- a/test/T030-config.sh +++ b/test/T030-config.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash test_description='"notmuch config"' -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_begin_subtest "Get string value" test_expect_equal "$(notmuch config get user.name)" "Notmuch Test Suite" diff --git a/test/T040-setup.sh b/test/T040-setup.sh index 0e9f279a..cf0c00bc 100755 --- a/test/T040-setup.sh +++ b/test/T040-setup.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash test_description='"notmuch setup"' -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_begin_subtest "Notmuch new without a config suggests notmuch setup" output=$(notmuch --config=new-notmuch-config new 2>&1) diff --git a/test/T050-new.sh b/test/T050-new.sh index e6c3291f..62573923 100755 --- a/test/T050-new.sh +++ b/test/T050-new.sh @@ -1,27 +1,27 @@ #!/usr/bin/env bash test_description='"notmuch new" in several variations' -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_begin_subtest "No new messages" -output=$(NOTMUCH_NEW) +output=$(NOTMUCH_NEW --debug) test_expect_equal "$output" "No new mail." test_begin_subtest "Single new message" generate_message -output=$(NOTMUCH_NEW) +output=$(NOTMUCH_NEW --debug) test_expect_equal "$output" "Added 1 new message to the database." test_begin_subtest "Multiple new messages" generate_message generate_message -output=$(NOTMUCH_NEW) +output=$(NOTMUCH_NEW --debug) test_expect_equal "$output" "Added 2 new messages to the database." test_begin_subtest "No new messages (non-empty DB)" -output=$(NOTMUCH_NEW) +output=$(NOTMUCH_NEW --debug) test_expect_equal "$output" "No new mail." @@ -31,7 +31,7 @@ mkdir "${MAIL_DIR}"/def mkdir "${MAIL_DIR}"/ghi generate_message [dir]=def -output=$(NOTMUCH_NEW) +output=$(NOTMUCH_NEW --debug) test_expect_equal "$output" "Added 1 new message to the database." @@ -42,7 +42,7 @@ mv "${MAIL_DIR}"/ghi "${MAIL_DIR}"/abc rm "${MAIL_DIR}"/def/* generate_message [dir]=abc -output=$(NOTMUCH_NEW) +output=$(NOTMUCH_NEW --debug) test_expect_equal "$output" "Added 1 new message to the database." @@ -54,7 +54,7 @@ mkdir -p "$(dirname "$tmp_msg_filename")" mv "$gen_msg_filename" "$tmp_msg_filename" notmuch new > /dev/null mv "$tmp_msg_filename" "$gen_msg_filename" -output=$(NOTMUCH_NEW) +output=$(NOTMUCH_NEW --debug) test_expect_equal "$output" "Added 1 new message to the database." @@ -63,15 +63,18 @@ test_begin_subtest "Renamed message" generate_message notmuch new > /dev/null mv "$gen_msg_filename" "${gen_msg_filename}"-renamed -output=$(NOTMUCH_NEW) -test_expect_equal "$output" "No new mail. Detected 1 file rename." +output=$(NOTMUCH_NEW --debug) +test_expect_equal "$output" "(D) add_files_recursive, pass 2: queuing passed file ${gen_msg_filename} for deletion from database +No new mail. Detected 1 file rename." test_begin_subtest "Deleted message" rm "${gen_msg_filename}"-renamed -output=$(NOTMUCH_NEW) -test_expect_equal "$output" "No new mail. Removed 1 message." +output=$(NOTMUCH_NEW --debug) +test_expect_equal "$output" "(D) add_files_recursive, pass 3: queuing leftover file ${gen_msg_filename}-renamed for deletion from database +No new mail. Removed 1 message." + test_begin_subtest "Renamed directory" @@ -84,34 +87,39 @@ notmuch new > /dev/null mv "${MAIL_DIR}"/dir "${MAIL_DIR}"/dir-renamed -output=$(NOTMUCH_NEW) -test_expect_equal "$output" "No new mail. Detected 3 file renames." +output=$(NOTMUCH_NEW --debug) +test_expect_equal "$output" "(D) add_files_recursive, pass 2: queuing passed directory ${MAIL_DIR}/dir for deletion from database +No new mail. Detected 3 file renames." test_begin_subtest "Deleted directory" - +test_subtest_known_broken rm -rf "${MAIL_DIR}"/dir-renamed -output=$(NOTMUCH_NEW) -test_expect_equal "$output" "No new mail. Removed 3 messages." +output=$(NOTMUCH_NEW --debug) +test_expect_equal "$output" "(D) add_files_recursive, pass 2: queuing passed directory ${MAIL_DIR}/dir-renamed for deletion from database +No new mail. Removed 3 messages." test_begin_subtest "New directory (at end of list)" +test_subtest_known_broken generate_message [dir]=zzz generate_message [dir]=zzz generate_message [dir]=zzz -output=$(NOTMUCH_NEW) +output=$(NOTMUCH_NEW --debug) test_expect_equal "$output" "Added 3 new messages to the database." test_begin_subtest "Deleted directory (end of list)" +test_subtest_known_broken rm -rf "${MAIL_DIR}"/zzz -output=$(NOTMUCH_NEW) -test_expect_equal "$output" "No new mail. Removed 3 messages." +output=$(NOTMUCH_NEW --debug) +test_expect_equal "$output" "(D) add_files_recursive, pass 3: queuing leftover directory ${MAIL_DIR}/zzz for deletion from database +No new mail. Removed 3 messages." test_begin_subtest "New symlink to directory" @@ -122,7 +130,7 @@ mv "${MAIL_DIR}" "${TMP_DIRECTORY}"/actual_maildir mkdir "${MAIL_DIR}" ln -s "${TMP_DIRECTORY}"/actual_maildir "${MAIL_DIR}"/symlink -output=$(NOTMUCH_NEW) +output=$(NOTMUCH_NEW --debug) test_expect_equal "$output" "Added 1 new message to the database." @@ -132,13 +140,13 @@ external_msg_filename="${TMP_DIRECTORY}"/external/"$(basename "$gen_msg_filename mkdir -p "$(dirname "$external_msg_filename")" mv "$gen_msg_filename" "$external_msg_filename" ln -s "$external_msg_filename" "$gen_msg_filename" -output=$(NOTMUCH_NEW) +output=$(NOTMUCH_NEW --debug) test_expect_equal "$output" "Added 1 new message to the database." test_begin_subtest "Broken symlink aborts" ln -s does-not-exist "${MAIL_DIR}/broken" -output=$(NOTMUCH_NEW 2>&1) +output=$(NOTMUCH_NEW --debug 2>&1) test_expect_equal "$output" \ "Error reading file ${MAIL_DIR}/broken: No such file or directory Note: A fatal error was encountered: Something went wrong trying to read or write a file @@ -152,7 +160,7 @@ generate_message [dir]=two/levels generate_message [dir]=two/levels generate_message [dir]=two/levels -output=$(NOTMUCH_NEW) +output=$(NOTMUCH_NEW --debug) test_expect_equal "$output" "Added 3 new messages to the database." @@ -160,10 +168,12 @@ test_begin_subtest "Deleted two-level directory" rm -rf "${MAIL_DIR}"/two -output=$(NOTMUCH_NEW) -test_expect_equal "$output" "No new mail. Removed 3 messages." +output=$(NOTMUCH_NEW --debug) +test_expect_equal "$output" "(D) add_files_recursive, pass 3: queuing leftover directory ${MAIL_DIR}/two for deletion from database +No new mail. Removed 3 messages." -test_begin_subtest "Support single-message mbox (deprecated)" +test_begin_subtest "Support single-message mbox" +test_subtest_known_broken cat > "${MAIL_DIR}"/mbox_file1 < @@ -172,11 +182,12 @@ Subject: Test mbox message 1 Body. EOF -output=$(NOTMUCH_NEW 2>&1) +output=$(NOTMUCH_NEW --debug 2>&1) test_expect_equal "$output" "Added 1 new message to the database." # This test requires that notmuch new has been run at least once. test_begin_subtest "Skip and report non-mail files" +test_subtest_known_broken generate_message mkdir -p "${MAIL_DIR}"/.git && touch "${MAIL_DIR}"/.git/config touch "${MAIL_DIR}"/ignored_file @@ -196,7 +207,7 @@ Subject: Test mbox message 2 Body 2. EOF -output=$(NOTMUCH_NEW 2>&1) +output=$(NOTMUCH_NEW --debug 2>&1) test_expect_equal "$output" \ "Note: Ignoring non-mail file: ${MAIL_DIR}/.git/config Note: Ignoring non-mail file: ${MAIL_DIR}/.ignored_hidden_file @@ -235,6 +246,7 @@ test_expect_equal "$output" \ (D) add_files_recursive, pass 2: explicitly ignoring ${MAIL_DIR}/one/two/ignored_file (D) add_files_recursive, pass 2: explicitly ignoring ${MAIL_DIR}/one/two/three/.git (D) add_files_recursive, pass 2: explicitly ignoring ${MAIL_DIR}/one/two/three/ignored_file +(D) add_files_recursive, pass 3: queuing leftover directory ${MAIL_DIR}/two for deletion from database No new mail." @@ -263,23 +275,23 @@ OLDCONFIG=$(notmuch config get new.tags) test_begin_subtest "Empty tags in new.tags are forbidden" notmuch config set new.tags "foo;;bar" -output=$(NOTMUCH_NEW 2>&1) +output=$(NOTMUCH_NEW --debug 2>&1) test_expect_equal "$output" "Error: tag '' in new.tags: empty tag forbidden" test_begin_subtest "Tags starting with '-' in new.tags are forbidden" notmuch config set new.tags "-foo;bar" -output=$(NOTMUCH_NEW 2>&1) +output=$(NOTMUCH_NEW --debug 2>&1) test_expect_equal "$output" "Error: tag '-foo' in new.tags: tag starting with '-' forbidden" test_expect_code 1 "Invalid tags set exit code" \ - "NOTMUCH_NEW 2>&1" + "NOTMUCH_NEW --debug 2>&1" notmuch config set new.tags $OLDCONFIG test_begin_subtest "Xapian exception: read only files" chmod u-w ${MAIL_DIR}/.notmuch/xapian/*.DB -output=$(NOTMUCH_NEW 2>&1 | sed 's/: .*$//' ) +output=$(NOTMUCH_NEW --debug 2>&1 | sed 's/: .*$//' ) chmod u+w ${MAIL_DIR}/.notmuch/xapian/*.DB test_expect_equal "$output" "A Xapian exception occurred opening database" diff --git a/test/T060-count.sh b/test/T060-count.sh index da86c8cc..3fec94e8 100755 --- a/test/T060-count.sh +++ b/test/T060-count.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description='"notmuch count" for messages and threads' -. ./test-lib.sh +. ./test-lib.sh || exit 1 add_email_corpus @@ -93,5 +93,35 @@ notmuch count --output=messages >>EXPECTED notmuch count --output=messages tag:inbox >>EXPECTED test_expect_equal_file EXPECTED OUTPUT +backup_database +test_begin_subtest "error message for database open" +dd if=/dev/zero of="${MAIL_DIR}/.notmuch/xapian/postlist.DB" count=3 +notmuch count '*' 2>OUTPUT 1>/dev/null +output=$(sed 's/^\(A Xapian exception [^:]*\):.*$/\1/' OUTPUT) +test_expect_equal "${output}" "A Xapian exception occurred opening database" +restore_database + +cat < count-files.gdb +set breakpoint pending on +break count_files +commands +shell cp /dev/null ${MAIL_DIR}/.notmuch/xapian/postlist.DB +continue +end +run +EOF + +backup_database +test_begin_subtest "error message from query_search_messages" +gdb --batch-silent --return-child-result -x count-files.gdb \ + --args notmuch count --output=files '*' 2>OUTPUT 1>/dev/null +cat < EXPECTED +notmuch count: A Xapian exception occurred +A Xapian exception occurred performing query +Query string was: * +EOF +sed 's/^\(A Xapian exception [^:]*\):.*$/\1/' < OUTPUT > OUTPUT.clean +test_expect_equal_file EXPECTED OUTPUT.clean +restore_database test_done diff --git a/test/T070-insert.sh b/test/T070-insert.sh index 74f19552..e7ec6a6c 100755 --- a/test/T070-insert.sh +++ b/test/T070-insert.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description='"notmuch insert"' -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_require_external_prereq gdb @@ -188,7 +188,7 @@ notmuch config set new.tags $OLDCONFIG # DUPLICATE_MESSAGE_ID is not tested here, because it should actually pass. for code in OUT_OF_MEMORY XAPIAN_EXCEPTION FILE_NOT_EMAIL \ - READ_ONLY_DATABASE UPGRADE_REQUIRED; do + READ_ONLY_DATABASE UPGRADE_REQUIRED PATH_ERROR; do gen_insert_msg cat < index-file-$code.gdb set breakpoint pending on diff --git a/test/T080-search.sh b/test/T080-search.sh index 05027fb0..5e8b20ce 100755 --- a/test/T080-search.sh +++ b/test/T080-search.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description='"notmuch search" in several variations' -. ./test-lib.sh +. ./test-lib.sh || exit 1 add_email_corpus diff --git a/test/T090-search-output.sh b/test/T090-search-output.sh index fe2ec9af..a28eaf20 100755 --- a/test/T090-search-output.sh +++ b/test/T090-search-output.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description='various settings for "notmuch search --output="' -. ./test-lib.sh +. ./test-lib.sh || exit 1 add_email_corpus diff --git a/test/T095-address.sh b/test/T095-address.sh index ed0cac78..a194faf3 100755 --- a/test/T095-address.sh +++ b/test/T095-address.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description='"notmuch address" in several variants' -. ./test-lib.sh +. ./test-lib.sh || exit 1 add_email_corpus @@ -145,4 +145,148 @@ cat <EXPECTED EOF test_expect_equal_file OUTPUT EXPECTED +test_begin_subtest "--deduplicate=no --sort=oldest-first --output=sender" +notmuch address --deduplicate=no --sort=oldest-first --output=sender '*' >OUTPUT +cat <EXPECTED +Mikhail Gusarov +Mikhail Gusarov +Carl Worth +Lars Kellogg-Stedman +Mikhail Gusarov +Alex Botero-Lowry +Carl Worth +Lars Kellogg-Stedman +Mikhail Gusarov +Mikhail Gusarov +Keith Packard +Keith Packard +Keith Packard +Jan Janak +Jan Janak +Jan Janak +Israel Herraiz +Adrian Perez de Castro +Aron Griffis +Ingmar Vanhassel +Alex Botero-Lowry +Lars Kellogg-Stedman +Lars Kellogg-Stedman +Lars Kellogg-Stedman +Stewart Smith +Stewart Smith +Keith Packard +Keith Packard +Keith Packard +Stewart Smith +Jjgod Jiang +Jan Janak +Rolland Santimano +Alexander Botero-Lowry +Jjgod Jiang +Alexander Botero-Lowry +Alexander Botero-Lowry +Keith Packard +Alexander Botero-Lowry +Carl Worth +Carl Worth +Carl Worth +Carl Worth +Carl Worth +Carl Worth +Carl Worth +Carl Worth +Carl Worth +Carl Worth +Chris Wilson +Olivier Berger +François Boulogne +EOF +test_expect_equal_file OUTPUT EXPECTED + +test_begin_subtest "--deduplicate=no --sort=newest-first --output=sender --output=recipients" +notmuch address --deduplicate=no --sort=newest-first --output=sender --output=recipients path:foo/new >OUTPUT +cat <EXPECTED +Mikhail Gusarov +notmuch@notmuchmail.org +Mikhail Gusarov +notmuch@notmuchmail.org +Lars Kellogg-Stedman +notmuch@notmuchmail.org +EOF +test_expect_equal_file OUTPUT EXPECTED + +test_begin_subtest "--deduplicate=address --output=sender --output=recipients" +notmuch address --deduplicate=address --output=sender --output=recipients '*' | sort >OUTPUT +cat <EXPECTED +"Discussion about the Arch User Repository (AUR)" +Adrian Perez de Castro +Alexander Botero-Lowry +Allan McRae +Aron Griffis +Carl Worth +Chris Wilson +François Boulogne +Ingmar Vanhassel +Israel Herraiz +Jan Janak +Jjgod Jiang +Keith Packard +Lars Kellogg-Stedman +Mikhail Gusarov +Olivier Berger +Rolland Santimano +Stewart Smith +notmuch@notmuchmail.org +EOF +test_expect_equal_file OUTPUT EXPECTED + +generate_message '[from]="Foo Bar "' +generate_message '[from]="Foo Bar "' +generate_message '[from]="Foo Bar "' +generate_message '[from]="Bar "' +generate_message '[from]="Foo "' +generate_message '[from]=""' +generate_message '[from]="foo.bar@example.com"' +generate_message '[from]="Baz "' +generate_message '[from]="Foo Bar "' +generate_message '[from]="Baz "' +notmuch new > /dev/null + +test_begin_subtest "--deduplicate=no --output=sender" +notmuch address --deduplicate=no --output=sender from:example.com | sort >OUTPUT +cat <EXPECTED +Bar +Baz +Baz +Foo +Foo Bar +Foo Bar +Foo Bar +Foo Bar +foo.bar@example.com +foo.bar@example.com +EOF +test_expect_equal_file OUTPUT EXPECTED + +test_begin_subtest "--deduplicate=mailbox --output=sender --output=count" +notmuch address --deduplicate=mailbox --output=sender --output=count from:example.com | sort -n >OUTPUT +cat <EXPECTED +1 Bar +1 Foo +1 Foo Bar +1 Foo Bar +2 Baz +2 Foo Bar +2 foo.bar@example.com +EOF +test_expect_equal_file OUTPUT EXPECTED + +test_begin_subtest "--deduplicate=address --output=sender --output=count" +notmuch address --deduplicate=address --output=sender --output=count from:example.com | sort -n >OUTPUT +cat <EXPECTED +3 Baz +7 Foo Bar +EOF +test_expect_equal_file OUTPUT EXPECTED + test_done diff --git a/test/T100-search-by-folder.sh b/test/T100-search-by-folder.sh index 583bdf5e..2844ec61 100755 --- a/test/T100-search-by-folder.sh +++ b/test/T100-search-by-folder.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description='"notmuch search" by folder: and path: (with variations)' -. ./test-lib.sh +. ./test-lib.sh || exit 1 add_message '[dir]=bad' '[subject]="To the bone"' add_message '[dir]=.' '[subject]="Top level"' diff --git a/test/T110-search-position-overlap-bug.sh b/test/T110-search-position-overlap-bug.sh index 5da6ad6f..2a4238f9 100755 --- a/test/T110-search-position-overlap-bug.sh +++ b/test/T110-search-position-overlap-bug.sh @@ -18,7 +18,7 @@ # id:3wd4o8wa7fx.fsf@testarossa.amd.com test_description='that notmuch does not overlap term positions' -. ./test-lib.sh +. ./test-lib.sh || exit 1 add_message '[to]="a@b.c, x@y.z"' diff --git a/test/T120-search-insufficient-from-quoting.sh b/test/T120-search-insufficient-from-quoting.sh index e83ea3d1..4862d826 100755 --- a/test/T120-search-insufficient-from-quoting.sh +++ b/test/T120-search-insufficient-from-quoting.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description='messages with unquoted . in name' -. ./test-lib.sh +. ./test-lib.sh || exit 1 add_message \ '[from]="Some.Name for Someone "' \ diff --git a/test/T130-search-limiting.sh b/test/T130-search-limiting.sh index 303762cf..c8986f4e 100755 --- a/test/T130-search-limiting.sh +++ b/test/T130-search-limiting.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description='"notmuch search" --offset and --limit parameters' -. ./test-lib.sh +. ./test-lib.sh || exit 1 add_email_corpus diff --git a/test/T140-excludes.sh b/test/T140-excludes.sh index 8bbbc2dd..f91d4d7f 100755 --- a/test/T140-excludes.sh +++ b/test/T140-excludes.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description='"notmuch search, count and show" with excludes in several variations' -. ./test-lib.sh +. ./test-lib.sh || exit 1 # Generates a thread consisting of a top level message and 'length' # replies. The subject of the top message 'subject: top message" diff --git a/test/T150-tagging.sh b/test/T150-tagging.sh index 4a2673d4..821d3933 100755 --- a/test/T150-tagging.sh +++ b/test/T150-tagging.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description='"notmuch tag"' -. ./test-lib.sh +. ./test-lib.sh || exit 1 add_message '[subject]=One' add_message '[subject]=Two' diff --git a/test/T160-json.sh b/test/T160-json.sh index c1cf649d..b346f37e 100755 --- a/test/T160-json.sh +++ b/test/T160-json.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description="--format=json output" -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_begin_subtest "Show message: json" add_message "[subject]=\"json-show-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[bcc]=\"test_suite+bcc@notmuchmail.org\"" "[reply-to]=\"test_suite+replyto@notmuchmail.org\"" "[body]=\"json-show-message\"" diff --git a/test/T170-sexp.sh b/test/T170-sexp.sh index 667e3195..800ebc63 100755 --- a/test/T170-sexp.sh +++ b/test/T170-sexp.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description="--format=sexp output" -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_begin_subtest "Show message: sexp" add_message "[subject]=\"sexp-show-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[bcc]=\"test_suite+bcc@notmuchmail.org\"" "[reply-to]=\"test_suite+replyto@notmuchmail.org\"" "[body]=\"sexp-show-message\"" diff --git a/test/T180-text.sh b/test/T180-text.sh index b5ccefc9..3a265dbd 100755 --- a/test/T180-text.sh +++ b/test/T180-text.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description="--format=text output" -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_begin_subtest "Show message: text" add_message "[subject]=\"text-show-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"text-show-message\"" diff --git a/test/T190-multipart.sh b/test/T190-multipart.sh index ad8d29ea..7c4c9f71 100755 --- a/test/T190-multipart.sh +++ b/test/T190-multipart.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description="output of multipart message" -. ./test-lib.sh +. ./test-lib.sh || exit 1 cat < embedded_message From: Carl Worth diff --git a/test/T200-thread-naming.sh b/test/T200-thread-naming.sh index dcfc1b36..132c1d77 100755 --- a/test/T200-thread-naming.sh +++ b/test/T200-thread-naming.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description="naming of threads with changing subject" -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_begin_subtest "Initial thread name (oldest-first search)" add_message '[subject]="thread-naming: Initial thread subject"' \ diff --git a/test/T205-author-naming.sh b/test/T205-author-naming.sh index cb678ae8..69d8dc50 100755 --- a/test/T205-author-naming.sh +++ b/test/T205-author-naming.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description="naming of authors with unusual addresses" -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_begin_subtest "Add author with empty quoted real name" add_message '[subject]="author-naming: Initial thread subject"' \ diff --git a/test/T210-raw.sh b/test/T210-raw.sh index daf5735c..dfea2d19 100755 --- a/test/T210-raw.sh +++ b/test/T210-raw.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash test_description='notmuch show --format=raw' -. ./test-lib.sh +. ./test-lib.sh || exit 1 add_message add_message diff --git a/test/T220-reply.sh b/test/T220-reply.sh index b0d854a1..30b78f67 100755 --- a/test/T220-reply.sh +++ b/test/T220-reply.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description="\"notmuch reply\" in several variations" -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_begin_subtest "Basic reply" add_message '[from]="Sender "' \ diff --git a/test/T230-reply-to-sender.sh b/test/T230-reply-to-sender.sh index 30e5e385..608334dc 100755 --- a/test/T230-reply-to-sender.sh +++ b/test/T230-reply-to-sender.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description="\"notmuch reply --reply-to=sender\" in several variations" -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_begin_subtest "Basic reply-to-sender" add_message '[from]="Sender "' \ diff --git a/test/T240-dump-restore.sh b/test/T240-dump-restore.sh index efe463ea..e6976ff8 100755 --- a/test/T240-dump-restore.sh +++ b/test/T240-dump-restore.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description="\"notmuch dump\" and \"notmuch restore\"" -. ./test-lib.sh +. ./test-lib.sh || exit 1 add_email_corpus diff --git a/test/T250-uuencode.sh b/test/T250-uuencode.sh index b3e1ac19..6f45d395 100755 --- a/test/T250-uuencode.sh +++ b/test/T250-uuencode.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description="handling of uuencoded data" -. ./test-lib.sh +. ./test-lib.sh || exit 1 add_message [subject]=uuencodetest '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' \ '[body]="This message is used to ensure that notmuch correctly handles a diff --git a/test/T260-thread-order.sh b/test/T260-thread-order.sh index 5239bd44..f720c998 100755 --- a/test/T260-thread-order.sh +++ b/test/T260-thread-order.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description="threading when messages received out of order" -. ./test-lib.sh +. ./test-lib.sh || exit 1 # Generate all single-root four message thread structures. We'll use # this for multiple tests below. diff --git a/test/T270-author-order.sh b/test/T270-author-order.sh index 6ffeffc7..9124ece6 100755 --- a/test/T270-author-order.sh +++ b/test/T270-author-order.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description="author reordering;" -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_begin_subtest "Adding parent message" generate_message [body]=findme [id]=new-parent-id [subject]=author-reorder-threadtest '[from]="User "' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' diff --git a/test/T280-from-guessing.sh b/test/T280-from-guessing.sh index 6dfaa40a..7c562fb9 100755 --- a/test/T280-from-guessing.sh +++ b/test/T280-from-guessing.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description="From line heuristics (with multiple configured addresses)" -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_begin_subtest "Magic from guessing (nothing to go on)" add_message '[from]="Sender "' \ diff --git a/test/T290-long-id.sh b/test/T290-long-id.sh index 85e620fa..1fb7c037 100755 --- a/test/T290-long-id.sh +++ b/test/T290-long-id.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description="messages with ridiculously-long message IDs" -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_begin_subtest "Referencing long ID before adding" generate_message '[subject]="Reference of ridiculously-long message ID"' \ diff --git a/test/T300-encoding.sh b/test/T300-encoding.sh index b6c86bf0..8d201c7e 100755 --- a/test/T300-encoding.sh +++ b/test/T300-encoding.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description="encoding issues" -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_begin_subtest "Message with text of unknown charset" add_message '[content-type]="text/plain; charset=unknown-8bit"' \ diff --git a/test/T310-emacs.sh b/test/T310-emacs.sh index d72799b4..61bc369a 100755 --- a/test/T310-emacs.sh +++ b/test/T310-emacs.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash test_description="emacs interface" -. ./test-lib.sh +. ./test-lib.sh || exit 1 EXPECTED=$TEST_DIRECTORY/emacs.expected-output diff --git a/test/T320-emacs-large-search-buffer.sh b/test/T320-emacs-large-search-buffer.sh index 8b1251fe..3fd6958a 100755 --- a/test/T320-emacs-large-search-buffer.sh +++ b/test/T320-emacs-large-search-buffer.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description="Emacs with large search results buffer" -. ./test-lib.sh +. ./test-lib.sh || exit 1 x=xxxxxxxxxx # 10 x=$x$x$x$x$x$x$x$x$x$x # 100 diff --git a/test/T330-emacs-subject-to-filename.sh b/test/T330-emacs-subject-to-filename.sh index 230c324d..517fa839 100755 --- a/test/T330-emacs-subject-to-filename.sh +++ b/test/T330-emacs-subject-to-filename.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash test_description="emacs: mail subject to filename" -. ./test-lib.sh +. ./test-lib.sh || exit 1 # emacs server can't be started in a child process with $(test_emacs ...) test_emacs '(ignore)' > /dev/null diff --git a/test/T340-maildir-sync.sh b/test/T340-maildir-sync.sh index 3186e70f..efeaa3f6 100755 --- a/test/T340-maildir-sync.sh +++ b/test/T340-maildir-sync.sh @@ -2,7 +2,7 @@ test_description="maildir synchronization" -. ./test-lib.sh +. ./test-lib.sh || exit 1 # Create the expected maildir structure mkdir $MAIL_DIR/cur diff --git a/test/T350-crypto.sh b/test/T350-crypto.sh index 477b397e..3656cce9 100755 --- a/test/T350-crypto.sh +++ b/test/T350-crypto.sh @@ -5,7 +5,7 @@ # - verification of signatures from expired/revoked keys test_description='PGP/MIME signature verification and decryption' -. ./test-lib.sh +. ./test-lib.sh || exit 1 add_gnupg_home () { diff --git a/test/T360-symbol-hiding.sh b/test/T360-symbol-hiding.sh index d2b5d1f5..4ec0ea65 100755 --- a/test/T360-symbol-hiding.sh +++ b/test/T360-symbol-hiding.sh @@ -9,18 +9,19 @@ test_description='exception symbol hiding' -. ./test-lib.sh - -run_test(){ - result=$(LD_LIBRARY_PATH="$TEST_DIRECTORY/../lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" $TEST_DIRECTORY/symbol-test 2>&1) -} - -output="A Xapian exception occurred opening database: Couldn't stat 'fakedb/.notmuch/xapian' -caught No chert database found at path \`./nonexistent'" - -mkdir -p fakedb/.notmuch - -test_expect_success 'running test' run_test +. ./test-lib.sh || exit 1 + +test_begin_subtest 'running test' run_test +mkdir -p ${PWD}/fakedb/.notmuch +( LD_LIBRARY_PATH="$TEST_DIRECTORY/../lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \ + $TEST_DIRECTORY/symbol-test ${PWD}/fakedb ${PWD}/nonexistent \ + 2>&1 | sed "s,${PWD},CWD,g") > OUTPUT + +cat < EXPECTED +A Xapian exception occurred opening database: Couldn't stat 'CWD/fakedb/.notmuch/xapian' +caught No chert database found at path \`CWD/nonexistent' +EOF +test_expect_equal_file EXPECTED OUTPUT test_begin_subtest 'checking output' test_expect_equal "$result" "$output" diff --git a/test/T370-search-folder-coherence.sh b/test/T370-search-folder-coherence.sh index 5e72a6cc..da4ec2b8 100755 --- a/test/T370-search-folder-coherence.sh +++ b/test/T370-search-folder-coherence.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description='folder tags removed and added through file renames remain consistent' -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_begin_subtest "No new messages" output=$(NOTMUCH_NEW) diff --git a/test/T380-atomicity.sh b/test/T380-atomicity.sh index ee1e2f43..845dfde7 100755 --- a/test/T380-atomicity.sh +++ b/test/T380-atomicity.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description='atomicity' -. ./test-lib.sh +. ./test-lib.sh || exit 1 # This script tests the effects of killing and restarting "notmuch # new" at arbitrary points. If notmuch new is properly atomic, the diff --git a/test/T390-python.sh b/test/T390-python.sh index 3f03a2e3..4726bc36 100755 --- a/test/T390-python.sh +++ b/test/T390-python.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash test_description="python bindings" -. ./test-lib.sh +. ./test-lib.sh || exit 1 + +test_require_external_prereq ${NOTMUCH_PYTHON} add_email_corpus @@ -11,7 +13,7 @@ db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY) q_new = notmuch.Query(db, 'tag:inbox') q_new.set_sort(notmuch.Query.SORT.OLDEST_FIRST) for t in q_new.search_threads(): - print t.get_thread_id() + print (t.get_thread_id()) EOF notmuch search --sort=oldest-first --output=threads tag:inbox | sed s/^thread:// > EXPECTED test_expect_equal_file OUTPUT EXPECTED @@ -23,7 +25,7 @@ db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY) q_new = notmuch.Query(db, 'tag:inbox') q_new.set_sort(notmuch.Query.SORT.OLDEST_FIRST) for m in q_new.search_messages(): - print m.get_message_id() + print (m.get_message_id()) EOF notmuch search --sort=oldest-first --output=messages tag:inbox | sed s/^id:// > EXPECTED test_expect_equal_file OUTPUT EXPECTED @@ -32,7 +34,7 @@ test_begin_subtest "get non-existent file" test_python < EXPECTED +test_expect_equal_file OUTPUT EXPECTED + +test_begin_subtest "compare message ids" +test_ruby <<"EOF" +require 'notmuch' +$maildir = ENV['MAIL_DIR'] +if not $maildir then + abort('environment variable MAIL_DIR must be set') +end +@db = Notmuch::Database.new($maildir) +@q = @db.query('tag:inbox') +@q.sort = Notmuch::SORT_OLDEST_FIRST +for m in @q.search_messages do + print m.message_id, "\n" +end +EOF +notmuch search --sort=oldest-first --output=messages tag:inbox | sed s/^id:// > EXPECTED +test_expect_equal_file OUTPUT EXPECTED + +test_begin_subtest "get non-existent file" +test_ruby <<"EOF" +require 'notmuch' +$maildir = ENV['MAIL_DIR'] +if not $maildir then + abort('environment variable MAIL_DIR must be set') +end +@db = Notmuch::Database.new($maildir) +result = @db.find_message_by_filename('i-dont-exist') +print (result == nil) +EOF +test_expect_equal "$(cat OUTPUT)" "true" + +test_begin_subtest "count messages" +test_ruby <<"EOF" +require 'notmuch' +$maildir = ENV['MAIL_DIR'] +if not $maildir then + abort('environment variable MAIL_DIR must be set') +end +@db = Notmuch::Database.new($maildir) +@q = @db.query('tag:inbox') +print @q.count_messages(),"\n" +EOF +notmuch count --output=messages tag:inbox > EXPECTED +test_expect_equal_file OUTPUT EXPECTED + +test_begin_subtest "count threads" +test_ruby <<"EOF" +require 'notmuch' +$maildir = ENV['MAIL_DIR'] +if not $maildir then + abort('environment variable MAIL_DIR must be set') +end +@db = Notmuch::Database.new($maildir) +@q = @db.query('tag:inbox') +print @q.count_threads(),"\n" +EOF +notmuch count --output=threads tag:inbox > EXPECTED +test_expect_equal_file OUTPUT EXPECTED + +test_done diff --git a/test/T400-hooks.sh b/test/T400-hooks.sh index 1adab2f0..ed119131 100755 --- a/test/T400-hooks.sh +++ b/test/T400-hooks.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description='hooks' -. ./test-lib.sh +. ./test-lib.sh || exit 1 HOOK_DIR=${MAIL_DIR}/.notmuch/hooks diff --git a/test/T410-argument-parsing.sh b/test/T410-argument-parsing.sh index 2e5d7ae3..f8ff8ff9 100755 --- a/test/T410-argument-parsing.sh +++ b/test/T410-argument-parsing.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description="argument parsing" -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_begin_subtest "sanity check" $TEST_DIRECTORY/arg-test pos1 --keyword=one --string=foo pos2 --int=7 --flag=one --flag=three > OUTPUT diff --git a/test/T420-emacs-test-functions.sh b/test/T420-emacs-test-functions.sh index ca4a7988..955c5f7f 100755 --- a/test/T420-emacs-test-functions.sh +++ b/test/T420-emacs-test-functions.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash test_description="emacs test function sanity" -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_begin_subtest "emacs test function sanity" test_emacs_expect_t 't' diff --git a/test/T430-emacs-address-cleaning.sh b/test/T430-emacs-address-cleaning.sh index 04723467..664b79d2 100755 --- a/test/T430-emacs-address-cleaning.sh +++ b/test/T430-emacs-address-cleaning.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash test_description="emacs address cleaning" -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_begin_subtest "notmuch-test-address-clean part 1" test_emacs_expect_t '(notmuch-test-address-cleaning-1)' diff --git a/test/T440-emacs-hello.sh b/test/T440-emacs-hello.sh index f7296166..a8ed2282 100755 --- a/test/T440-emacs-hello.sh +++ b/test/T440-emacs-hello.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash test_description="emacs notmuch-hello view" -. ./test-lib.sh +. ./test-lib.sh || exit 1 EXPECTED=$TEST_DIRECTORY/emacs.expected-output diff --git a/test/T450-emacs-show.sh b/test/T450-emacs-show.sh index bfcd5efe..0342a874 100755 --- a/test/T450-emacs-show.sh +++ b/test/T450-emacs-show.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash test_description="emacs notmuch-show view" -. ./test-lib.sh +. ./test-lib.sh || exit 1 EXPECTED=$TEST_DIRECTORY/emacs-show.expected-output diff --git a/test/T455-emacs-charsets.sh b/test/T455-emacs-charsets.sh index 3078f9c9..7624fa4d 100755 --- a/test/T455-emacs-charsets.sh +++ b/test/T455-emacs-charsets.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash test_description="emacs notmuch-show charset handling" -. ./test-lib.sh +. ./test-lib.sh || exit 1 UTF8_YEN=$'\xef\xbf\xa5' diff --git a/test/T460-emacs-tree.sh b/test/T460-emacs-tree.sh index 8e9f37cb..b6181b51 100755 --- a/test/T460-emacs-tree.sh +++ b/test/T460-emacs-tree.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash test_description="emacs tree view interface" -. test-lib.sh +. ./test-lib.sh || exit 1 EXPECTED=$TEST_DIRECTORY/tree.expected-output diff --git a/test/T470-missing-headers.sh b/test/T470-missing-headers.sh index cb38301c..e256c10a 100755 --- a/test/T470-missing-headers.sh +++ b/test/T470-missing-headers.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description='messages with missing headers' -. ./test-lib.sh +. ./test-lib.sh || exit 1 # Notmuch requires at least one of from, subject, or to or it will # ignore the file. Generate two messages so that together they cover diff --git a/test/T480-hex-escaping.sh b/test/T480-hex-escaping.sh index ad50e1bc..10527b18 100755 --- a/test/T480-hex-escaping.sh +++ b/test/T480-hex-escaping.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description="hex encoding and decoding" -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_begin_subtest "round trip" find $TEST_DIRECTORY/corpus -type f -print | sort | xargs cat > EXPECTED diff --git a/test/T490-parse-time-string.sh b/test/T490-parse-time-string.sh index 6aa9d433..ab90fcc5 100755 --- a/test/T490-parse-time-string.sh +++ b/test/T490-parse-time-string.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description="date/time parser module" -. ./test-lib.sh +. ./test-lib.sh || exit 1 # Sanity/smoke tests for the date/time parser independent of notmuch diff --git a/test/T500-search-date.sh b/test/T500-search-date.sh index 70bcf344..f5cea421 100755 --- a/test/T500-search-date.sh +++ b/test/T500-search-date.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash test_description="date:since..until queries" -. ./test-lib.sh +. ./test-lib.sh || exit 1 add_email_corpus @@ -8,6 +8,10 @@ test_begin_subtest "Absolute date range" output=$(notmuch search date:2010-12-16..12/16/2010 | notmuch_search_sanitize) test_expect_equal "$output" "thread:XXX 2010-12-16 [1/1] Olivier Berger; Essai accentué (inbox unread)" +test_begin_subtest "Absolute date range with 'same' operator" +output=$(notmuch search date:2010-12-16..! | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2010-12-16 [1/1] Olivier Berger; Essai accentué (inbox unread)" + test_begin_subtest "Absolute time range with TZ" notmuch search date:18-Nov-2009_02:19:26-0800..2009-11-18_04:49:52-06:00 | notmuch_search_sanitize > OUTPUT cat <EXPECTED diff --git a/test/T510-thread-replies.sh b/test/T510-thread-replies.sh index 1392fbed..5ab066ac 100755 --- a/test/T510-thread-replies.sh +++ b/test/T510-thread-replies.sh @@ -9,7 +9,7 @@ test_description='test of proper handling of in-reply-to and references headers' # database is constructed properly, even in the presence of # non-RFC-compliant headers' -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_begin_subtest "Use References when In-Reply-To is broken" add_message '[id]="foo@one.com"' \ diff --git a/test/T520-show.sh b/test/T520-show.sh index 0657c993..fb232a32 100755 --- a/test/T520-show.sh +++ b/test/T520-show.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash test_description='"notmuch show"' -. ./test-lib.sh +. ./test-lib.sh || exit 1 add_email_corpus diff --git a/test/T530-upgrade.sh b/test/T530-upgrade.sh index 6b42a690..7faf03d3 100755 --- a/test/T530-upgrade.sh +++ b/test/T530-upgrade.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash test_description="database upgrade" -. ./test-lib.sh +. ./test-lib.sh || exit 1 dbtarball=database-v1.tar.xz diff --git a/test/T550-db-features.sh b/test/T550-db-features.sh index 5569768c..f94a660d 100755 --- a/test/T550-db-features.sh +++ b/test/T550-db-features.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash test_description="database version and feature compatibility" -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_begin_subtest "future database versions abort open" ${TEST_DIRECTORY}/make-db-version ${MAIL_DIR} 9999 "" diff --git a/test/T560-lib-error.sh b/test/T560-lib-error.sh index c99b17ed..c280939c 100755 --- a/test/T560-lib-error.sh +++ b/test/T560-lib-error.sh @@ -1,17 +1,7 @@ #!/usr/bin/env bash test_description="error reporting for library" -. ./test-lib.sh - -backup_database () { - rm -rf notmuch-dir-backup - cp -pR ${MAIL_DIR}/.notmuch notmuch-dir-backup -} -restore_database () { - rm -rf ${MAIL_DIR}/.notmuch - cp -pR notmuch-dir-backup ${MAIL_DIR}/.notmuch -} - +. ./test-lib.sh || exit 1 add_email_corpus @@ -35,7 +25,7 @@ Error: Cannot open a database for a NULL path. EOF test_expect_equal_file EXPECTED OUTPUT -test_begin_subtest "Open nonexistent database" +test_begin_subtest "Open relative path" test_C <<'EOF' #include #include @@ -49,7 +39,43 @@ EOF cat <<'EOF' >EXPECTED == stdout == == stderr == -Error opening database at ./nonexistent/foo/.notmuch: No such file or directory +Error: Database path must be absolute. +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Create database in relative path" +test_C <<'EOF' +#include +#include +int main (int argc, char** argv) +{ + notmuch_database_t *db; + notmuch_status_t stat; + stat = notmuch_database_create ("./nonexistent/foo", &db); +} +EOF +cat <<'EOF' >EXPECTED +== stdout == +== stderr == +Error: Database path must be absolute. +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Open nonexistent database" +test_C ${PWD}/nonexistent/foo <<'EOF' +#include +#include +int main (int argc, char** argv) +{ + notmuch_database_t *db; + notmuch_status_t stat; + stat = notmuch_database_open (argv[1], 0, 0); +} +EOF +cat <<'EOF' >EXPECTED +== stdout == +== stderr == +Error opening database at CWD/nonexistent/foo/.notmuch: No such file or directory EOF test_expect_equal_file EXPECTED OUTPUT @@ -70,21 +96,21 @@ Error: Cannot create a database for a NULL path. EOF test_expect_equal_file EXPECTED OUTPUT -test_begin_subtest "Create database in non-existant directory" -test_C <<'EOF' +test_begin_subtest "Create database in nonexistent directory" +test_C ${PWD}/nonexistent/foo<<'EOF' #include #include int main (int argc, char** argv) { notmuch_database_t *db; notmuch_status_t stat; - stat = notmuch_database_create ("./nonexistent/foo", &db); + stat = notmuch_database_create (argv[1], &db); } EOF cat <<'EOF' >EXPECTED == stdout == == stderr == -Error: Cannot create database at ./nonexistent/foo: No such file or directory. +Error: Cannot create database at CWD/nonexistent/foo: No such file or directory. EOF test_expect_equal_file EXPECTED OUTPUT diff --git a/test/T570-revision-tracking.sh b/test/T570-revision-tracking.sh new file mode 100755 index 00000000..0936011a --- /dev/null +++ b/test/T570-revision-tracking.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +test_description="database revision tracking" + +. ./test-lib.sh || exit 1 + +add_email_corpus + +test_begin_subtest "notmuch_database_get_revision" +test_C ${MAIL_DIR} <<'EOF' +#include +#include +#include +int main (int argc, char** argv) +{ + notmuch_database_t *db; + notmuch_status_t stat; + unsigned long revision; + const char *uuid; + + unsigned long rev; + + stat = notmuch_database_open (argv[1], NOTMUCH_DATABASE_MODE_READ_ONLY, &db); + if (stat) + fputs ("open failed\n", stderr); + revision = notmuch_database_get_revision (db, &uuid); + printf("%s\t%lu\n", uuid, revision); +} +EOF +notmuch_uuid_sanitize < OUTPUT > CLEAN +cat <<'EOF' >EXPECTED +== stdout == +UUID 53 +== stderr == +EOF +test_expect_equal_file EXPECTED CLEAN + +grep '^[0-9a-f]' OUTPUT > INITIAL_OUTPUT + +test_begin_subtest "output of count matches test code" +notmuch count --lastmod '*' | cut -f2-3 > OUTPUT +test_expect_equal_file INITIAL_OUTPUT OUTPUT + +test_begin_subtest "modification count increases" +before=$(notmuch count --lastmod '*' | cut -f3) +notmuch tag +a-random-tag-8743632 '*' +after=$(notmuch count --lastmod '*' | cut -f3) +result=$(($before < $after)) +test_expect_equal 1 ${result} + +notmuch count --lastmod '*' | cut -f2 > UUID + +test_expect_success 'search succeeds with correct uuid' \ + "notmuch search --uuid=$(cat UUID) '*'" + +test_expect_success 'uuid works as global option ' \ + "notmuch --uuid=$(cat UUID) search '*'" + +test_expect_code 1 'uuid works as global option II' \ + "notmuch --uuid=this-is-no-uuid search '*'" + +test_expect_code 1 'search fails with incorrect uuid' \ + "notmuch search --uuid=this-is-no-uuid '*'" + +test_expect_success 'show succeeds with correct uuid' \ + "notmuch show --uuid=$(cat UUID) '*'" + +test_expect_code 1 'show fails with incorrect uuid' \ + "notmuch show --uuid=this-is-no-uuid '*'" + +test_expect_success 'tag succeeds with correct uuid' \ + "notmuch tag --uuid=$(cat UUID) +test '*'" + +test_expect_code 1 'tag fails with incorrect uuid' \ + "notmuch tag --uuid=this-is-no-uuid '*' +test2" + +test_begin_subtest 'lastmod:0.. matches everything' +total=$(notmuch count '*') +modtotal=$(notmuch count lastmod:0..) +test_expect_equal "$total" "$modtotal" + +test_begin_subtest 'lastmod:1000000.. matches nothing' +modtotal=$(notmuch count lastmod:1000000..) +test_expect_equal 0 "$modtotal" + +test_begin_subtest 'exclude one message using lastmod' +lastmod=$(notmuch count --lastmod '*' | cut -f3) +total=$(notmuch count '*') +notmuch tag +4EFC743A.3060609@april.org id:4EFC743A.3060609@april.org +subtotal=$(notmuch count lastmod:..$lastmod) +result=$(($subtotal == $total-1)) +test_expect_equal 1 "$result" + +test_done diff --git a/test/aggregate-results.sh b/test/aggregate-results.sh index b016edb9..63228546 100755 --- a/test/aggregate-results.sh +++ b/test/aggregate-results.sh @@ -82,7 +82,10 @@ if [ "$skipped" != "0" ]; then echo "$skipped $tests skipped." fi -if [ $success -gt 0 -a $fixed -eq 0 -a $failed -eq 0 -a $skipped -eq 0 ] +# Note that we currently do not consider skipped tests as failing the +# build. + +if [ $success -gt 0 -a $fixed -eq 0 -a $failed -eq 0 ] then exit 0 else diff --git a/test/random-corpus.c b/test/random-corpus.c index 790193d2..d74271d9 100644 --- a/test/random-corpus.c +++ b/test/random-corpus.c @@ -114,6 +114,25 @@ random_utf8_string (void *ctx, size_t char_count) return buf; } +/* stubs since we cannot link with notmuch.o */ +const notmuch_opt_desc_t notmuch_shared_options[] = { + { 0, 0, 0, 0, 0 } +}; + +char *notmuch_requested_db_uuid = NULL; + +void +notmuch_process_shared_options (unused (const char *dummy)) +{ +} + +int +notmuch_minimal_options (unused (const char *subcommand), + unused (int argc), + unused (char **argv)) +{ + return 0; +} int main (int argc, char **argv) diff --git a/test/symbol-test.cc b/test/symbol-test.cc index f17ddc85..7454838b 100644 --- a/test/symbol-test.cc +++ b/test/symbol-test.cc @@ -3,23 +3,28 @@ #include #include +int main (int argc, char** argv) +{ + notmuch_database_t *notmuch; + char *message = NULL; -int main() { - notmuch_database_t *notmuch; - char *message = NULL; + if (argc != 3) + return 1; - if (notmuch_database_open_verbose ("fakedb", NOTMUCH_DATABASE_MODE_READ_ONLY, ¬much, &message)) - if (message) { - fputs (message, stderr); - free (message); - } + if (notmuch_database_open_verbose (argv[1], NOTMUCH_DATABASE_MODE_READ_ONLY, + ¬much, &message)) { + if (message) { + fputs (message, stderr); + free (message); + } + } - try { - (void) new Xapian::WritableDatabase("./nonexistent", Xapian::DB_OPEN); - } catch (const Xapian::Error &error) { - printf("caught %s\n", error.get_msg().c_str()); - return 0; - } + try { + (void) new Xapian::WritableDatabase (argv[2], Xapian::DB_OPEN); + } catch (const Xapian::Error &error) { + printf("caught %s\n", error.get_msg().c_str()); + return 0; + } - return 1; + return 1; } diff --git a/test/test-lib-common.sh b/test/test-lib-common.sh index f99ed111..5eb618c1 100644 --- a/test/test-lib-common.sh +++ b/test/test-lib-common.sh @@ -34,13 +34,25 @@ find_notmuch_path () done } +backup_database () { + test_name=$(basename $0 .sh) + rm -rf notmuch-dir-backup."$test_name" + cp -pR ${MAIL_DIR}/.notmuch notmuch-dir-backup."${test_name}" +} + +restore_database () { + test_name=$(basename $0 .sh) + rm -rf ${MAIL_DIR}/.notmuch + cp -pR notmuch-dir-backup."${test_name}" ${MAIL_DIR}/.notmuch +} + # Test the binaries we have just built. The tests are kept in # 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 +. $notmuch_path/sh.config || exit 1 if test -n "$valgrind" then diff --git a/test/test-lib.sh b/test/test-lib.sh index 486d1c43..126911fb 100644 --- a/test/test-lib.sh +++ b/test/test-lib.sh @@ -96,7 +96,8 @@ _x32="$_x04$_x04$_x04$_x04$_x04$_x04$_x04$_x04" # test_description='Description of this test... # This test checks if command xyzzy does the right thing... # ' -# . ./test-lib.sh +# . ./test-lib.sh || exit 1 + [ "x$ORIGINAL_TERM" != "xdumb" ] && ( TERM=$ORIGINAL_TERM && export TERM && @@ -487,7 +488,7 @@ emacs_deliver_message () (message-goto-body) (insert \"${body}\") $@ - (message-send-and-exit))" + (notmuch-mua-send-and-exit))" # In case message was sent properly, client waits for confirmation # before exiting and resuming control here; therefore making sure @@ -522,7 +523,7 @@ emacs_fcc_message () (message-goto-body) (insert \"${body}\") $@ - (message-send-and-exit))" || return 1 + (notmuch-mua-send-and-exit))" || return 1 notmuch new >/dev/null } @@ -621,9 +622,9 @@ test_expect_equal_json () { # The test suite forces LC_ALL=C, but this causes Python 3 to # decode stdin as ASCII. We need to read JSON in UTF-8, so # override Python's stdio encoding defaults. - output=$(echo "$1" | PYTHONIOENCODING=utf-8 python -mjson.tool \ + output=$(echo "$1" | PYTHONIOENCODING=utf-8 $NOTMUCH_PYTHON -mjson.tool \ || echo "$1") - expected=$(echo "$2" | PYTHONIOENCODING=utf-8 python -mjson.tool \ + expected=$(echo "$2" | PYTHONIOENCODING=utf-8 $NOTMUCH_PYTHON -mjson.tool \ || echo "$2") shift 2 test_expect_equal "$output" "$expected" "$@" @@ -719,6 +720,11 @@ notmuch_date_sanitize () sed \ -e 's/^Date: Fri, 05 Jan 2001 .*0000/Date: GENERATED_DATE/' } + +notmuch_uuid_sanitize () +{ + sed 's/[0-9a-f]\{8\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{12\}/UUID/g' +} # End of notmuch helper functions # Use test_set_prereq to tell that a particular prerequisite is available. @@ -1153,14 +1159,13 @@ test_python() { export LD_LIBRARY_PATH=$TEST_DIRECTORY/../lib export PYTHONPATH=$TEST_DIRECTORY/../bindings/python - # Some distros (e.g. Arch Linux) ship Python 2.* as /usr/bin/python2, - # most others as /usr/bin/python. So first try python2, and fallback to - # python if python2 doesn't exist. - cmd=python2 - [[ ${test_missing_external_prereq_[python2]} == t ]] && cmd=python - (echo "import sys; _orig_stdout=sys.stdout; sys.stdout=open('OUTPUT', 'w')"; cat) \ - | $cmd - + | $NOTMUCH_PYTHON - +} + +test_ruby() { + export LD_LIBRARY_PATH=$TEST_DIRECTORY/../lib + MAIL_DIR=$MAIL_DIR ruby -I $TEST_DIRECTORY/../bindings/ruby> OUTPUT } test_C () { @@ -1224,14 +1229,14 @@ test_init_ () { } -. ./test-lib-common.sh +. ./test-lib-common.sh || exit 1 emacs_generate_script # Use -P to resolve symlinks in our working directory so that the cwd # in subprocesses like git equals our $PWD (for pathname comparisons). -cd -P "$test" || error "Cannot setup test environment" +cd -P "$test" || error "Cannot set up test environment" if test "$verbose" = "t" then @@ -1320,5 +1325,4 @@ test_declare_external_prereq emacs test_declare_external_prereq ${TEST_EMACSCLIENT} test_declare_external_prereq gdb test_declare_external_prereq gpg -test_declare_external_prereq python -test_declare_external_prereq python2 +test_declare_external_prereq ${NOTMUCH_PYTHON} diff --git a/test/test-verbose b/test/test-verbose index 4100c051..1723ce66 100755 --- a/test/test-verbose +++ b/test/test-verbose @@ -2,7 +2,7 @@ test_description='the verbosity options of the test framework itself.' -. ./test-lib.sh +. ./test-lib.sh || exit 1 test_expect_success 'print something in test_expect_success and pass' ' echo "hello stdout" && diff --git a/util/string-util.c b/util/string-util.c index a90501ee..92af937f 100644 --- a/util/string-util.c +++ b/util/string-util.c @@ -221,3 +221,37 @@ parse_boolean_term (void *ctx, const char *str, errno = err; return -1; } + +int +strcmp_null (const char *s1, const char *s2) +{ + if (s1 && s2) + return strcmp (s1, s2); + else if (! s1 && ! s2) + return 0; + else if (s1) + return 1; /* s1 (non-NULL) is greater than s2 (NULL) */ + else + return -1; /* s1 (NULL) is less than s2 (non-NULL) */ +} + +int +strcase_equal (const void *a, const void *b) +{ + return strcasecmp (a, b) == 0; +} + +unsigned int +strcase_hash (const void *ptr) +{ + const char *s = ptr; + + /* This is the djb2 hash. */ + unsigned int hash = 5381; + while (s && *s) { + hash = ((hash << 5) + hash) + tolower (*s); + s++; + } + + return hash; +} diff --git a/util/string-util.h b/util/string-util.h index e409cb3d..87917b8f 100644 --- a/util/string-util.h +++ b/util/string-util.h @@ -64,6 +64,17 @@ int parse_boolean_term (void *ctx, const char *str, char **prefix_out, char **term_out); +/* strcmp that handles NULL strings; in strcmp terms a NULL string is + * considered to be less than a non-NULL string. + */ +int strcmp_null (const char *s1, const char *s2); + +/* GLib GEqualFunc compatible strcasecmp wrapper */ +int strcase_equal (const void *a, const void *b); + +/* GLib GHashFunc compatible case insensitive hash function */ +unsigned int strcase_hash (const void *ptr); + #ifdef __cplusplus } #endif