unofficial mirror of notmuch@notmuchmail.org
 help / color / mirror / code / Atom feed
* (no subject)
@ 2022-06-04 17:22 David Bremner
  2022-06-04 17:22 ` [PATCH v3 01/17] nmbug: promote to user tool "notmuch-git" David Bremner
                   ` (17 more replies)
  0 siblings, 18 replies; 19+ messages in thread
From: David Bremner @ 2022-06-04 17:22 UTC (permalink / raw)
  To: notmuch

This series is against branch "next", which is currently ahead of
master by the following 7 commits

9c1ed5ab CLI: document handling of --config for external commands
0d33392f CLI: pass --config to external commands via NOTMUCH_CONFIG.
163dae81 test: initial tests for external commands
a5a3ed90 CLI: set NOTMUCH_CONFIG in hooks.
cd050fd7 test: add known broken tests for setting NOTMUCH_CONFIG in hooks
e7732d21 CLI: mention sexp-queries in help topics, alphabetize
383d9db2 CLI: simplify help command

These are all on the list, and will probably be applied to master
within the next week or so.

The changes since last version are not too huge (interdiff at the end).
Functional changes are as follows

- drop --version command for notmuch commit (people can use notmuch
  --version to get the same info)

- check that there are tags with the given prefix in the database
  already; if not require --force to proceed.

- enable --config handling (this is actually in the prerequisite
  patches in branch next).


diff --git a/Makefile.local b/Makefile.local
index 0fadfb26..7699c208 100644
--- a/Makefile.local
+++ b/Makefile.local
@@ -45,6 +45,15 @@ $(SHA256_FILE): $(TAR_FILE)
 $(DETACHED_SIG_FILE): $(TAR_FILE)
 	gpg --armor --detach-sign $^
 
+CLEAN := $(CLEAN) notmuch-git
+notmuch-git: notmuch-git.py
+	cp $< $@
+	chmod ugo+x $@
+
+CLEAN := $(CLEAN) nmbug
+nmbug: notmuch-git
+	ln -s $< $@
+
 .PHONY: dist
 dist: $(TAR_FILE)
 
@@ -294,7 +303,7 @@ endif
 SRCS  := $(SRCS) $(notmuch_client_srcs)
 CLEAN := $(CLEAN) notmuch notmuch-shared $(notmuch_client_modules)
 CLEAN := $(CLEAN) version.stamp notmuch-*.tar.gz.tmp
-CLEAN := $(CLEAN) .deps notmuch-git
+CLEAN := $(CLEAN) .deps
 
 DISTCLEAN := $(DISTCLEAN) .first-build-message Makefile.config sh.config sphinx.config
 
@@ -307,10 +316,6 @@ cppcheck:
 	@echo "No cppcheck found during configure; skipping static checking"
 endif
 
-nmbug notmuch-git: notmuch-git.in
-	sed s/@NOTMUCH_VERSION@/${VERSION}/ < notmuch-git.in > notmuch-git
-	chmod ugo+rx notmuch-git
-	ln -sf notmuch-git nmbug
 
 DEPS := $(SRCS:%.c=.deps/%.d)
 DEPS := $(DEPS:%.cc=.deps/%.d)
diff --git a/notmuch-git.in b/notmuch-git.py
old mode 100755
new mode 100644
similarity index 99%
rename from notmuch-git.in
rename to notmuch-git.py
index 6505c2e5..24ab3e5c
--- a/notmuch-git.in
+++ b/notmuch-git.py
@@ -40,8 +40,6 @@ from urllib.parse import quote as _quote
 from urllib.parse import unquote as _unquote
 import json as _json
 
-__version__ = '@NOTMUCH_VERSION@'
-
 _LOG = _logging.getLogger('nmbug')
 _LOG.setLevel(_logging.WARNING)
 _LOG.addHandler(_logging.StreamHandler())
@@ -376,6 +374,10 @@ def check_safe_fraction(status):
         safe=float(conf)
 
     total = count_messages (TAG_PREFIX)
+    if total == 0:
+        _LOG.error('No existing tags with given prefix, stopping.'.format(safe))
+        _LOG.error('Use --force to override.')
+        exit(1)
     change = len(status['added'])+len(status['deleted'])+len(status['missing'])
     fraction = change/total
     _LOG.debug('total messages {:d}, change: {:d}, fraction: {:f}'.format(total,change,fraction))
@@ -931,9 +933,6 @@ if __name__ == '__main__':
     parser.add_argument(
         '-N', '--nmbug', action='store_true',
         help='Set defaults for --tag-prefix and --git-dir for the notmuch bug tracker')
-    parser.add_argument(
-        '-v', '--version', action='version',
-        version='%(prog)s {}'.format(__version__))
     parser.add_argument(
         '-l', '--log-level',
         choices=['critical', 'error', 'warning', 'info', 'debug'],
diff --git a/notmuch.c b/notmuch.c
index a6c49bcb..c75b0188 100644
--- a/notmuch.c
+++ b/notmuch.c
@@ -201,8 +201,6 @@ static const command_t commands[] = {
     { "emacs-mua", NULL, 0,
       "send mail with notmuch and emacs." },
 #endif
-    { "git", NULL, 0,
-      "manage notmuch tags with git" },
     { "help", notmuch_help_command, NOTMUCH_COMMAND_CONFIG_CREATE, /* create but don't save config */
       "This message, or more detailed help for the named command." }
 };
diff --git a/test/T850-git.sh b/test/T850-git.sh
index 508615e1..7ea50939 100755
--- a/test/T850-git.sh
+++ b/test/T850-git.sh
@@ -7,6 +7,9 @@ if [ $NOTMUCH_HAVE_SFSEXP -ne 1 ]; then
     test_done
 fi
 
+# be very careful using backup_database / restore_database in this
+# file, as they fool the cache invalidation checks in notmuch-git.
+
 add_email_corpus
 
 git config --global user.email notmuch@example.org
@@ -28,6 +31,35 @@ test_expect_success "notmuch git -p '' -C tags.git clone remote.git"
 test_begin_subtest "initial commit needs force"
 test_expect_code 1 "notmuch git -C tags.git commit"
 
+test_begin_subtest "committing new prefix requires force"
+notmuch git -C force-prefix.git init
+notmuch tag +new-prefix::foo id:20091117190054.GU3165@dottiness.seas.harvard.edu
+test_expect_code 1 "notmuch git -l debug -p 'new-prefix::' -C force-prefix.git commit"
+notmuch tag -new-prefix::foo id:20091117190054.GU3165@dottiness.seas.harvard.edu
+
+test_begin_subtest "committing new prefix works with force"
+notmuch tag +new-prefix::foo id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch git -l debug -p 'new-prefix::' -C force-prefix.git commit --force
+git -C force-prefix.git ls-tree -r --name-only HEAD | xargs dirname | sort -u | sed s,tags/,id:, > OUTPUT
+notmuch tag -new-prefix::foo id:20091117190054.GU3165@dottiness.seas.harvard.edu
+cat <<EOF>EXPECTED
+id:20091117190054.GU3165@dottiness.seas.harvard.edu
+EOF
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_begin_subtest "checkout new prefix requires force"
+test_expect_code 1 "notmuch git -l debug -p 'new-prefix::' -C force-prefix.git checkout"
+
+test_begin_subtest "checkout new prefix works with force"
+notmuch dump > BEFORE
+notmuch git -l debug -p 'new-prefix::' -C force-prefix.git checkout --force
+notmuch dump --include=tags id:20091117190054.GU3165@dottiness.seas.harvard.edu | grep -v '^#' > OUTPUT
+notmuch restore < BEFORE
+cat <<EOF > EXPECTED
++inbox +new-prefix%3a%3afoo +signed +unread -- id:20091117190054.GU3165@dottiness.seas.harvard.edu
+EOF
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
 test_begin_subtest "commit"
 notmuch git -C tags.git commit --force
 git -C tags.git ls-tree -r --name-only HEAD | xargs dirname | sort -u | sed s,tags/,id:, > OUTPUT
@@ -138,6 +170,17 @@ EOF
 notmuch git -C tags.git checkout
 test_expect_equal_file EXPECTED OUTPUT
 
+test_begin_subtest "status (global config argument)"
+cp notmuch-config notmuch-config.new
+notmuch --config=notmuch-config.new config set git.path tags.git
+notmuch tag +test id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch --config=./notmuch-config.new git status > OUTPUT
+cat <<EOF > EXPECTED
+A	20091117190054.GU3165@dottiness.seas.harvard.edu	test
+EOF
+notmuch --config=notmuch-config.new git checkout
+test_expect_equal_file EXPECTED OUTPUT
+
 test_begin_subtest "fetch"
 notmuch tag +test2 id:20091117190054.GU3165@dottiness.seas.harvard.edu
 notmuch git -C remote.git commit --force

^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v3 01/17] nmbug: promote to user tool "notmuch-git"
  2022-06-04 17:22 David Bremner
@ 2022-06-04 17:22 ` David Bremner
  2022-06-04 17:22 ` [PATCH v3 02/17] CLI/git: drop support for python < 3.2 David Bremner
                   ` (16 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: David Bremner @ 2022-06-04 17:22 UTC (permalink / raw)
  To: notmuch

Initially just a rename, and drop the --version argument that clashes
with the global notmuch --version argument.
---
 Makefile.local                      | 7 ++++++-
 devel/nmbug/nmbug => notmuch-git.py | 6 ------
 2 files changed, 6 insertions(+), 7 deletions(-)
 rename devel/nmbug/nmbug => notmuch-git.py (99%)
 mode change 100755 => 100644

diff --git a/Makefile.local b/Makefile.local
index d8bbf3e1..d2e0c7a8 100644
--- a/Makefile.local
+++ b/Makefile.local
@@ -1,7 +1,7 @@
 # -*- makefile-gmake -*-
 
 .PHONY: all
-all: notmuch notmuch-shared build-man build-info ruby-bindings python-cffi-bindings
+all: notmuch notmuch-shared build-man build-info ruby-bindings python-cffi-bindings notmuch-git
 ifeq ($(MAKECMDGOALS),)
 ifeq ($(shell cat .first-build-message 2>/dev/null),)
 	@NOTMUCH_FIRST_BUILD=1 $(MAKE) --no-print-directory all
@@ -45,6 +45,11 @@ $(SHA256_FILE): $(TAR_FILE)
 $(DETACHED_SIG_FILE): $(TAR_FILE)
 	gpg --armor --detach-sign $^
 
+CLEAN := $(CLEAN) notmuch-git
+notmuch-git: notmuch-git.py
+	cp $< $@
+	chmod ugo+x $@
+
 .PHONY: dist
 dist: $(TAR_FILE)
 
diff --git a/devel/nmbug/nmbug b/notmuch-git.py
old mode 100755
new mode 100644
similarity index 99%
rename from devel/nmbug/nmbug
rename to notmuch-git.py
index 043c1863..04cdae0f
--- a/devel/nmbug/nmbug
+++ b/notmuch-git.py
@@ -50,9 +50,6 @@ except ImportError:  # Python 2
     from urllib import quote as _quote
     from urllib import unquote as _unquote
 
-
-__version__ = '0.3'
-
 _LOG = _logging.getLogger('nmbug')
 _LOG.setLevel(_logging.WARNING)
 _LOG.addHandler(_logging.StreamHandler())
@@ -718,9 +715,6 @@ if __name__ == '__main__':
     parser = argparse.ArgumentParser(
         description=__doc__.strip(),
         formatter_class=argparse.RawDescriptionHelpFormatter)
-    parser.add_argument(
-        '-v', '--version', action='version',
-        version='%(prog)s {}'.format(__version__))
     parser.add_argument(
         '-l', '--log-level',
         choices=['critical', 'error', 'warning', 'info', 'debug'],
-- 
2.35.2

^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v3 02/17] CLI/git: drop support for python < 3.2
  2022-06-04 17:22 David Bremner
  2022-06-04 17:22 ` [PATCH v3 01/17] nmbug: promote to user tool "notmuch-git" David Bremner
@ 2022-06-04 17:22 ` David Bremner
  2022-06-04 17:22 ` [PATCH v3 03/17] notmuch-git: add --git-dir, --tag-prefix arguments David Bremner
                   ` (15 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: David Bremner @ 2022-06-04 17:22 UTC (permalink / raw)
  To: notmuch

Debian stable had python 3.4.2 3 releases ago (approximately 6 years
ago), so attempting to keep track of the changes in python is probably
no longer worthwhile. We already require python 3.5 for the
python-cffi bindings (although those are not yet used in notmuch-git).
---
 notmuch-git.py | 33 ++-------------------------------
 1 file changed, 2 insertions(+), 31 deletions(-)

diff --git a/notmuch-git.py b/notmuch-git.py
index 04cdae0f..fb42e5bf 100644
--- a/notmuch-git.py
+++ b/notmuch-git.py
@@ -43,12 +43,8 @@ import subprocess as _subprocess
 import sys as _sys
 import tempfile as _tempfile
 import textwrap as _textwrap
-try:  # Python 3
-    from urllib.parse import quote as _quote
-    from urllib.parse import unquote as _unquote
-except ImportError:  # Python 2
-    from urllib import quote as _quote
-    from urllib import unquote as _unquote
+from urllib.parse import quote as _quote
+from urllib.parse import unquote as _unquote
 
 _LOG = _logging.getLogger('nmbug')
 _LOG.setLevel(_logging.WARNING)
@@ -68,31 +64,6 @@ _TAG_FILE_REGEX = _re.compile(_TAG_DIRECTORY + '(?P<id>[^/]*)/(?P<tag>[^/]*)')
 # magic hash for Git (git hash-object -t blob /dev/null)
 _EMPTYBLOB = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'
 
-
-try:
-    getattr(_tempfile, 'TemporaryDirectory')
-except AttributeError:  # Python < 3.2
-    class _TemporaryDirectory(object):
-        """
-        Fallback context manager for Python < 3.2
-
-        See PEP 343 for details on context managers [1].
-
-        [1]: https://www.python.org/dev/peps/pep-0343/
-        """
-        def __init__(self, **kwargs):
-            self.name = _tempfile.mkdtemp(**kwargs)
-
-        def __enter__(self):
-            return self.name
-
-        def __exit__(self, type, value, traceback):
-            _shutil.rmtree(self.name)
-
-
-    _tempfile.TemporaryDirectory = _TemporaryDirectory
-
-
 def _hex_quote(string, safe='+@=:,'):
     """
     quote('abc def') -> 'abc%20def'.
-- 
2.35.2

^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v3 03/17] notmuch-git: add --git-dir, --tag-prefix arguments
  2022-06-04 17:22 David Bremner
  2022-06-04 17:22 ` [PATCH v3 01/17] nmbug: promote to user tool "notmuch-git" David Bremner
  2022-06-04 17:22 ` [PATCH v3 02/17] CLI/git: drop support for python < 3.2 David Bremner
@ 2022-06-04 17:22 ` David Bremner
  2022-06-04 17:23 ` [PATCH v3 04/17] CLI/git: make existance of config branch optional on clone David Bremner
                   ` (14 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: David Bremner @ 2022-06-04 17:22 UTC (permalink / raw)
  To: notmuch

It is often more convenient to use command line arguments than
environment variables.
---
 notmuch-git.py | 31 +++++++++++++++++++++----------
 1 file changed, 21 insertions(+), 10 deletions(-)

diff --git a/notmuch-git.py b/notmuch-git.py
index fb42e5bf..34d07125 100644
--- a/notmuch-git.py
+++ b/notmuch-git.py
@@ -50,13 +50,9 @@ _LOG = _logging.getLogger('nmbug')
 _LOG.setLevel(_logging.WARNING)
 _LOG.addHandler(_logging.StreamHandler())
 
-NMBGIT = _os.path.expanduser(
-    _os.getenv('NMBGIT', _os.path.join('~', '.nmbug')))
-_NMBGIT = _os.path.join(NMBGIT, '.git')
-if _os.path.isdir(_NMBGIT):
-    NMBGIT = _NMBGIT
+NMBGIT = None
+TAG_PREFIX = None
 
-TAG_PREFIX = _os.getenv('NMBPREFIX', 'notmuch::')
 _HEX_ESCAPE_REGEX = _re.compile('%[0-9A-F]{2}')
 _TAG_DIRECTORY = 'tags/'
 _TAG_FILE_REGEX = _re.compile(_TAG_DIRECTORY + '(?P<id>[^/]*)/(?P<tag>[^/]*)')
@@ -77,10 +73,6 @@ def _hex_quote(string, safe='+@=:,'):
         lambda match: match.group(0).lower(),
         uppercase_escapes)
 
-
-_ENCODED_TAG_PREFIX = _hex_quote(TAG_PREFIX, safe='+@=,')  # quote ':'
-
-
 def _xapian_quote(string):
     """
     Quote a string for Xapian's QueryParser.
@@ -686,6 +678,13 @@ if __name__ == '__main__':
     parser = argparse.ArgumentParser(
         description=__doc__.strip(),
         formatter_class=argparse.RawDescriptionHelpFormatter)
+    parser.add_argument(
+        '-C', '--git-dir', metavar='REPO',
+        help='Git repository to operate on.')
+    parser.add_argument(
+        '-p', '--tag-prefix', metavar='PREFIX',
+        default = _os.getenv('NMBPREFIX', 'notmuch::'),
+        help='Prefix of tags to operate on.')
     parser.add_argument(
         '-l', '--log-level',
         choices=['critical', 'error', 'warning', 'info', 'debug'],
@@ -795,6 +794,18 @@ if __name__ == '__main__':
 
     args = parser.parse_args()
 
+    if args.git_dir:
+        NMBGIT = args.git_dir
+    else:
+        NMBGIT = _os.path.expanduser(
+        _os.getenv('NMBGIT', _os.path.join('~', '.nmbug')))
+        _NMBGIT = _os.path.join(NMBGIT, '.git')
+        if _os.path.isdir(_NMBGIT):
+            NMBGIT = _NMBGIT
+
+    TAG_PREFIX = args.tag_prefix
+    _ENCODED_TAG_PREFIX = _hex_quote(TAG_PREFIX, safe='+@=,')  # quote ':'
+
     if args.log_level:
         level = getattr(_logging, args.log_level.upper())
         _LOG.setLevel(level)
-- 
2.35.2

^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v3 04/17] CLI/git: make existance of config branch optional on clone
  2022-06-04 17:22 David Bremner
                   ` (2 preceding siblings ...)
  2022-06-04 17:22 ` [PATCH v3 03/17] notmuch-git: add --git-dir, --tag-prefix arguments David Bremner
@ 2022-06-04 17:23 ` David Bremner
  2022-06-04 17:23 ` [PATCH v3 05/17] CLI/git: Add an 'init' command David Bremner
                   ` (13 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: David Bremner @ 2022-06-04 17:23 UTC (permalink / raw)
  To: notmuch

This branch is actually only used by an associated
utility (notmuch-report), and notmuch-git works fine without it.
---
 notmuch-git.py | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/notmuch-git.py b/notmuch-git.py
index 34d07125..0cbfec61 100644
--- a/notmuch-git.py
+++ b/notmuch-git.py
@@ -271,7 +271,13 @@ def clone(repository):
             wait=True)
     _git(args=['config', '--unset', 'core.worktree'], wait=True, expect=(0, 5))
     _git(args=['config', 'core.bare', 'true'], wait=True)
-    _git(args=['branch', 'config', 'origin/config'], wait=True)
+    (status, stdout, stderr) = _git(args=['show-ref', '--verify',
+                                          '--quiet',
+                                          'refs/remotes/origin/config'],
+                                    expect=(0,1),
+                                    wait=True)
+    if status == 0:
+        _git(args=['branch', 'config', 'origin/config'], wait=True)
     existing_tags = get_tags()
     if existing_tags:
         _LOG.warning(
-- 
2.35.2

^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v3 05/17] CLI/git: Add an 'init' command
  2022-06-04 17:22 David Bremner
                   ` (3 preceding siblings ...)
  2022-06-04 17:23 ` [PATCH v3 04/17] CLI/git: make existance of config branch optional on clone David Bremner
@ 2022-06-04 17:23 ` David Bremner
  2022-06-04 17:23 ` [PATCH v3 06/17] test: initial tests for notmuch-git David Bremner
                   ` (12 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: David Bremner @ 2022-06-04 17:23 UTC (permalink / raw)
  To: notmuch; +Cc: W. Trevor King

From: "W. Trevor King" <wking@tremily.us>

For folks that want to start versioning a new tag-space, instead of
cloning one that someone else has already started.

The empty-blob hash-object call avoids errors like:

  $ nmbug commit
  error: invalid object 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 for
'tags/...'
  fatal: git-write-tree: error building trees
  'git HASH(0x9ef3eb8) write-tree' exited with nonzero value

David Bremner suggested [1]:

  $ git hash-object -w /dev/null

instead of my Python version of:

  $ git hash-object -w --stdin <&-

but I expect that closing stdin is more portable than the /dev/null
path (which doesn't exist on Windows, for example).

The --bare init and use of NMBGIT as the work tree (what could go
wrong with an empty commit?) are suggestions from Michal Sojka [2].

[1]: id:87y4vu6uvf.fsf@maritornes.cs.unb.ca
     http://thread.gmane.org/gmane.mail.notmuch.general/18626/focus=18720
[2]: id:87a93a5or2.fsf@resox.2x.cz
     http://thread.gmane.org/gmane.mail.notmuch.general/19495/focus=19767
---
 notmuch-git.py | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/notmuch-git.py b/notmuch-git.py
index 0cbfec61..ce3c07bb 100644
--- a/notmuch-git.py
+++ b/notmuch-git.py
@@ -351,6 +351,25 @@ def fetch(remote=None):
     _git(args=args, wait=True)
 
 
+def init(remote=None):
+    """
+    Create an empty nmbug repository.
+
+    This wraps 'git init' with a few extra steps to support subsequent
+    status and commit commands.
+    """
+    _spawn(args=['git', '--git-dir', NMBGIT, 'init', '--bare'], wait=True)
+    _git(args=['config', 'core.logallrefupdates', 'true'], wait=True)
+    # create an empty blob (e69de29bb2d1d6434b8b29ae775ad8c2e48c5391)
+    _git(args=['hash-object', '-w', '--stdin'], input='', wait=True)
+    _git(
+        args=[
+            'commit', '--allow-empty', '-m', 'Start a new nmbug repository'
+        ],
+        additional_env={'GIT_WORK_TREE': NMBGIT},
+        wait=True)
+
+
 def checkout():
     """
     Update the notmuch database from Git.
@@ -711,6 +730,7 @@ if __name__ == '__main__':
             'commit',
             'fetch',
             'help',
+            'init',
             'log',
             'merge',
             'pull',
-- 
2.35.2

^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v3 06/17] test: initial tests for notmuch-git
  2022-06-04 17:22 David Bremner
                   ` (4 preceding siblings ...)
  2022-06-04 17:23 ` [PATCH v3 05/17] CLI/git: Add an 'init' command David Bremner
@ 2022-06-04 17:23 ` David Bremner
  2022-06-04 17:23 ` [PATCH v3 07/17] CLI/git: rename environment variables David Bremner
                   ` (11 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: David Bremner @ 2022-06-04 17:23 UTC (permalink / raw)
  To: notmuch

Exercise the main functionality of notmuch-git.  add_git_repos() will
hopefully be simplifed when an init subcommand is added.
---
 notmuch-git.py   |  4 +++
 test/T850-git.sh | 93 ++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 97 insertions(+)
 create mode 100755 test/T850-git.sh

diff --git a/notmuch-git.py b/notmuch-git.py
index ce3c07bb..5475d0db 100644
--- a/notmuch-git.py
+++ b/notmuch-git.py
@@ -836,6 +836,10 @@ if __name__ == '__main__':
         level = getattr(_logging, args.log_level.upper())
         _LOG.setLevel(level)
 
+    # for test suite
+    for var in ['NMBGIT', 'NMBPREFIX', 'NOTMUCH_PROFILE', 'NOTMUCH_CONFIG' ]:
+        _LOG.debug('env {:s} = {:s}'.format(var, _os.getenv(var,'%None%')))
+
     if not getattr(args, 'func', None):
         parser.print_usage()
         _sys.exit(1)
diff --git a/test/T850-git.sh b/test/T850-git.sh
new file mode 100755
index 00000000..713b326f
--- /dev/null
+++ b/test/T850-git.sh
@@ -0,0 +1,93 @@
+#!/usr/bin/env bash
+test_description='"notmuch git" to save and restore tags'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+add_email_corpus
+
+git config --global user.email notmuch@example.org
+git config --global user.name  "Notmuch Test Suite"
+
+test_begin_subtest "init"
+test_expect_success "notmuch git -p '' -C remote.git init"
+
+test_begin_subtest "clone"
+test_expect_success "notmuch git -p '' -C tags.git clone remote.git"
+
+test_begin_subtest "commit"
+notmuch git -C tags.git -p '' commit
+git -C tags.git ls-tree -r --name-only HEAD | xargs dirname | sort -u | sed s,tags/,id:, > OUTPUT
+notmuch search --output=messages '*' | sort > EXPECTED
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_begin_subtest "checkout"
+notmuch dump > BEFORE
+notmuch tag -inbox '*'
+notmuch git -C tags.git -p '' checkout
+notmuch dump > AFTER
+test_expect_equal_file_nonempty BEFORE AFTER
+
+test_begin_subtest "archive"
+notmuch git -C tags.git -p '' archive | tar tf - | \
+    grep 20091117190054.GU3165@dottiness.seas.harvard.edu | sort > OUTPUT
+cat <<EOF > EXPECTED
+tags/20091117190054.GU3165@dottiness.seas.harvard.edu/
+tags/20091117190054.GU3165@dottiness.seas.harvard.edu/inbox
+tags/20091117190054.GU3165@dottiness.seas.harvard.edu/signed
+tags/20091117190054.GU3165@dottiness.seas.harvard.edu/unread
+EOF
+notmuch git -C tags.git -p '' checkout
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "status"
+notmuch tag +test id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch git -C tags.git -p '' status > OUTPUT
+cat <<EOF > EXPECTED
+A	20091117190054.GU3165@dottiness.seas.harvard.edu	test
+EOF
+notmuch git -C tags.git -p '' checkout
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "fetch"
+notmuch tag +test2 id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch git -C remote.git -p '' commit
+notmuch tag -test2 id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch git -C tags.git -p '' fetch
+notmuch git -C tags.git -p '' status > OUTPUT
+cat <<EOF > EXPECTED
+ a	20091117190054.GU3165@dottiness.seas.harvard.edu	test2
+EOF
+notmuch git -C tags.git -p '' checkout
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "merge"
+notmuch git -C tags.git -p '' merge
+notmuch dump id:20091117190054.GU3165@dottiness.seas.harvard.edu | grep -v '^#' > OUTPUT
+cat <<EOF > EXPECTED
++inbox +signed +test2 +unread -- id:20091117190054.GU3165@dottiness.seas.harvard.edu
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "push"
+notmuch tag +test3 id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch git -C tags.git -p '' commit
+notmuch tag -test3 id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch git -C tags.git -p '' push
+notmuch git -C remote.git -p '' checkout
+notmuch dump id:20091117190054.GU3165@dottiness.seas.harvard.edu | grep -v '^#' > OUTPUT
+cat <<EOF > EXPECTED
++inbox +signed +test2 +test3 +unread -- id:20091117190054.GU3165@dottiness.seas.harvard.edu
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "environment passed through when run as 'notmuch git'"
+env NMBGIT=foo NMBPREFIX=bar NOTMUCH_PROFILE=default notmuch git -C tags.git -p '' -ldebug status |& \
+    grep '^env ' | notmuch_dir_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+env NMBGIT = foo
+env NMBPREFIX = bar
+env NOTMUCH_PROFILE = default
+env NOTMUCH_CONFIG = CWD/notmuch-config
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
-- 
2.35.2

^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v3 07/17] CLI/git: rename environment variables.
  2022-06-04 17:22 David Bremner
                   ` (5 preceding siblings ...)
  2022-06-04 17:23 ` [PATCH v3 06/17] test: initial tests for notmuch-git David Bremner
@ 2022-06-04 17:23 ` David Bremner
  2022-06-04 17:23 ` [PATCH v3 08/17] CLI/git: suppress warnings about initial branch name David Bremner
                   ` (10 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: David Bremner @ 2022-06-04 17:23 UTC (permalink / raw)
  To: notmuch

Although the code required to support both new and old environment
variables is small, it complicates the semantics of configuration, and
make the documentation harder to follow.
---
 notmuch-git.py   | 35 ++++++++++++++---------------------
 test/T850-git.sh |  6 +++---
 2 files changed, 17 insertions(+), 24 deletions(-)

diff --git a/notmuch-git.py b/notmuch-git.py
index 5475d0db..f3ad6927 100644
--- a/notmuch-git.py
+++ b/notmuch-git.py
@@ -18,13 +18,6 @@
 
 """
 Manage notmuch tags with Git
-
-Environment variables:
-
-* NMBGIT specifies the location of the git repository used by nmbug.
-  If not specified $HOME/.nmbug is used.
-* NMBPREFIX specifies the prefix in the notmuch database for tags of
-  interest to nmbug. If not specified 'notmuch::' is used.
 """
 
 from __future__ import print_function
@@ -50,7 +43,7 @@ _LOG = _logging.getLogger('nmbug')
 _LOG.setLevel(_logging.WARNING)
 _LOG.addHandler(_logging.StreamHandler())
 
-NMBGIT = None
+NOTMUCH_GIT_DIR = None
 TAG_PREFIX = None
 
 _HEX_ESCAPE_REGEX = _re.compile('%[0-9A-F]{2}')
@@ -198,7 +191,7 @@ def _spawn(args, input=None, additional_env=None, wait=False, stdin=None,
 
 
 def _git(args, **kwargs):
-    args = ['git', '--git-dir', NMBGIT] + list(args)
+    args = ['git', '--git-dir', NOTMUCH_GIT_DIR] + list(args)
     return _spawn(args=args, **kwargs)
 
 
@@ -266,7 +259,7 @@ def clone(repository):
     with _tempfile.TemporaryDirectory(prefix='nmbug-clone.') as workdir:
         _spawn(
             args=[
-                'git', 'clone', '--no-checkout', '--separate-git-dir', NMBGIT,
+                'git', 'clone', '--no-checkout', '--separate-git-dir', NOTMUCH_GIT_DIR,
                 repository, workdir],
             wait=True)
     _git(args=['config', '--unset', 'core.worktree'], wait=True, expect=(0, 5))
@@ -358,7 +351,7 @@ def init(remote=None):
     This wraps 'git init' with a few extra steps to support subsequent
     status and commit commands.
     """
-    _spawn(args=['git', '--git-dir', NMBGIT, 'init', '--bare'], wait=True)
+    _spawn(args=['git', '--git-dir', NOTMUCH_GIT_DIR, 'init', '--bare'], wait=True)
     _git(args=['config', 'core.logallrefupdates', 'true'], wait=True)
     # create an empty blob (e69de29bb2d1d6434b8b29ae775ad8c2e48c5391)
     _git(args=['hash-object', '-w', '--stdin'], input='', wait=True)
@@ -366,7 +359,7 @@ def init(remote=None):
         args=[
             'commit', '--allow-empty', '-m', 'Start a new nmbug repository'
         ],
-        additional_env={'GIT_WORK_TREE': NMBGIT},
+        additional_env={'GIT_WORK_TREE': NOTMUCH_GIT_DIR},
         wait=True)
 
 
@@ -589,7 +582,7 @@ def get_status():
 
 def _index_tags():
     "Write notmuch tags to the nmbug.index."
-    path = _os.path.join(NMBGIT, 'nmbug.index')
+    path = _os.path.join(NOTMUCH_GIT_DIR, 'nmbug.index')
     query = ' '.join('tag:"{tag}"'.format(tag=tag) for tag in get_tags())
     prefix = '+{0}'.format(_ENCODED_TAG_PREFIX)
     _git(
@@ -708,7 +701,7 @@ if __name__ == '__main__':
         help='Git repository to operate on.')
     parser.add_argument(
         '-p', '--tag-prefix', metavar='PREFIX',
-        default = _os.getenv('NMBPREFIX', 'notmuch::'),
+        default = _os.getenv('NOTMUCH_GIT_PREFIX', 'notmuch::'),
         help='Prefix of tags to operate on.')
     parser.add_argument(
         '-l', '--log-level',
@@ -821,13 +814,13 @@ if __name__ == '__main__':
     args = parser.parse_args()
 
     if args.git_dir:
-        NMBGIT = args.git_dir
+        NOTMUCH_GIT_DIR = args.git_dir
     else:
-        NMBGIT = _os.path.expanduser(
-        _os.getenv('NMBGIT', _os.path.join('~', '.nmbug')))
-        _NMBGIT = _os.path.join(NMBGIT, '.git')
-        if _os.path.isdir(_NMBGIT):
-            NMBGIT = _NMBGIT
+        NOTMUCH_GIT_DIR = _os.path.expanduser(
+        _os.getenv('NOTMUCH_GIT_DIR', _os.path.join('~', '.nmbug')))
+        _NOTMUCH_GIT_DIR = _os.path.join(NOTMUCH_GIT_DIR, '.git')
+        if _os.path.isdir(_NOTMUCH_GIT_DIR):
+            NOTMUCH_GIT_DIR = _NOTMUCH_GIT_DIR
 
     TAG_PREFIX = args.tag_prefix
     _ENCODED_TAG_PREFIX = _hex_quote(TAG_PREFIX, safe='+@=,')  # quote ':'
@@ -837,7 +830,7 @@ if __name__ == '__main__':
         _LOG.setLevel(level)
 
     # for test suite
-    for var in ['NMBGIT', 'NMBPREFIX', 'NOTMUCH_PROFILE', 'NOTMUCH_CONFIG' ]:
+    for var in ['NOTMUCH_GIT_DIR', 'NOTMUCH_GIT_PREFIX', 'NOTMUCH_PROFILE', 'NOTMUCH_CONFIG' ]:
         _LOG.debug('env {:s} = {:s}'.format(var, _os.getenv(var,'%None%')))
 
     if not getattr(args, 'func', None):
diff --git a/test/T850-git.sh b/test/T850-git.sh
index 713b326f..994950ed 100755
--- a/test/T850-git.sh
+++ b/test/T850-git.sh
@@ -80,11 +80,11 @@ EOF
 test_expect_equal_file EXPECTED OUTPUT
 
 test_begin_subtest "environment passed through when run as 'notmuch git'"
-env NMBGIT=foo NMBPREFIX=bar NOTMUCH_PROFILE=default notmuch git -C tags.git -p '' -ldebug status |& \
+env NOTMUCH_GIT_DIR=foo NOTMUCH_GIT_PREFIX=bar NOTMUCH_PROFILE=default notmuch git -C tags.git -p '' -ldebug status |& \
     grep '^env ' | notmuch_dir_sanitize > OUTPUT
 cat <<EOF > EXPECTED
-env NMBGIT = foo
-env NMBPREFIX = bar
+env NOTMUCH_GIT_DIR = foo
+env NOTMUCH_GIT_PREFIX = bar
 env NOTMUCH_PROFILE = default
 env NOTMUCH_CONFIG = CWD/notmuch-config
 EOF
-- 
2.35.2

^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v3 08/17] CLI/git: suppress warnings about initial branch name
  2022-06-04 17:22 David Bremner
                   ` (6 preceding siblings ...)
  2022-06-04 17:23 ` [PATCH v3 07/17] CLI/git: rename environment variables David Bremner
@ 2022-06-04 17:23 ` David Bremner
  2022-06-04 17:23 ` [PATCH v3 09/17] test/git: add known broken test for tag with quotes David Bremner
                   ` (9 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: David Bremner @ 2022-06-04 17:23 UTC (permalink / raw)
  To: notmuch

The canonical nmbug repository still uses "master" as the main branch
name, so defer any potential switch away from that name.
---
 notmuch-git.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/notmuch-git.py b/notmuch-git.py
index f3ad6927..543ef4f1 100644
--- a/notmuch-git.py
+++ b/notmuch-git.py
@@ -351,7 +351,8 @@ def init(remote=None):
     This wraps 'git init' with a few extra steps to support subsequent
     status and commit commands.
     """
-    _spawn(args=['git', '--git-dir', NOTMUCH_GIT_DIR, 'init', '--bare'], wait=True)
+    _spawn(args=['git', '--git-dir', NOTMUCH_GIT_DIR, 'init',
+                 '--initial-branch=master', '--quiet', '--bare'], wait=True)
     _git(args=['config', 'core.logallrefupdates', 'true'], wait=True)
     # create an empty blob (e69de29bb2d1d6434b8b29ae775ad8c2e48c5391)
     _git(args=['hash-object', '-w', '--stdin'], input='', wait=True)
-- 
2.35.2

^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v3 09/17] test/git: add known broken test for tag with quotes.
  2022-06-04 17:22 David Bremner
                   ` (7 preceding siblings ...)
  2022-06-04 17:23 ` [PATCH v3 08/17] CLI/git: suppress warnings about initial branch name David Bremner
@ 2022-06-04 17:23 ` David Bremner
  2022-06-04 17:23 ` [PATCH v3 10/17] CLI/git: replace enumeration of tags with sexp query David Bremner
                   ` (8 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: David Bremner @ 2022-06-04 17:23 UTC (permalink / raw)
  To: notmuch

There is current insufficient sanitization and/or escaping of tag names
internally in notmuch-git.
---
 test/T850-git.sh | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/test/T850-git.sh b/test/T850-git.sh
index 994950ed..2badc52d 100755
--- a/test/T850-git.sh
+++ b/test/T850-git.sh
@@ -19,6 +19,16 @@ git -C tags.git ls-tree -r --name-only HEAD | xargs dirname | sort -u | sed s,ta
 notmuch search --output=messages '*' | sort > EXPECTED
 test_expect_equal_file_nonempty EXPECTED OUTPUT
 
+test_begin_subtest "commit, with quoted tag"
+test_subtest_known_broken
+notmuch git -C clone2.git -p '' clone tags.git
+git -C clone2.git ls-tree -r --name-only HEAD | grep /inbox > BEFORE
+notmuch tag '+"quoted tag"' '*'
+notmuch git -C clone2.git -p '' commit
+notmuch tag '-"quoted tag"' '*'
+git -C clone2.git ls-tree -r --name-only HEAD | grep /inbox > AFTER
+test_expect_equal_file_nonempty BEFORE AFTER
+
 test_begin_subtest "checkout"
 notmuch dump > BEFORE
 notmuch tag -inbox '*'
-- 
2.35.2

^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v3 10/17] CLI/git: replace enumeration of tags with sexp query.
  2022-06-04 17:22 David Bremner
                   ` (8 preceding siblings ...)
  2022-06-04 17:23 ` [PATCH v3 09/17] test/git: add known broken test for tag with quotes David Bremner
@ 2022-06-04 17:23 ` David Bremner
  2022-06-04 17:23 ` [PATCH v3 11/17] CLI/git: add @timed decorator, time a few functions David Bremner
                   ` (7 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: David Bremner @ 2022-06-04 17:23 UTC (permalink / raw)
  To: notmuch

Unlike the (current) infix query parser provided by Xapian, the
notmuch specific sexp query parser supports prefixed wildcard queries,
so use those. In addition to being somewhat faster, this avoids
needing to escape all of the user's tags to pass via the shell.
---
 notmuch-git.py   | 26 +++++++++++++++++++-------
 test/T850-git.sh |  6 +++++-
 2 files changed, 24 insertions(+), 8 deletions(-)

diff --git a/notmuch-git.py b/notmuch-git.py
index 543ef4f1..badf9000 100644
--- a/notmuch-git.py
+++ b/notmuch-git.py
@@ -219,16 +219,17 @@ def _get_remote():
         stdout=_subprocess.PIPE, wait=True)
     return remote.strip()
 
+def _tag_query(prefix=None):
+    if prefix is None:
+        prefix = TAG_PREFIX
+    return '(tag (starts-with "{:s}"))'.format(prefix.replace('"','\\\"'))
 
 def get_tags(prefix=None):
     "Get a list of tags with a given prefix."
-    if prefix is None:
-        prefix = TAG_PREFIX
     (status, stdout, stderr) = _spawn(
-        args=['notmuch', 'search', '--output=tags', '*'],
+        args=['notmuch', 'search', '--query=sexp', '--output=tags', _tag_query(prefix)],
         stdout=_subprocess.PIPE, wait=True)
-    return [tag for tag in stdout.splitlines() if tag.startswith(prefix)]
-
+    return [tag for tag in stdout.splitlines()]
 
 def archive(treeish='HEAD', args=()):
     """
@@ -584,13 +585,12 @@ def get_status():
 def _index_tags():
     "Write notmuch tags to the nmbug.index."
     path = _os.path.join(NOTMUCH_GIT_DIR, 'nmbug.index')
-    query = ' '.join('tag:"{tag}"'.format(tag=tag) for tag in get_tags())
     prefix = '+{0}'.format(_ENCODED_TAG_PREFIX)
     _git(
         args=['read-tree', '--empty'],
         additional_env={'GIT_INDEX_FILE': path}, wait=True)
     with _spawn(
-            args=['notmuch', 'dump', '--format=batch-tag', '--', query],
+            args=['notmuch', 'dump', '--format=batch-tag', '--query=sexp', '--', _tag_query()],
             stdout=_subprocess.PIPE) as notmuch:
         with _git(
                 args=['update-index', '--index-info'],
@@ -690,6 +690,14 @@ def _help(parser, command=None):
     else:
         parser.parse_args(['--help'])
 
+def _notmuch_config_get(key):
+    (status, stdout, stderr) = _spawn(
+        args=['notmuch', 'config', 'get', key],
+        stdout=_subprocess.PIPE, wait=True)
+    if status != 0:
+        _LOG.error("failed to run notmuch config")
+        sys.exit(1)
+    return stdout.rstrip()
 
 if __name__ == '__main__':
     import argparse
@@ -834,6 +842,10 @@ if __name__ == '__main__':
     for var in ['NOTMUCH_GIT_DIR', 'NOTMUCH_GIT_PREFIX', 'NOTMUCH_PROFILE', 'NOTMUCH_CONFIG' ]:
         _LOG.debug('env {:s} = {:s}'.format(var, _os.getenv(var,'%None%')))
 
+    if _notmuch_config_get('built_with.sexp_queries') != 'true':
+        _LOG.error("notmuch git needs sexp query support")
+        sys.exit(1)
+
     if not getattr(args, 'func', None):
         parser.print_usage()
         _sys.exit(1)
diff --git a/test/T850-git.sh b/test/T850-git.sh
index 2badc52d..72091b56 100755
--- a/test/T850-git.sh
+++ b/test/T850-git.sh
@@ -2,6 +2,11 @@
 test_description='"notmuch git" to save and restore tags'
 . $(dirname "$0")/test-lib.sh || exit 1
 
+if [ $NOTMUCH_HAVE_SFSEXP -ne 1 ]; then
+    printf "Skipping due to missing sfsexp library\n"
+    test_done
+fi
+
 add_email_corpus
 
 git config --global user.email notmuch@example.org
@@ -20,7 +25,6 @@ notmuch search --output=messages '*' | sort > EXPECTED
 test_expect_equal_file_nonempty EXPECTED OUTPUT
 
 test_begin_subtest "commit, with quoted tag"
-test_subtest_known_broken
 notmuch git -C clone2.git -p '' clone tags.git
 git -C clone2.git ls-tree -r --name-only HEAD | grep /inbox > BEFORE
 notmuch tag '+"quoted tag"' '*'
-- 
2.35.2

^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v3 11/17] CLI/git: add @timed decorator, time a few functions
  2022-06-04 17:22 David Bremner
                   ` (9 preceding siblings ...)
  2022-06-04 17:23 ` [PATCH v3 10/17] CLI/git: replace enumeration of tags with sexp query David Bremner
@ 2022-06-04 17:23 ` David Bremner
  2022-06-04 17:23 ` [PATCH v3 12/17] CLI/git: cache git indices David Bremner
                   ` (6 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: David Bremner @ 2022-06-04 17:23 UTC (permalink / raw)
  To: notmuch

Perf will show which binaries are using the CPU cycles, and standard
python profilers will show which python functions, but neither is
great at finding which call to an external binary is taking time, or
locating I/O hotspots.
---
 notmuch-git.py | 19 ++++++++++++++++++-
 1 file changed, 18 insertions(+), 1 deletion(-)

diff --git a/notmuch-git.py b/notmuch-git.py
index badf9000..6ea50fe8 100644
--- a/notmuch-git.py
+++ b/notmuch-git.py
@@ -89,6 +89,20 @@ def _xapian_unquote(string):
     return string
 
 
+def timed(fn):
+    """Timer decorator"""
+    from time import perf_counter
+
+    def inner(*args, **kwargs):
+        start_time = perf_counter()
+        rval = fn(*args, **kwargs)
+        end_time = perf_counter()
+        _LOG.info('{0}: {1:.8f}s elapsed'.format(fn.__name__, end_time - start_time))
+        return rval
+
+    return inner
+
+
 class SubprocessError(RuntimeError):
     "A subprocess exited with a nonzero status"
     def __init__(self, args, status, stdout=None, stderr=None):
@@ -321,6 +335,7 @@ def commit(treeish='HEAD', message=None):
         _git(args=['read-tree', treeish], wait=True)
         raise
 
+@timed
 def _update_index(status):
     with _git(
             args=['update-index', '--index-info'],
@@ -561,6 +576,7 @@ def _is_unmerged(ref='@{upstream}'):
     return base != fetch_head
 
 
+@timed
 def get_status():
     status = {
         'deleted': {},
@@ -581,7 +597,7 @@ def get_status():
     _os.remove(index)
     return status
 
-
+@timed
 def _index_tags():
     "Write notmuch tags to the nmbug.index."
     path = _os.path.join(NOTMUCH_GIT_DIR, 'nmbug.index')
@@ -630,6 +646,7 @@ def _index_tags_for_message(id, status, tags):
         yield '{mode} {hash}\t{path}\n'.format(mode=mode, hash=hash, path=path)
 
 
+@timed
 def _diff_index(index, filter):
     """
     Get an {id: {tag, ...}} dict for a given filter.
-- 
2.35.2

^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v3 12/17] CLI/git: cache git indices
  2022-06-04 17:22 David Bremner
                   ` (10 preceding siblings ...)
  2022-06-04 17:23 ` [PATCH v3 11/17] CLI/git: add @timed decorator, time a few functions David Bremner
@ 2022-06-04 17:23 ` David Bremner
  2022-06-04 17:23 ` [PATCH v3 13/17] doc/notmuch-git: initial documentation David Bremner
                   ` (5 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: David Bremner @ 2022-06-04 17:23 UTC (permalink / raw)
  To: notmuch

If the private index file matches a previously known revision of the
database, we can update the index incrementally using the recorded
lastmod counter. This is typically much faster than a full update,
although it could be slower in the case of large changes to the
database.

The "git-read-tree HEAD" is also a bottleneck, but unfortunately
sometimes is needed. Cache the index checksum and hash to reduce the
number of times the operation is run. The overall design is a
simplified version of the PrivateIndex class.
---
 notmuch-git.py   | 318 ++++++++++++++++++++++++++++++++++-------------
 test/T850-git.sh |  41 ++++++
 2 files changed, 274 insertions(+), 85 deletions(-)

diff --git a/notmuch-git.py b/notmuch-git.py
index 6ea50fe8..e419cdf7 100644
--- a/notmuch-git.py
+++ b/notmuch-git.py
@@ -38,6 +38,7 @@ import tempfile as _tempfile
 import textwrap as _textwrap
 from urllib.parse import quote as _quote
 from urllib.parse import unquote as _unquote
+import json as _json
 
 _LOG = _logging.getLogger('nmbug')
 _LOG.setLevel(_logging.WARNING)
@@ -299,41 +300,98 @@ def _is_committed(status):
     return len(status['added']) + len(status['deleted']) == 0
 
 
+class CachedIndex:
+    def __init__(self, repo, treeish):
+        self.cache_path = _os.path.join(repo, 'notmuch', 'index_cache.json')
+        self.index_path = _os.path.join(repo, 'index')
+        self.current_treeish = treeish
+        # cached values
+        self.treeish = None
+        self.hash = None
+        self.index_checksum = None
+
+        self._load_cache_file()
+
+    def _load_cache_file(self):
+        try:
+            with open(self.cache_path) as f:
+                data = _json.load(f)
+                self.treeish = data['treeish']
+                self.hash = data['hash']
+                self.index_checksum = data['index_checksum']
+        except FileNotFoundError:
+            pass
+        except _json.JSONDecodeError:
+            _LOG.error("Error decoding cache")
+            _sys.exit(1)
+
+    def __enter__(self):
+        self.read_tree()
+        return self
+
+    def __exit__(self, type, value, traceback):
+        checksum = _read_index_checksum(self.index_path)
+        (_, hash, _) = _git(
+            args=['rev-parse', self.current_treeish],
+            stdout=_subprocess.PIPE,
+            wait=True)
+
+        with open(self.cache_path, "w") as f:
+            _json.dump({'treeish': self.current_treeish,
+                        'hash': hash.rstrip(),  'index_checksum': checksum }, f)
+
+    @timed
+    def read_tree(self):
+        current_checksum = _read_index_checksum(self.index_path)
+        (_, hash, _) = _git(
+            args=['rev-parse', self.current_treeish],
+            stdout=_subprocess.PIPE,
+            wait=True)
+        current_hash = hash.rstrip()
+
+        if self.current_treeish == self.treeish and \
+           self.index_checksum and self.index_checksum == current_checksum and \
+           self.hash and self.hash == current_hash:
+            return
+
+        _git(args=['read-tree', self.current_treeish], wait=True)
+
+
 def commit(treeish='HEAD', message=None):
     """
     Commit prefix-matching tags from the notmuch database to Git.
     """
+
     status = get_status()
 
     if _is_committed(status=status):
         _LOG.warning('Nothing to commit')
         return
 
-    _git(args=['read-tree', '--empty'], wait=True)
-    _git(args=['read-tree', treeish], wait=True)
-    try:
-        _update_index(status=status)
-        (_, tree, _) = _git(
-            args=['write-tree'],
-            stdout=_subprocess.PIPE,
-            wait=True)
-        (_, parent, _) = _git(
-            args=['rev-parse', treeish],
-            stdout=_subprocess.PIPE,
-            wait=True)
-        (_, commit, _) = _git(
-            args=['commit-tree', tree.strip(), '-p', parent.strip()],
-            input=message,
-            stdout=_subprocess.PIPE,
-            wait=True)
-        _git(
-            args=['update-ref', treeish, commit.strip()],
-            stdout=_subprocess.PIPE,
-            wait=True)
-    except Exception as e:
-        _git(args=['read-tree', '--empty'], wait=True)
-        _git(args=['read-tree', treeish], wait=True)
-        raise
+    with CachedIndex(NOTMUCH_GIT_DIR, treeish) as index:
+        try:
+            _update_index(status=status)
+            (_, tree, _) = _git(
+                args=['write-tree'],
+                stdout=_subprocess.PIPE,
+                wait=True)
+            (_, parent, _) = _git(
+                args=['rev-parse', treeish],
+                stdout=_subprocess.PIPE,
+                wait=True)
+            (_, commit, _) = _git(
+                args=['commit-tree', tree.strip(), '-p', parent.strip()],
+                input=message,
+                stdout=_subprocess.PIPE,
+                wait=True)
+            _git(
+                args=['update-ref', treeish, commit.strip()],
+                stdout=_subprocess.PIPE,
+                wait=True)
+        except Exception as e:
+            _git(args=['read-tree', '--empty'], wait=True)
+            _git(args=['read-tree', treeish], wait=True)
+            raise
 
 @timed
 def _update_index(status):
@@ -582,50 +640,160 @@ def get_status():
         'deleted': {},
         'missing': {},
         }
-    index = _index_tags()
-    maybe_deleted = _diff_index(index=index, filter='D')
-    for id, tags in maybe_deleted.items():
-        (_, stdout, stderr) = _spawn(
-            args=['notmuch', 'search', '--output=files', 'id:{0}'.format(id)],
-            stdout=_subprocess.PIPE,
-            wait=True)
-        if stdout:
-            status['deleted'][id] = tags
-        else:
-            status['missing'][id] = tags
-    status['added'] = _diff_index(index=index, filter='A')
-    _os.remove(index)
+    with PrivateIndex(repo=NOTMUCH_GIT_DIR, prefix=TAG_PREFIX) as index:
+        maybe_deleted = index.diff(filter='D')
+        for id, tags in maybe_deleted.items():
+            (_, stdout, stderr) = _spawn(
+                args=['notmuch', 'search', '--output=files', 'id:{0}'.format(id)],
+                stdout=_subprocess.PIPE,
+                wait=True)
+            if stdout:
+                status['deleted'][id] = tags
+            else:
+                status['missing'][id] = tags
+        status['added'] = index.diff(filter='A')
+
     return status
 
-@timed
-def _index_tags():
-    "Write notmuch tags to the nmbug.index."
-    path = _os.path.join(NOTMUCH_GIT_DIR, 'nmbug.index')
-    prefix = '+{0}'.format(_ENCODED_TAG_PREFIX)
-    _git(
-        args=['read-tree', '--empty'],
-        additional_env={'GIT_INDEX_FILE': path}, wait=True)
-    with _spawn(
-            args=['notmuch', 'dump', '--format=batch-tag', '--query=sexp', '--', _tag_query()],
-            stdout=_subprocess.PIPE) as notmuch:
+class PrivateIndex:
+    def __init__(self, repo, prefix):
+        try:
+            _os.makedirs(_os.path.join(repo, 'notmuch'))
+        except FileExistsError:
+            pass
+
+        file_name = 'notmuch/index'
+        self.index_path = _os.path.join(repo, file_name)
+        self.cache_path = _os.path.join(repo, 'notmuch', '{:s}.json'.format(_hex_quote(file_name)))
+
+        self.current_prefix = prefix
+
+        self.prefix = None
+        self.uuid = None
+        self.lastmod = None
+        self.checksum = None
+        self._load_cache_file()
+        self._index_tags()
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, traceback):
+        checksum = _read_index_checksum(self.index_path)
+        (count, uuid, lastmod) = _read_database_lastmod()
+        with open(self.cache_path, "w") as f:
+            _json.dump({'prefix': self.current_prefix, 'uuid': uuid, 'lastmod': lastmod,  'checksum': checksum }, f)
+
+    def _load_cache_file(self):
+        try:
+            with open(self.cache_path) as f:
+                data = _json.load(f)
+                self.prefix = data['prefix']
+                self.uuid = data['uuid']
+                self.lastmod = data['lastmod']
+                self.checksum = data['checksum']
+        except FileNotFoundError:
+            return None
+        except _json.JSONDecodeError:
+            _LOG.error("Error decoding cache")
+            _sys.exit(1)
+
+    @timed
+    def _index_tags(self):
+        "Write notmuch tags to private git index."
+        prefix = '+{0}'.format(_ENCODED_TAG_PREFIX)
+        current_checksum = _read_index_checksum(self.index_path)
+        if (self.prefix == None or self.prefix != self.current_prefix
+            or self.checksum == None or self.checksum != current_checksum):
+            _git(
+                args=['read-tree', '--empty'],
+                additional_env={'GIT_INDEX_FILE': self.index_path}, wait=True)
+
+        query = _tag_query()
+        clear_tags = False
+        (count,uuid,lastmod) = _read_database_lastmod()
+        if self.prefix == self.current_prefix and self.uuid \
+           and self.uuid == uuid and self.checksum == current_checksum:
+            query = '(and (infix "lastmod:{:d}..")) {:s})'.format(self.lastmod+1, query)
+            clear_tags = True
+        with _spawn(
+                args=['notmuch', 'dump', '--format=batch-tag', '--query=sexp', '--', query],
+                stdout=_subprocess.PIPE) as notmuch:
+            with _git(
+                    args=['update-index', '--index-info'],
+                    stdin=_subprocess.PIPE,
+                    additional_env={'GIT_INDEX_FILE': self.index_path}) as git:
+                for line in notmuch.stdout:
+                    if line.strip().startswith('#'):
+                        continue
+                    (tags_string, id) = [_.strip() for _ in line.split(' -- id:')]
+                    tags = [
+                        _unquote(tag[len(prefix):])
+                        for tag in tags_string.split()
+                        if tag.startswith(prefix)]
+                    id = _xapian_unquote(string=id)
+                    if clear_tags:
+                        for line in _clear_tags_for_message(index=self.index_path, id=id):
+                            git.stdin.write(line)
+                    for line in _index_tags_for_message(
+                            id=id, status='A', tags=tags):
+                        git.stdin.write(line)
+
+    @timed
+    def diff(self, filter):
+        """
+        Get an {id: {tag, ...}} dict for a given filter.
+
+        For example, use 'A' to find added tags, and 'D' to find deleted tags.
+        """
+        s = _collections.defaultdict(set)
         with _git(
-                args=['update-index', '--index-info'],
-                stdin=_subprocess.PIPE,
-                additional_env={'GIT_INDEX_FILE': path}) as git:
-            for line in notmuch.stdout:
-                if line.strip().startswith('#'):
-                    continue
-                (tags_string, id) = [_.strip() for _ in line.split(' -- id:')]
-                tags = [
-                    _unquote(tag[len(prefix):])
-                    for tag in tags_string.split()
-                    if tag.startswith(prefix)]
-                id = _xapian_unquote(string=id)
-                for line in _index_tags_for_message(
-                        id=id, status='A', tags=tags):
-                    git.stdin.write(line)
-    return path
+                args=[
+                    'diff-index', '--cached', '--diff-filter', filter,
+                    '--name-only', 'HEAD'],
+                additional_env={'GIT_INDEX_FILE': self.index_path},
+                stdout=_subprocess.PIPE) as p:
+            # Once we drop Python < 3.3, we can use 'yield from' here
+            for id, tag in _unpack_diff_lines(stream=p.stdout):
+                s[id].add(tag)
+        return s
+
+def _read_index_checksum (index_path):
+    """Read the index checksum, as defined by index-format.txt in the git source
+    WARNING: assumes SHA1 repo"""
+    import binascii
+    try:
+        with open(index_path, 'rb') as f:
+            size=_os.path.getsize(index_path)
+            f.seek(size-20);
+            return binascii.hexlify(f.read(20)).decode('ascii')
+    except FileNotFoundError:
+        return None
+
+
+def _clear_tags_for_message(index, id):
+    """
+    Clear any existing index entries for message 'id'
+
+    Neither 'id' nor the tags in 'tags' should be encoded/escaped.
+    """
 
+    dir = 'tags/{id}'.format(id=_hex_quote(string=id))
+
+    with _git(
+            args=['ls-files', dir],
+            additional_env={'GIT_INDEX_FILE': index},
+            stdout=_subprocess.PIPE) as git:
+        for file in git.stdout:
+            line = '0 0000000000000000000000000000000000000000\t{:s}\n'.format(file.strip())
+            yield line
+
+def _read_database_lastmod():
+    with _spawn(
+            args=['notmuch', 'count', '--lastmod', '*'],
+            stdout=_subprocess.PIPE) as notmuch:
+        (count,uuid,lastmod_str) = notmuch.stdout.readline().split()
+        return (count,uuid,int(lastmod_str))
 
 def _index_tags_for_message(id, status, tags):
     """
@@ -646,26 +814,6 @@ def _index_tags_for_message(id, status, tags):
         yield '{mode} {hash}\t{path}\n'.format(mode=mode, hash=hash, path=path)
 
 
-@timed
-def _diff_index(index, filter):
-    """
-    Get an {id: {tag, ...}} dict for a given filter.
-
-    For example, use 'A' to find added tags, and 'D' to find deleted tags.
-    """
-    s = _collections.defaultdict(set)
-    with _git(
-            args=[
-                'diff-index', '--cached', '--diff-filter', filter,
-                '--name-only', 'HEAD'],
-            additional_env={'GIT_INDEX_FILE': index},
-            stdout=_subprocess.PIPE) as p:
-        # Once we drop Python < 3.3, we can use 'yield from' here
-        for id, tag in _unpack_diff_lines(stream=p.stdout):
-            s[id].add(tag)
-    return s
-
-
 def _diff_refs(filter, a='HEAD', b='@{upstream}'):
     with _git(
             args=['diff', '--diff-filter', filter, '--name-only', a, b],
diff --git a/test/T850-git.sh b/test/T850-git.sh
index 72091b56..db76dae9 100755
--- a/test/T850-git.sh
+++ b/test/T850-git.sh
@@ -33,6 +33,47 @@ notmuch tag '-"quoted tag"' '*'
 git -C clone2.git ls-tree -r --name-only HEAD | grep /inbox > AFTER
 test_expect_equal_file_nonempty BEFORE AFTER
 
+test_begin_subtest "commit (incremental)"
+notmuch tag +test id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch git -C tags.git -p '' commit
+git -C tags.git ls-tree -r --name-only HEAD |
+    grep 20091117190054 | sort > OUTPUT
+echo "--------------------------------------------------" >> OUTPUT
+notmuch tag -test id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch git -C tags.git -p '' commit
+git -C tags.git ls-tree -r --name-only HEAD |
+    grep 20091117190054 | sort >> OUTPUT
+cat <<EOF > EXPECTED
+tags/20091117190054.GU3165@dottiness.seas.harvard.edu/inbox
+tags/20091117190054.GU3165@dottiness.seas.harvard.edu/signed
+tags/20091117190054.GU3165@dottiness.seas.harvard.edu/test
+tags/20091117190054.GU3165@dottiness.seas.harvard.edu/unread
+--------------------------------------------------
+tags/20091117190054.GU3165@dottiness.seas.harvard.edu/inbox
+tags/20091117190054.GU3165@dottiness.seas.harvard.edu/signed
+tags/20091117190054.GU3165@dottiness.seas.harvard.edu/unread
+EOF
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_begin_subtest "commit (change prefix)"
+notmuch tag +test::one id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch git -C tags.git -p 'test::' commit
+git -C tags.git ls-tree -r --name-only HEAD |
+    grep 20091117190054 | sort > OUTPUT
+echo "--------------------------------------------------" >> OUTPUT
+notmuch tag -test::one id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch git -C tags.git -p '' commit
+git -C tags.git ls-tree -r --name-only HEAD |
+    grep 20091117190054 | sort >> OUTPUT
+cat <<EOF > EXPECTED
+tags/20091117190054.GU3165@dottiness.seas.harvard.edu/one
+--------------------------------------------------
+tags/20091117190054.GU3165@dottiness.seas.harvard.edu/inbox
+tags/20091117190054.GU3165@dottiness.seas.harvard.edu/signed
+tags/20091117190054.GU3165@dottiness.seas.harvard.edu/unread
+EOF
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
 test_begin_subtest "checkout"
 notmuch dump > BEFORE
 notmuch tag -inbox '*'
-- 
2.35.2

^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v3 13/17] doc/notmuch-git: initial documentation
  2022-06-04 17:22 David Bremner
                   ` (11 preceding siblings ...)
  2022-06-04 17:23 ` [PATCH v3 12/17] CLI/git: cache git indices David Bremner
@ 2022-06-04 17:23 ` David Bremner
  2022-06-04 17:23 ` [PATCH v3 14/17] CLI/git: change defaults for repo and prefix David Bremner
                   ` (4 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: David Bremner @ 2022-06-04 17:23 UTC (permalink / raw)
  To: notmuch

This is mainly derived from the various help outputs from the script,
with some massaging of markup and addition of links.
---
 doc/conf.py              |   4 +
 doc/index.rst            |   1 +
 doc/man1/notmuch-git.rst | 271 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 276 insertions(+)
 create mode 100644 doc/man1/notmuch-git.rst

diff --git a/doc/conf.py b/doc/conf.py
index e4bad9f1..f01c0058 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -123,6 +123,10 @@ man_pages = [
      u'send mail with notmuch and emacs',
      [notmuch_authors], 1),
 
+    ('man1/notmuch-git', 'notmuch-git',
+     u'manage notmuch tags with git',
+     [notmuch_authors], 1),
+
     ('man5/notmuch-hooks', 'notmuch-hooks',
      u'hooks for notmuch',
      [notmuch_authors], 5),
diff --git a/doc/index.rst b/doc/index.rst
index fbdcf779..c380ee1d 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -15,6 +15,7 @@ Contents:
    man1/notmuch-dump
    notmuch-emacs
    man1/notmuch-emacs-mua
+   man1/notmuch-git
    man5/notmuch-hooks
    man1/notmuch-insert
    man1/notmuch-new
diff --git a/doc/man1/notmuch-git.rst b/doc/man1/notmuch-git.rst
new file mode 100644
index 00000000..86f246b6
--- /dev/null
+++ b/doc/man1/notmuch-git.rst
@@ -0,0 +1,271 @@
+.. _notmuch-git(1):
+
+===========
+notmuch-git
+===========
+
+SYNOPSIS
+========
+
+**notmuch** **git** [-h] [-C *repo*] [-p *prefix*] [-v] [-l *log level*] *subcommand*
+
+DESCRIPTION
+===========
+
+Manage notmuch tags with Git.
+
+OPTIONS
+-------
+
+Supported options for `notmuch git` include
+
+.. program:: notmuch-git
+
+.. option::  -h, --help
+
+   show help message and exit
+
+.. option:: -C <repo>, --git-dir <repo>
+
+   Operate on git repository *repo*. See :ref:`repo_location` for
+   defaults.
+
+.. option::  -p <prefix>, --tag-prefix <prefix>
+
+   Operate only on tags with prefix *prefix*. See :ref:`prefix_val` for
+   defaults.
+
+.. option::   -v, --version
+
+   show notmuch-git's version number and exit
+
+.. option::   -l <level>, --log-level <level>
+
+   Log verbosity, one of: `critical`, `error`, `warning`, `info`,
+   `debug`. Defaults to `warning`.
+
+SUBCOMMANDS
+-----------
+
+For help on a particular subcommand, run: 'notmuch-git ... <command> --help'.
+
+.. program:: notmuch-git
+
+.. option:: archive [tree-ish] [arg ...]
+
+Dump a tar archive of a committed tag set using 'git archive'. See
+:any:`format` for details of the archive contents.
+
+   .. describe:: tree-ish
+
+   The tree or commit to produce an archive for. Defaults to 'HEAD'.
+
+   .. describe:: arg
+
+   If present, any optional arguments are passed through to
+   :manpage:`git-archive(1)`. Arguments to `git-archive` are reordered
+   so that *tree-ish* comes last.
+
+.. option:: checkout
+
+Update the notmuch database from Git.
+
+This is mainly useful to discard your changes in notmuch relative
+to Git.
+
+.. option:: clone <repository>
+
+Create a local `notmuch git` repository from a remote source.
+
+This wraps 'git clone', adding some options to avoid creating a
+working tree while preserving remote-tracking branches and
+upstreams.
+
+    .. describe:: repository
+
+    The (possibly remote) repository to clone from. See the URLS
+    section of :manpage:`git-clone(1)` for more information on
+    specifying repositories.
+
+.. option:: commit [message]
+
+Commit prefix-matching tags from the notmuch database to Git.
+
+   .. describe:: message
+
+   Optional text for the commit message.
+
+.. option:: fetch [remote]
+
+Fetch changes from the remote repository.
+
+    .. describe:: remote
+
+    Override the default configured in `branch.<name>.remote` to fetch
+    from a particular remote repository (e.g. `origin`).
+
+.. option:: help
+
+Show brief help for an `notmuch git` command.
+
+.. option:: init
+
+Create an empty `notmuch git` repository.
+
+This wraps 'git init' with a few extra steps to support subsequent
+status and commit commands.
+
+.. option:: log [arg ...]
+
+A wrapper for 'git log'.
+
+   .. describe:: arg
+
+   Additional arguments are passed through to 'git log'.
+
+After running `notmuch git fetch`, you can inspect the changes with
+
+::
+
+   $ notmuch git log HEAD..@{upstream}
+
+.. option:: merge [reference]
+
+Merge changes from 'reference' into HEAD and load the result into notmuch.
+
+   .. describe:: reference
+
+   Reference, usually other branch heads, to merge into our
+   branch. Defaults to `@{upstream}`.
+
+.. option:: pull [repository] [refspec ...]
+
+Pull (merge) remote repository changes to notmuch.
+
+**pull** is equivalent to **fetch** followed by **merge**.  We use the
+Git-configured repository for your current branch
+(`branch.<name>.repository`, likely `origin`, and `branch.<name>.merge`,
+likely `master` or `main`).
+
+   .. describe:: repository
+
+   The "remote" repository that is the source of the pull. This parameter
+   can be either a URL (see the section GIT URLS in :manpage:`git-pull(1)`) or the
+   name of a remote (see the section REMOTES in :manpage:`git-pull(1)`).
+
+   .. describe:: refspec
+
+   Refspec (usually a branch name) to fetch and merge. See the
+   *refspec* entry in the OPTIONS section of :manpage:`git-pull(1`) for
+   other possibilities.
+
+.. option:: push [repository] [refspec]
+
+Push the local `notmuch git` Git state to a remote repository.
+
+    .. describe::  repository
+
+    The "remote" repository that is the destination of the push. This
+    parameter can be either a URL (see the section GIT URLS in
+    :manpage:`git-push(1)`) or the name of a remote (see the section
+    REMOTES in :manpage:`git-push(1)`).
+
+    .. describe:: refspec
+
+    Refspec (usually a branch name) to push. See the *refspec* entry in the OPTIONS section of
+    :manpage:`git-push(1)` for other possibilities.
+
+.. option:: status
+
+Show pending updates in notmuch or git repo.
+
+Prints lines of the form
+
+|  ng Message-Id tag
+
+where n is a single character representing notmuch database status
+
+   .. describe:: A
+
+   Tag is present in notmuch database, but not committed to nmbug
+   (equivalently, tag has been deleted in nmbug repo, e.g. by a
+   pull, but not restored to notmuch database).
+
+   .. describe:: D
+
+   Tag is present in nmbug repo, but not restored to notmuch
+   database (equivalently, tag has been deleted in notmuch).
+
+   .. describe:: U
+
+   Message is unknown (missing from local notmuch database).
+
+The second character *g* (if present) represents a difference between
+local and upstream branches. Typically `notmuch git fetch` needs to be
+run to update this.
+
+   .. describe:: a
+
+   Tag is present in upstream, but not in the local Git branch.
+
+   .. describe:: d
+
+   Tag is present in local Git branch, but not upstream.
+
+.. _format:
+
+REPOSITORY CONTENTS
+===================
+
+The tags are stored in the git repo (and exported) as a set of empty
+files. For a message with Message-Id *id*, for each tag *tag*, there
+is an empty file with path
+
+       tags/ `encode` (*id*) / `encode` (*tag*)
+
+The encoding preserves alphanumerics, and the characters `+-_@=.,:`.
+All other octets are replaced with `%` followed by a two digit hex
+number.
+
+.. _repo_location:
+
+REPOSITORY LOCATION
+===================
+
+:any:`notmuch-git` uses the first of the following with a non-empty
+value to locate the git repository.
+
+- Option :option:`--git-dir`.
+
+- Environment variable :envvar:`NOTMUCH_GIT_DIR`.
+
+.. _prefix_val:
+
+PREFIX VALUE
+============
+
+:any:`notmuch-git` uses the first of the following with a non-null
+value to define the tag prefix.
+
+- Option :option:`--tag-prefix`.
+
+- Environment variable :envvar:`NOTMUCH_GIT_PREFIX`.
+
+ENVIRONMENT
+===========
+
+.. envvar:: NOTMUCH_GIT_DIR
+
+   Default location of git repository. Overriden by :option:`--git-dir`.
+
+.. envvar:: NOTMUCH_GIT_PREFIX
+
+   Default tag prefix (filter). Overriden by :option:`--tag-prefix`.
+
+SEE ALSO
+========
+
+:any:`notmuch(1)`,
+:any:`notmuch-dump(1)`,
+:any:`notmuch-restore(1)`,
+:any:`notmuch-tag(1)`
-- 
2.35.2

^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v3 14/17] CLI/git: change defaults for repo and prefix
  2022-06-04 17:22 David Bremner
                   ` (12 preceding siblings ...)
  2022-06-04 17:23 ` [PATCH v3 13/17] doc/notmuch-git: initial documentation David Bremner
@ 2022-06-04 17:23 ` David Bremner
  2022-06-04 17:23 ` [PATCH v3 15/17] CLI/git: support configuration for repo location / prefix David Bremner
                   ` (3 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: David Bremner @ 2022-06-04 17:23 UTC (permalink / raw)
  To: notmuch

The previous defaults were not suitable for personal (i.e. not
bugtracking for notmuch development) use.

Provide two ways for the user to select nmbug compatible defaults;
command line argument and checking the name of the script.
---
 Makefile.local           |   6 ++-
 doc/Makefile.local       |   1 +
 doc/man1/notmuch-git.rst |  21 +++++++-
 notmuch-git.py           |  62 +++++++++++++++++++----
 test/T850-git.sh         | 103 +++++++++++++++++++++++++++++++--------
 5 files changed, 162 insertions(+), 31 deletions(-)

diff --git a/Makefile.local b/Makefile.local
index d2e0c7a8..7699c208 100644
--- a/Makefile.local
+++ b/Makefile.local
@@ -1,7 +1,7 @@
 # -*- makefile-gmake -*-
 
 .PHONY: all
-all: notmuch notmuch-shared build-man build-info ruby-bindings python-cffi-bindings notmuch-git
+all: notmuch notmuch-shared build-man build-info ruby-bindings python-cffi-bindings notmuch-git nmbug
 ifeq ($(MAKECMDGOALS),)
 ifeq ($(shell cat .first-build-message 2>/dev/null),)
 	@NOTMUCH_FIRST_BUILD=1 $(MAKE) --no-print-directory all
@@ -50,6 +50,10 @@ notmuch-git: notmuch-git.py
 	cp $< $@
 	chmod ugo+x $@
 
+CLEAN := $(CLEAN) nmbug
+nmbug: notmuch-git
+	ln -s $< $@
+
 .PHONY: dist
 dist: $(TAR_FILE)
 
diff --git a/doc/Makefile.local b/doc/Makefile.local
index d43ef269..2f67f4de 100644
--- a/doc/Makefile.local
+++ b/doc/Makefile.local
@@ -131,6 +131,7 @@ install-man: ${MAN_GZIP_FILES}
 	install -m0644 $(filter %.5.gz,$(MAN_GZIP_FILES)) $(DESTDIR)/$(mandir)/man5
 	install -m0644 $(filter %.7.gz,$(MAN_GZIP_FILES)) $(DESTDIR)/$(mandir)/man7
 	cd $(DESTDIR)/$(mandir)/man1 && ln -sf notmuch.1.gz notmuch-setup.1.gz
+	cd $(DESTDIR)/$(mandir)/man1 && ln -sf notmuch-git.1.gz nmbug.1.gz
 endif
 
 ifneq ($(HAVE_SPHINX)$(HAVE_MAKEINFO),11)
diff --git a/doc/man1/notmuch-git.rst b/doc/man1/notmuch-git.rst
index 86f246b6..6a2d7fcd 100644
--- a/doc/man1/notmuch-git.rst
+++ b/doc/man1/notmuch-git.rst
@@ -7,7 +7,9 @@ notmuch-git
 SYNOPSIS
 ========
 
-**notmuch** **git** [-h] [-C *repo*] [-p *prefix*] [-v] [-l *log level*] *subcommand*
+**notmuch** **git** [-h] [-N] [-C *repo*] [-p *prefix*] [-v] [-l *log level*] *subcommand*
+
+**nmbug** [-h] [-C *repo*] [-p *prefix*] [-v] [-l *log level*] *subcommand*
 
 DESCRIPTION
 ===========
@@ -25,12 +27,17 @@ Supported options for `notmuch git` include
 
    show help message and exit
 
+.. option:: -N, --nmbug
+
+   Set defaults for :option:`--tag-prefix` and :option:`--git-dir` suitable for the
+   :any:`notmuch` bug tracker
+
 .. option:: -C <repo>, --git-dir <repo>
 
    Operate on git repository *repo*. See :ref:`repo_location` for
    defaults.
 
-.. option::  -p <prefix>, --tag-prefix <prefix>
+.. option:: -p <prefix>, --tag-prefix <prefix>
 
    Operate only on tags with prefix *prefix*. See :ref:`prefix_val` for
    defaults.
@@ -239,6 +246,10 @@ value to locate the git repository.
 
 - Environment variable :envvar:`NOTMUCH_GIT_DIR`.
 
+- If invoked as `nmbug` or with the :option:`--nmbug` option,
+  :code:`$HOME/.nmbug`; otherwise
+  :code:`$XDG_DATA_HOME/notmuch/$NOTMUCH_PROFILE/git`.
+
 .. _prefix_val:
 
 PREFIX VALUE
@@ -251,9 +262,15 @@ value to define the tag prefix.
 
 - Environment variable :envvar:`NOTMUCH_GIT_PREFIX`.
 
+- If invoked as `nmbug` or with the :option:`--nmbug` option,
+  :code:`notmuch::`, otherwise the empty string.
+
 ENVIRONMENT
 ===========
 
+Variable :envvar:`NOTMUCH_PROFILE` influences :ref:`repo_location`.
+If it is unset, 'default' is assumed.
+
 .. envvar:: NOTMUCH_GIT_DIR
 
    Default location of git repository. Overriden by :option:`--git-dir`.
diff --git a/notmuch-git.py b/notmuch-git.py
index e419cdf7..9d5c7876 100644
--- a/notmuch-git.py
+++ b/notmuch-git.py
@@ -425,6 +425,13 @@ def init(remote=None):
     This wraps 'git init' with a few extra steps to support subsequent
     status and commit commands.
     """
+    from pathlib import Path
+    parent = Path(NOTMUCH_GIT_DIR).parent
+    try:
+        _os.makedirs(parent)
+    except FileExistsError:
+        pass
+
     _spawn(args=['git', '--git-dir', NOTMUCH_GIT_DIR, 'init',
                  '--initial-branch=master', '--quiet', '--bare'], wait=True)
     _git(args=['config', 'core.logallrefupdates', 'true'], wait=True)
@@ -861,9 +868,19 @@ def _notmuch_config_get(key):
         stdout=_subprocess.PIPE, wait=True)
     if status != 0:
         _LOG.error("failed to run notmuch config")
-        sys.exit(1)
+        _sys.exit(1)
     return stdout.rstrip()
 
+# based on BaseDirectory.save_data_path from pyxdg (LGPL2+)
+def xdg_data_path(profile):
+    resource = _os.path.join('notmuch',profile,'git')
+    assert not resource.startswith('/')
+    _home = _os.path.expanduser('~')
+    xdg_data_home = _os.environ.get('XDG_DATA_HOME') or \
+        _os.path.join(_home, '.local', 'share')
+    path = _os.path.join(xdg_data_home, resource)
+    return path
+
 if __name__ == '__main__':
     import argparse
 
@@ -875,8 +892,11 @@ if __name__ == '__main__':
         help='Git repository to operate on.')
     parser.add_argument(
         '-p', '--tag-prefix', metavar='PREFIX',
-        default = _os.getenv('NOTMUCH_GIT_PREFIX', 'notmuch::'),
+        default = None,
         help='Prefix of tags to operate on.')
+    parser.add_argument(
+        '-N', '--nmbug', action='store_true',
+        help='Set defaults for --tag-prefix and --git-dir for the notmuch bug tracker')
     parser.add_argument(
         '-l', '--log-level',
         choices=['critical', 'error', 'warning', 'info', 'debug'],
@@ -987,16 +1007,36 @@ if __name__ == '__main__':
 
     args = parser.parse_args()
 
+    nmbug_mode = False
+    notmuch_profile = _os.getenv('NOTMUCH_PROFILE','default')
+
+    if args.nmbug or _os.path.basename(__file__) == 'nmbug':
+        nmbug_mode = True
+
     if args.git_dir:
         NOTMUCH_GIT_DIR = args.git_dir
     else:
-        NOTMUCH_GIT_DIR = _os.path.expanduser(
-        _os.getenv('NOTMUCH_GIT_DIR', _os.path.join('~', '.nmbug')))
-        _NOTMUCH_GIT_DIR = _os.path.join(NOTMUCH_GIT_DIR, '.git')
-        if _os.path.isdir(_NOTMUCH_GIT_DIR):
-            NOTMUCH_GIT_DIR = _NOTMUCH_GIT_DIR
+        if nmbug_mode:
+            default = _os.path.join('~', '.nmbug')
+        else:
+            default = xdg_data_path(notmuch_profile)
+
+        NOTMUCH_GIT_DIR = _os.path.expanduser(_os.getenv('NOTMUCH_GIT_DIR', default))
+
+    _NOTMUCH_GIT_DIR = _os.path.join(NOTMUCH_GIT_DIR, '.git')
+    if _os.path.isdir(_NOTMUCH_GIT_DIR):
+        NOTMUCH_GIT_DIR = _NOTMUCH_GIT_DIR
+
+    if args.tag_prefix:
+        TAG_PREFIX = args.tag_prefix
+    else:
+        if nmbug_mode:
+            prefix = 'notmuch::'
+        else:
+            prefix = ''
+
+        TAG_PREFIX =  _os.getenv('NOTMUCH_GIT_PREFIX', prefix)
 
-    TAG_PREFIX = args.tag_prefix
     _ENCODED_TAG_PREFIX = _hex_quote(TAG_PREFIX, safe='+@=,')  # quote ':'
 
     if args.log_level:
@@ -1009,12 +1049,16 @@ if __name__ == '__main__':
 
     if _notmuch_config_get('built_with.sexp_queries') != 'true':
         _LOG.error("notmuch git needs sexp query support")
-        sys.exit(1)
+        _sys.exit(1)
 
     if not getattr(args, 'func', None):
         parser.print_usage()
         _sys.exit(1)
 
+    # The following two lines are used by the test suite.
+    _LOG.debug('prefix = {:s}'.format(TAG_PREFIX))
+    _LOG.debug('repository = {:s}'.format(NOTMUCH_GIT_DIR))
+
     if args.func == help:
         arg_names = ['command']
     else:
diff --git a/test/T850-git.sh b/test/T850-git.sh
index db76dae9..98233055 100755
--- a/test/T850-git.sh
+++ b/test/T850-git.sh
@@ -19,28 +19,28 @@ test_begin_subtest "clone"
 test_expect_success "notmuch git -p '' -C tags.git clone remote.git"
 
 test_begin_subtest "commit"
-notmuch git -C tags.git -p '' commit
+notmuch git -C tags.git commit
 git -C tags.git ls-tree -r --name-only HEAD | xargs dirname | sort -u | sed s,tags/,id:, > OUTPUT
 notmuch search --output=messages '*' | sort > EXPECTED
 test_expect_equal_file_nonempty EXPECTED OUTPUT
 
 test_begin_subtest "commit, with quoted tag"
-notmuch git -C clone2.git -p '' clone tags.git
+notmuch git -C clone2.git clone tags.git
 git -C clone2.git ls-tree -r --name-only HEAD | grep /inbox > BEFORE
 notmuch tag '+"quoted tag"' '*'
-notmuch git -C clone2.git -p '' commit
+notmuch git -C clone2.git commit
 notmuch tag '-"quoted tag"' '*'
 git -C clone2.git ls-tree -r --name-only HEAD | grep /inbox > AFTER
 test_expect_equal_file_nonempty BEFORE AFTER
 
 test_begin_subtest "commit (incremental)"
 notmuch tag +test id:20091117190054.GU3165@dottiness.seas.harvard.edu
-notmuch git -C tags.git -p '' commit
+notmuch git -C tags.git commit
 git -C tags.git ls-tree -r --name-only HEAD |
     grep 20091117190054 | sort > OUTPUT
 echo "--------------------------------------------------" >> OUTPUT
 notmuch tag -test id:20091117190054.GU3165@dottiness.seas.harvard.edu
-notmuch git -C tags.git -p '' commit
+notmuch git -C tags.git commit
 git -C tags.git ls-tree -r --name-only HEAD |
     grep 20091117190054 | sort >> OUTPUT
 cat <<EOF > EXPECTED
@@ -62,7 +62,7 @@ git -C tags.git ls-tree -r --name-only HEAD |
     grep 20091117190054 | sort > OUTPUT
 echo "--------------------------------------------------" >> OUTPUT
 notmuch tag -test::one id:20091117190054.GU3165@dottiness.seas.harvard.edu
-notmuch git -C tags.git -p '' commit
+notmuch git -C tags.git commit
 git -C tags.git ls-tree -r --name-only HEAD |
     grep 20091117190054 | sort >> OUTPUT
 cat <<EOF > EXPECTED
@@ -77,12 +77,12 @@ test_expect_equal_file_nonempty EXPECTED OUTPUT
 test_begin_subtest "checkout"
 notmuch dump > BEFORE
 notmuch tag -inbox '*'
-notmuch git -C tags.git -p '' checkout
+notmuch git -C tags.git checkout
 notmuch dump > AFTER
 test_expect_equal_file_nonempty BEFORE AFTER
 
 test_begin_subtest "archive"
-notmuch git -C tags.git -p '' archive | tar tf - | \
+notmuch git -C tags.git archive | tar tf - | \
     grep 20091117190054.GU3165@dottiness.seas.harvard.edu | sort > OUTPUT
 cat <<EOF > EXPECTED
 tags/20091117190054.GU3165@dottiness.seas.harvard.edu/
@@ -90,32 +90,32 @@ tags/20091117190054.GU3165@dottiness.seas.harvard.edu/inbox
 tags/20091117190054.GU3165@dottiness.seas.harvard.edu/signed
 tags/20091117190054.GU3165@dottiness.seas.harvard.edu/unread
 EOF
-notmuch git -C tags.git -p '' checkout
+notmuch git -C tags.git checkout
 test_expect_equal_file EXPECTED OUTPUT
 
 test_begin_subtest "status"
 notmuch tag +test id:20091117190054.GU3165@dottiness.seas.harvard.edu
-notmuch git -C tags.git -p '' status > OUTPUT
+notmuch git -C tags.git status > OUTPUT
 cat <<EOF > EXPECTED
 A	20091117190054.GU3165@dottiness.seas.harvard.edu	test
 EOF
-notmuch git -C tags.git -p '' checkout
+notmuch git -C tags.git checkout
 test_expect_equal_file EXPECTED OUTPUT
 
 test_begin_subtest "fetch"
 notmuch tag +test2 id:20091117190054.GU3165@dottiness.seas.harvard.edu
-notmuch git -C remote.git -p '' commit
+notmuch git -C remote.git commit
 notmuch tag -test2 id:20091117190054.GU3165@dottiness.seas.harvard.edu
-notmuch git -C tags.git -p '' fetch
-notmuch git -C tags.git -p '' status > OUTPUT
+notmuch git -C tags.git fetch
+notmuch git -C tags.git status > OUTPUT
 cat <<EOF > EXPECTED
  a	20091117190054.GU3165@dottiness.seas.harvard.edu	test2
 EOF
-notmuch git -C tags.git -p '' checkout
+notmuch git -C tags.git checkout
 test_expect_equal_file EXPECTED OUTPUT
 
 test_begin_subtest "merge"
-notmuch git -C tags.git -p '' merge
+notmuch git -C tags.git merge
 notmuch dump id:20091117190054.GU3165@dottiness.seas.harvard.edu | grep -v '^#' > OUTPUT
 cat <<EOF > EXPECTED
 +inbox +signed +test2 +unread -- id:20091117190054.GU3165@dottiness.seas.harvard.edu
@@ -124,10 +124,10 @@ test_expect_equal_file EXPECTED OUTPUT
 
 test_begin_subtest "push"
 notmuch tag +test3 id:20091117190054.GU3165@dottiness.seas.harvard.edu
-notmuch git -C tags.git -p '' commit
+notmuch git -C tags.git commit
 notmuch tag -test3 id:20091117190054.GU3165@dottiness.seas.harvard.edu
-notmuch git -C tags.git -p '' push
-notmuch git -C remote.git -p '' checkout
+notmuch git -C tags.git push
+notmuch git -C remote.git checkout
 notmuch dump id:20091117190054.GU3165@dottiness.seas.harvard.edu | grep -v '^#' > OUTPUT
 cat <<EOF > EXPECTED
 +inbox +signed +test2 +test3 +unread -- id:20091117190054.GU3165@dottiness.seas.harvard.edu
@@ -145,4 +145,69 @@ env NOTMUCH_CONFIG = CWD/notmuch-config
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 
+test_begin_subtest "--nmbug argument sets defaults"
+notmuch git -ldebug --nmbug status |& grep '^\(prefix\|repository\)' | notmuch_dir_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+prefix = notmuch::
+repository = CWD/home/.nmbug
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "invoke as nmbug sets defaults"
+"$NOTMUCH_BUILDDIR"/nmbug -ldebug status |& grep '^\(prefix\|repository\)' | notmuch_dir_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+prefix = notmuch::
+repository = CWD/home/.nmbug
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "env variable NOTMUCH_GIT_DIR works when invoked as nmbug"
+NOTMUCH_GIT_DIR=`pwd`/foo "$NOTMUCH_BUILDDIR"/nmbug -ldebug status |& grep '^repository' | notmuch_dir_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+repository = CWD/foo
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "env variable NOTMUCH_GIT_DIR works when invoked as 'notmuch git'"
+NOTMUCH_GIT_DIR=`pwd`/remote.git notmuch git -ldebug status |& grep '^repository' | notmuch_dir_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+repository = CWD/remote.git
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "env variable NOTMUCH_GIT_PREFIX works when invoked as nmbug"
+NOTMUCH_GIT_PREFIX=foo:: "$NOTMUCH_BUILDDIR"/nmbug -ldebug status |& grep '^prefix' | notmuch_dir_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+prefix = foo::
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "env variable NOTMUCH_GIT_PREFIX works when invoked as 'notmuch git'"
+NOTMUCH_GIT_PREFIX=env:: notmuch git -ldebug status |& grep '^prefix' | notmuch_dir_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+prefix = env::
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+
+test_begin_subtest "init, xdg default location"
+repo=home/.local/share/notmuch/default/git
+notmuch git -ldebug init |& grep '^repository' | notmuch_dir_sanitize > OUTPUT
+git -C $repo rev-parse --absolute-git-dir | notmuch_dir_sanitize >> OUTPUT
+cat <<EOF > EXPECTED
+repository = CWD/$repo
+CWD/$repo
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "init, xdg default location, with profile"
+repo=home/.local/share/notmuch/work/git
+NOTMUCH_PROFILE=work notmuch git -ldebug init |& grep '^repository' | notmuch_dir_sanitize > OUTPUT
+git -C $repo rev-parse --absolute-git-dir | notmuch_dir_sanitize >> OUTPUT
+cat <<EOF > EXPECTED
+repository = CWD/$repo
+CWD/$repo
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
 test_done
-- 
2.35.2

^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v3 15/17] CLI/git: support configuration for repo location / prefix
  2022-06-04 17:22 David Bremner
                   ` (13 preceding siblings ...)
  2022-06-04 17:23 ` [PATCH v3 14/17] CLI/git: change defaults for repo and prefix David Bremner
@ 2022-06-04 17:23 ` David Bremner
  2022-06-04 17:23 ` [PATCH v3 16/17] CLI/git: add safety checks for checkout and commit David Bremner
                   ` (2 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: David Bremner @ 2022-06-04 17:23 UTC (permalink / raw)
  To: notmuch

This is probably more convenient than always passing a command line
argument.

Use notmuch-config for consistency with other notmuch CLI tools.

Now that there is something relevant in the config files, test the
--config option.
---
 doc/man1/notmuch-config.rst |  8 ++++
 doc/man1/notmuch-git.rst    |  4 ++
 notmuch-git.py              |  6 ++-
 test/T850-git.sh            | 78 ++++++++++++++++++++++++++++++++++++-
 4 files changed, 93 insertions(+), 3 deletions(-)

diff --git a/doc/man1/notmuch-config.rst b/doc/man1/notmuch-config.rst
index 36d48725..e2e9a632 100644
--- a/doc/man1/notmuch-config.rst
+++ b/doc/man1/notmuch-config.rst
@@ -107,6 +107,14 @@ paths are presumed relative to `$HOME` for items in section
 
     Default: see :ref:`database`
 
+.. nmconfig:: git.path
+
+    Default location for git repository for :any:`notmuch-git`.
+
+.. nmconfig:: git.tag_prefix
+
+    Default tag prefix (filter) for :any:`notmuch-git`.
+
 .. nmconfig:: index.decrypt
 
     Policy for decrypting encrypted messages during indexing.  Must be
diff --git a/doc/man1/notmuch-git.rst b/doc/man1/notmuch-git.rst
index 6a2d7fcd..ad859b80 100644
--- a/doc/man1/notmuch-git.rst
+++ b/doc/man1/notmuch-git.rst
@@ -246,6 +246,8 @@ value to locate the git repository.
 
 - Environment variable :envvar:`NOTMUCH_GIT_DIR`.
 
+- Configuration item :nmconfig:`git.path`
+
 - If invoked as `nmbug` or with the :option:`--nmbug` option,
   :code:`$HOME/.nmbug`; otherwise
   :code:`$XDG_DATA_HOME/notmuch/$NOTMUCH_PROFILE/git`.
@@ -262,6 +264,8 @@ value to define the tag prefix.
 
 - Environment variable :envvar:`NOTMUCH_GIT_PREFIX`.
 
+- Configuration item :nmconfig:`git.tag_prefix`.
+
 - If invoked as `nmbug` or with the :option:`--nmbug` option,
   :code:`notmuch::`, otherwise the empty string.
 
diff --git a/notmuch-git.py b/notmuch-git.py
index 9d5c7876..35785336 100644
--- a/notmuch-git.py
+++ b/notmuch-git.py
@@ -1019,7 +1019,9 @@ if __name__ == '__main__':
         if nmbug_mode:
             default = _os.path.join('~', '.nmbug')
         else:
-            default = xdg_data_path(notmuch_profile)
+            default = _notmuch_config_get ('git.path')
+            if default == '':
+                default = xdg_data_path(notmuch_profile)
 
         NOTMUCH_GIT_DIR = _os.path.expanduser(_os.getenv('NOTMUCH_GIT_DIR', default))
 
@@ -1033,7 +1035,7 @@ if __name__ == '__main__':
         if nmbug_mode:
             prefix = 'notmuch::'
         else:
-            prefix = ''
+            prefix = _notmuch_config_get ('git.tag_prefix')
 
         TAG_PREFIX =  _os.getenv('NOTMUCH_GIT_PREFIX', prefix)
 
diff --git a/test/T850-git.sh b/test/T850-git.sh
index 98233055..f8537064 100755
--- a/test/T850-git.sh
+++ b/test/T850-git.sh
@@ -15,6 +15,13 @@ git config --global user.name  "Notmuch Test Suite"
 test_begin_subtest "init"
 test_expect_success "notmuch git -p '' -C remote.git init"
 
+test_begin_subtest "init (git.path)"
+notmuch config set git.path configured.git
+notmuch git init
+notmuch config set git.path
+output=$(git -C configured.git rev-parse --is-bare-repository)
+test_expect_equal "$output" "true"
+
 test_begin_subtest "clone"
 test_expect_success "notmuch git -p '' -C tags.git clone remote.git"
 
@@ -102,6 +109,17 @@ EOF
 notmuch git -C tags.git checkout
 test_expect_equal_file EXPECTED OUTPUT
 
+test_begin_subtest "status (global config argument)"
+cp notmuch-config notmuch-config.new
+notmuch --config=notmuch-config.new config set git.path tags.git
+notmuch tag +test id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch --config=./notmuch-config.new git status > OUTPUT
+cat <<EOF > EXPECTED
+A	20091117190054.GU3165@dottiness.seas.harvard.edu	test
+EOF
+notmuch --config=notmuch-config.new git checkout
+test_expect_equal_file EXPECTED OUTPUT
+
 test_begin_subtest "fetch"
 notmuch tag +test2 id:20091117190054.GU3165@dottiness.seas.harvard.edu
 notmuch git -C remote.git commit
@@ -175,6 +193,32 @@ repository = CWD/remote.git
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 
+
+test_begin_subtest "env variable NOTMUCH_GIT_DIR overrides config when invoked as 'nmbug'"
+notmuch config set git.path `pwd`/bar
+NOTMUCH_GIT_DIR=`pwd`/remote.git  "$NOTMUCH_BUILDDIR"/nmbug -ldebug status |& grep '^repository' | notmuch_dir_sanitize > OUTPUT
+notmuch config set git.path
+cat <<EOF > EXPECTED
+repository = CWD/remote.git
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "env variable NOTMUCH_GIT_DIR overrides config when invoked as 'notmuch git'"
+notmuch config set git.path `pwd`/bar
+NOTMUCH_GIT_DIR=`pwd`/remote.git notmuch git -ldebug status |& grep '^repository' | notmuch_dir_sanitize > OUTPUT
+notmuch config set git.path
+cat <<EOF > EXPECTED
+repository = CWD/remote.git
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "env variable NOTMUCH_GIT_PREFIX works when invoked as 'nmbug'"
+NOTMUCH_GIT_PREFIX=env:: "$NOTMUCH_BUILDDIR"/nmbug -ldebug status |& grep '^prefix' | notmuch_dir_sanitize > OUTPUT
+cat <<EOF > EXPECTED
+prefix = env::
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
 test_begin_subtest "env variable NOTMUCH_GIT_PREFIX works when invoked as nmbug"
 NOTMUCH_GIT_PREFIX=foo:: "$NOTMUCH_BUILDDIR"/nmbug -ldebug status |& grep '^prefix' | notmuch_dir_sanitize > OUTPUT
 cat <<EOF > EXPECTED
@@ -182,8 +226,19 @@ prefix = foo::
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 
-test_begin_subtest "env variable NOTMUCH_GIT_PREFIX works when invoked as 'notmuch git'"
+test_begin_subtest "env variable NOTMUCH_GIT_PREFIX overrides config when invoked as 'nmbug'"
+notmuch config set git.tag_prefix config::
+NOTMUCH_GIT_PREFIX=env:: "$NOTMUCH_BUILDDIR"/nmbug -ldebug status |& grep '^prefix' | notmuch_dir_sanitize > OUTPUT
+notmuch config set git.path
+cat <<EOF > EXPECTED
+prefix = env::
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "env variable NOTMUCH_GIT_PREFIX overrides config when invoked as 'notmuch git'"
+notmuch config set git.tag_prefix config::
 NOTMUCH_GIT_PREFIX=env:: notmuch git -ldebug status |& grep '^prefix' | notmuch_dir_sanitize > OUTPUT
+notmuch config set git.path
 cat <<EOF > EXPECTED
 prefix = env::
 EOF
@@ -210,4 +265,25 @@ CWD/$repo
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 
+test_begin_subtest "init, configured location"
+repo=configured-tags
+notmuch config set git.path `pwd`/$repo
+notmuch git -ldebug init |& grep '^repository' | notmuch_dir_sanitize > OUTPUT
+notmuch config set git.path
+git -C $repo rev-parse --absolute-git-dir | notmuch_dir_sanitize >> OUTPUT
+cat <<EOF > EXPECTED
+repository = CWD/$repo
+CWD/$repo
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "configured tag prefix"
+notmuch config set git.tag_prefix test::
+notmuch git -ldebug status |& grep '^prefix' > OUTPUT
+notmuch config set git.tag_prefix
+cat <<EOF > EXPECTED
+prefix = test::
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
 test_done
-- 
2.35.2

^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v3 16/17] CLI/git: add safety checks for checkout and commit
  2022-06-04 17:22 David Bremner
                   ` (14 preceding siblings ...)
  2022-06-04 17:23 ` [PATCH v3 15/17] CLI/git: support configuration for repo location / prefix David Bremner
@ 2022-06-04 17:23 ` David Bremner
  2022-06-04 17:23 ` [PATCH v3 17/17] debian: install notmuch-git David Bremner
  2022-06-18 11:21 ` notmuch-git v3 David Bremner
  17 siblings, 0 replies; 19+ messages in thread
From: David Bremner @ 2022-06-04 17:23 UTC (permalink / raw)
  To: notmuch

Commits or checkouts that modify a large fraction of the messages in
the database should be relatively rare (and in some automated process,
probably non-existent). For initial setup, where such operations are
expected, the user can pass --force.
---
 doc/man1/notmuch-config.rst |  7 ++++
 doc/man1/notmuch-git.rst    | 14 ++++++--
 notmuch-git.py              | 47 ++++++++++++++++++++++--
 test/T850-git.sh            | 71 ++++++++++++++++++++++++++++++++++---
 4 files changed, 130 insertions(+), 9 deletions(-)

diff --git a/doc/man1/notmuch-config.rst b/doc/man1/notmuch-config.rst
index e2e9a632..388315f6 100644
--- a/doc/man1/notmuch-config.rst
+++ b/doc/man1/notmuch-config.rst
@@ -111,6 +111,13 @@ paths are presumed relative to `$HOME` for items in section
 
     Default location for git repository for :any:`notmuch-git`.
 
+.. nmconfig:: git.safe_fraction
+
+   Some :any:`notmuch-git` operations check that the fraction of
+   messages changed (in the database or in git, as appropriate) is not
+   too large. This item controls what fraction of total messages is
+   considered "not too large".
+
 .. nmconfig:: git.tag_prefix
 
     Default tag prefix (filter) for :any:`notmuch-git`.
diff --git a/doc/man1/notmuch-git.rst b/doc/man1/notmuch-git.rst
index ad859b80..fa7a748e 100644
--- a/doc/man1/notmuch-git.rst
+++ b/doc/man1/notmuch-git.rst
@@ -73,13 +73,18 @@ Dump a tar archive of a committed tag set using 'git archive'. See
    :manpage:`git-archive(1)`. Arguments to `git-archive` are reordered
    so that *tree-ish* comes last.
 
-.. option:: checkout
+.. option:: checkout [-f|--force]
 
 Update the notmuch database from Git.
 
 This is mainly useful to discard your changes in notmuch relative
 to Git.
 
+   .. describe:: [-f|--force]
+
+   Override checks that prevent modifying tags for large fractions of
+   messages in the database. See also :nmconfig:`git.safe_fraction`.
+
 .. option:: clone <repository>
 
 Create a local `notmuch git` repository from a remote source.
@@ -94,7 +99,7 @@ upstreams.
     section of :manpage:`git-clone(1)` for more information on
     specifying repositories.
 
-.. option:: commit [message]
+.. option:: commit [-f|--force] [message]
 
 Commit prefix-matching tags from the notmuch database to Git.
 
@@ -102,6 +107,11 @@ Commit prefix-matching tags from the notmuch database to Git.
 
    Optional text for the commit message.
 
+   .. describe:: -f|--force
+
+   Override checks that prevent modifying tags for large fractions of
+   messages in the database. See also :nmconfig:`git.safe_fraction`.
+
 .. option:: fetch [remote]
 
 Fetch changes from the remote repository.
diff --git a/notmuch-git.py b/notmuch-git.py
index 35785336..24ab3e5c 100644
--- a/notmuch-git.py
+++ b/notmuch-git.py
@@ -239,6 +239,16 @@ def _tag_query(prefix=None):
         prefix = TAG_PREFIX
     return '(tag (starts-with "{:s}"))'.format(prefix.replace('"','\\\"'))
 
+def count_messages(prefix=None):
+    "count messages with a given prefix."
+    (status, stdout, stderr) = _spawn(
+        args=['notmuch', 'count', '--query=sexp', _tag_query(prefix)],
+        stdout=_subprocess.PIPE, wait=True)
+    if status != 0:
+        _LOG.error("failed to run notmuch config")
+        sys.exit(1)
+    return int(stdout.rstrip())
+
 def get_tags(prefix=None):
     "Get a list of tags with a given prefix."
     (status, stdout, stderr) = _spawn(
@@ -357,7 +367,26 @@ class CachedIndex:
         _git(args=['read-tree', self.current_treeish], wait=True)
 
 
-def commit(treeish='HEAD', message=None):
+def check_safe_fraction(status):
+    safe = 0.1
+    conf = _notmuch_config_get ('git.safe_fraction')
+    if conf and conf != '':
+        safe=float(conf)
+
+    total = count_messages (TAG_PREFIX)
+    if total == 0:
+        _LOG.error('No existing tags with given prefix, stopping.'.format(safe))
+        _LOG.error('Use --force to override.')
+        exit(1)
+    change = len(status['added'])+len(status['deleted'])+len(status['missing'])
+    fraction = change/total
+    _LOG.debug('total messages {:d}, change: {:d}, fraction: {:f}'.format(total,change,fraction))
+    if fraction > safe:
+        _LOG.error('safe fraction {:f} exceeded, stopping.'.format(safe))
+        _LOG.error('Use --force to override or reconfigure git.safe_fraction.')
+        exit(1)
+
+def commit(treeish='HEAD', message=None, force=False):
     """
     Commit prefix-matching tags from the notmuch database to Git.
     """
@@ -368,6 +397,9 @@ def commit(treeish='HEAD', message=None):
         _LOG.warning('Nothing to commit')
         return
 
+    if not force:
+        check_safe_fraction (status)
+
     with CachedIndex(NOTMUCH_GIT_DIR, treeish) as index:
         try:
             _update_index(status=status)
@@ -445,7 +477,7 @@ def init(remote=None):
         wait=True)
 
 
-def checkout():
+def checkout(force=None):
     """
     Update the notmuch database from Git.
 
@@ -453,6 +485,10 @@ def checkout():
     to Git.
     """
     status = get_status()
+
+    if not force:
+        check_safe_fraction(status)
+
     with _spawn(
             args=['notmuch', 'tag', '--batch'], stdin=_subprocess.PIPE) as p:
         for id, tags in status['added'].items():
@@ -943,6 +979,10 @@ if __name__ == '__main__':
                 help=(
                     "Argument passed through to 'git archive'.  Set anything "
                     'before <tree-ish>, see git-archive(1) for details.'))
+        elif command == 'checkout':
+            subparser.add_argument(
+                '-f', '--force', action='store_true',
+                help='checkout a large fraction of tags.')
         elif command == 'clone':
             subparser.add_argument(
                 'repository',
@@ -951,6 +991,9 @@ if __name__ == '__main__':
                     'URLS section of git-clone(1) for more information on '
                     'specifying repositories.'))
         elif command == 'commit':
+            subparser.add_argument(
+                '-f', '--force', action='store_true',
+                help='commit a large fraction of tags.')
             subparser.add_argument(
                 'message', metavar='MESSAGE', default='', nargs='?',
                 help='Text for the commit message.')
diff --git a/test/T850-git.sh b/test/T850-git.sh
index f8537064..7ea50939 100755
--- a/test/T850-git.sh
+++ b/test/T850-git.sh
@@ -7,6 +7,9 @@ if [ $NOTMUCH_HAVE_SFSEXP -ne 1 ]; then
     test_done
 fi
 
+# be very careful using backup_database / restore_database in this
+# file, as they fool the cache invalidation checks in notmuch-git.
+
 add_email_corpus
 
 git config --global user.email notmuch@example.org
@@ -25,12 +28,54 @@ test_expect_equal "$output" "true"
 test_begin_subtest "clone"
 test_expect_success "notmuch git -p '' -C tags.git clone remote.git"
 
+test_begin_subtest "initial commit needs force"
+test_expect_code 1 "notmuch git -C tags.git commit"
+
+test_begin_subtest "committing new prefix requires force"
+notmuch git -C force-prefix.git init
+notmuch tag +new-prefix::foo id:20091117190054.GU3165@dottiness.seas.harvard.edu
+test_expect_code 1 "notmuch git -l debug -p 'new-prefix::' -C force-prefix.git commit"
+notmuch tag -new-prefix::foo id:20091117190054.GU3165@dottiness.seas.harvard.edu
+
+test_begin_subtest "committing new prefix works with force"
+notmuch tag +new-prefix::foo id:20091117190054.GU3165@dottiness.seas.harvard.edu
+notmuch git -l debug -p 'new-prefix::' -C force-prefix.git commit --force
+git -C force-prefix.git ls-tree -r --name-only HEAD | xargs dirname | sort -u | sed s,tags/,id:, > OUTPUT
+notmuch tag -new-prefix::foo id:20091117190054.GU3165@dottiness.seas.harvard.edu
+cat <<EOF>EXPECTED
+id:20091117190054.GU3165@dottiness.seas.harvard.edu
+EOF
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
+test_begin_subtest "checkout new prefix requires force"
+test_expect_code 1 "notmuch git -l debug -p 'new-prefix::' -C force-prefix.git checkout"
+
+test_begin_subtest "checkout new prefix works with force"
+notmuch dump > BEFORE
+notmuch git -l debug -p 'new-prefix::' -C force-prefix.git checkout --force
+notmuch dump --include=tags id:20091117190054.GU3165@dottiness.seas.harvard.edu | grep -v '^#' > OUTPUT
+notmuch restore < BEFORE
+cat <<EOF > EXPECTED
++inbox +new-prefix%3a%3afoo +signed +unread -- id:20091117190054.GU3165@dottiness.seas.harvard.edu
+EOF
+test_expect_equal_file_nonempty EXPECTED OUTPUT
+
 test_begin_subtest "commit"
-notmuch git -C tags.git commit
+notmuch git -C tags.git commit --force
 git -C tags.git ls-tree -r --name-only HEAD | xargs dirname | sort -u | sed s,tags/,id:, > OUTPUT
 notmuch search --output=messages '*' | sort > EXPECTED
 test_expect_equal_file_nonempty EXPECTED OUTPUT
 
+test_begin_subtest "commit --force succeeds"
+notmuch git -C force.git init
+test_expect_success "notmuch git -C force.git commit --force"
+
+test_begin_subtest "changing git.safe_fraction succeeds"
+notmuch config set git.safe_fraction 1
+notmuch git -C force2.git init
+test_expect_success "notmuch git -C force2.git commit"
+notmuch config set git.safe_fraction
+
 test_begin_subtest "commit, with quoted tag"
 notmuch git -C clone2.git clone tags.git
 git -C clone2.git ls-tree -r --name-only HEAD | grep /inbox > BEFORE
@@ -64,12 +109,12 @@ test_expect_equal_file_nonempty EXPECTED OUTPUT
 
 test_begin_subtest "commit (change prefix)"
 notmuch tag +test::one id:20091117190054.GU3165@dottiness.seas.harvard.edu
-notmuch git -C tags.git -p 'test::' commit
+notmuch git -C tags.git -p 'test::' commit --force
 git -C tags.git ls-tree -r --name-only HEAD |
     grep 20091117190054 | sort > OUTPUT
 echo "--------------------------------------------------" >> OUTPUT
 notmuch tag -test::one id:20091117190054.GU3165@dottiness.seas.harvard.edu
-notmuch git -C tags.git commit
+notmuch git -C tags.git commit --force
 git -C tags.git ls-tree -r --name-only HEAD |
     grep 20091117190054 | sort >> OUTPUT
 cat <<EOF > EXPECTED
@@ -81,10 +126,26 @@ tags/20091117190054.GU3165@dottiness.seas.harvard.edu/unread
 EOF
 test_expect_equal_file_nonempty EXPECTED OUTPUT
 
+backup_database
+test_begin_subtest "large checkout needs --force"
+notmuch tag -inbox '*'
+test_expect_code 1 "notmuch git -C tags.git checkout"
+restore_database
+
+test_begin_subtest "checkout (git.safe_fraction)"
+notmuch git -C force3.git clone tags.git
+notmuch dump > BEFORE
+notmuch tag -inbox '*'
+notmuch config set git.safe_fraction 1
+notmuch git -C force3.git checkout
+notmuch config set git.safe_fraction
+notmuch dump > AFTER
+test_expect_equal_file_nonempty BEFORE AFTER
+
 test_begin_subtest "checkout"
 notmuch dump > BEFORE
 notmuch tag -inbox '*'
-notmuch git -C tags.git checkout
+notmuch git -C tags.git checkout --force
 notmuch dump > AFTER
 test_expect_equal_file_nonempty BEFORE AFTER
 
@@ -122,7 +183,7 @@ test_expect_equal_file EXPECTED OUTPUT
 
 test_begin_subtest "fetch"
 notmuch tag +test2 id:20091117190054.GU3165@dottiness.seas.harvard.edu
-notmuch git -C remote.git commit
+notmuch git -C remote.git commit --force
 notmuch tag -test2 id:20091117190054.GU3165@dottiness.seas.harvard.edu
 notmuch git -C tags.git fetch
 notmuch git -C tags.git status > OUTPUT
-- 
2.35.2

^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v3 17/17] debian: install notmuch-git
  2022-06-04 17:22 David Bremner
                   ` (15 preceding siblings ...)
  2022-06-04 17:23 ` [PATCH v3 16/17] CLI/git: add safety checks for checkout and commit David Bremner
@ 2022-06-04 17:23 ` David Bremner
  2022-06-18 11:21 ` notmuch-git v3 David Bremner
  17 siblings, 0 replies; 19+ messages in thread
From: David Bremner @ 2022-06-04 17:23 UTC (permalink / raw)
  To: notmuch

Use a separate binary package to avoid dragging in dependencies on
python and git for those that do not want them.
---
 debian/control              | 16 ++++++++++++++++
 debian/notmuch-git.install  |  2 ++
 debian/notmuch-git.manpages |  2 ++
 3 files changed, 20 insertions(+)
 create mode 100644 debian/notmuch-git.install
 create mode 100644 debian/notmuch-git.manpages

diff --git a/debian/control b/debian/control
index a11d4130..9706b0f7 100644
--- a/debian/control
+++ b/debian/control
@@ -66,6 +66,22 @@ Description: thread-based email index, search and tagging
  .
  This package contains the notmuch command-line interface
 
+Package: notmuch-git
+Architecture: all
+Depends:
+ git,
+ notmuch,
+ python3,
+ ${misc:Depends}
+Description: thread-based email index, search and tagging
+ Notmuch is a system for indexing, searching, reading, and tagging
+ large collections of email messages in maildir or mh format. It uses
+ the Xapian library to provide fast, full-text search with a very
+ convenient search syntax.
+ .
+ This package contains a simple tool to save, restore, and synchronize
+ notmuch tags via git repositories.
+
 Package: notmuch-doc
 Architecture: all
 Depends:
diff --git a/debian/notmuch-git.install b/debian/notmuch-git.install
new file mode 100644
index 00000000..2be08276
--- /dev/null
+++ b/debian/notmuch-git.install
@@ -0,0 +1,2 @@
+notmuch-git /usr/bin
+nmbug /usr/bin
diff --git a/debian/notmuch-git.manpages b/debian/notmuch-git.manpages
new file mode 100644
index 00000000..e0895c86
--- /dev/null
+++ b/debian/notmuch-git.manpages
@@ -0,0 +1,2 @@
+usr/share/man/man1/notmuch-git.1.gz
+usr/share/man/man1/nmbug.1.gz
-- 
2.35.2

^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: notmuch-git v3
  2022-06-04 17:22 David Bremner
                   ` (16 preceding siblings ...)
  2022-06-04 17:23 ` [PATCH v3 17/17] debian: install notmuch-git David Bremner
@ 2022-06-18 11:21 ` David Bremner
  17 siblings, 0 replies; 19+ messages in thread
From: David Bremner @ 2022-06-18 11:21 UTC (permalink / raw)
  To: notmuch

David Bremner <david@tethera.net> writes:

> This series is against branch "next", which is currently ahead of
> master by the following 7 commits

Slightly updated version of this series applied to master. Interdiff follows.

diff --git a/Makefile.local b/Makefile.local
index 0fadfb26..7699c208 100644
--- a/Makefile.local
+++ b/Makefile.local
@@ -45,6 +45,15 @@ $(SHA256_FILE): $(TAR_FILE)
 $(DETACHED_SIG_FILE): $(TAR_FILE)
 	gpg --armor --detach-sign $^
 
+CLEAN := $(CLEAN) notmuch-git
+notmuch-git: notmuch-git.py
+	cp $< $@
+	chmod ugo+x $@
+
+CLEAN := $(CLEAN) nmbug
+nmbug: notmuch-git
+	ln -s $< $@
+
 .PHONY: dist
 dist: $(TAR_FILE)
 
@@ -294,7 +303,7 @@ endif
 SRCS  := $(SRCS) $(notmuch_client_srcs)
 CLEAN := $(CLEAN) notmuch notmuch-shared $(notmuch_client_modules)
 CLEAN := $(CLEAN) version.stamp notmuch-*.tar.gz.tmp
-CLEAN := $(CLEAN) .deps notmuch-git
+CLEAN := $(CLEAN) .deps
 
 DISTCLEAN := $(DISTCLEAN) .first-build-message Makefile.config sh.config sphinx.config
 
@@ -307,10 +316,6 @@ cppcheck:
 	@echo "No cppcheck found during configure; skipping static checking"
 endif
 
-nmbug notmuch-git: notmuch-git.in
-	sed s/@NOTMUCH_VERSION@/${VERSION}/ < notmuch-git.in > notmuch-git
-	chmod ugo+rx notmuch-git
-	ln -sf notmuch-git nmbug
 
 DEPS := $(SRCS:%.c=.deps/%.d)
 DEPS := $(DEPS:%.cc=.deps/%.d)
diff --git a/notmuch-git.in b/notmuch-git.py
old mode 100755
new mode 100644
similarity index 99%
rename from notmuch-git.in
rename to notmuch-git.py
index 0e35db10..f188660c
--- a/notmuch-git.in
+++ b/notmuch-git.py
@@ -40,8 +40,6 @@ from urllib.parse import quote as _quote
 from urllib.parse import unquote as _unquote
 import json as _json
 
-__version__ = '@NOTMUCH_VERSION@'
-
 _LOG = _logging.getLogger('nmbug')
 _LOG.setLevel(_logging.WARNING)
 _LOG.addHandler(_logging.StreamHandler())
@@ -380,7 +378,7 @@ def check_safe_fraction(status):
         _LOG.error('No existing tags with given prefix, stopping.'.format(safe))
         _LOG.error('Use --force to override.')
         exit(1)
-    change = len(status['added'])+len(status['deleted'])+len(status['missing'])
+    change = len(status['added'])+len(status['deleted'])
     fraction = change/total
     _LOG.debug('total messages {:d}, change: {:d}, fraction: {:f}'.format(total,change,fraction))
     if fraction > safe:
@@ -935,9 +933,6 @@ if __name__ == '__main__':
     parser.add_argument(
         '-N', '--nmbug', action='store_true',
         help='Set defaults for --tag-prefix and --git-dir for the notmuch bug tracker')
-    parser.add_argument(
-        '-v', '--version', action='version',
-        version='%(prog)s {}'.format(__version__))
     parser.add_argument(
         '-l', '--log-level',
         choices=['critical', 'error', 'warning', 'info', 'debug'],
diff --git a/notmuch.c b/notmuch.c
index a6c49bcb..c75b0188 100644
--- a/notmuch.c
+++ b/notmuch.c
@@ -201,8 +201,6 @@ static const command_t commands[] = {
     { "emacs-mua", NULL, 0,
       "send mail with notmuch and emacs." },
 #endif
-    { "git", NULL, 0,
-      "manage notmuch tags with git" },
     { "help", notmuch_help_command, NOTMUCH_COMMAND_CONFIG_CREATE, /* create but don't save config */
       "This message, or more detailed help for the named command." }
 };
diff --git a/test/T850-git.sh b/test/T850-git.sh
index a865c7bf..7ea50939 100755
--- a/test/T850-git.sh
+++ b/test/T850-git.sh
@@ -171,13 +171,14 @@ notmuch git -C tags.git checkout
 test_expect_equal_file EXPECTED OUTPUT
 
 test_begin_subtest "status (global config argument)"
-test_subtest_known_broken
+cp notmuch-config notmuch-config.new
+notmuch --config=notmuch-config.new config set git.path tags.git
 notmuch tag +test id:20091117190054.GU3165@dottiness.seas.harvard.edu
-notmuch --config=./notmuch-config git -C tags.git status > OUTPUT
+notmuch --config=./notmuch-config.new git status > OUTPUT
 cat <<EOF > EXPECTED
 A	20091117190054.GU3165@dottiness.seas.harvard.edu	test
 EOF
-notmuch git -C tags.git checkout
+notmuch --config=notmuch-config.new git checkout
 test_expect_equal_file EXPECTED OUTPUT
 
 test_begin_subtest "fetch"

^ permalink raw reply	[flat|nested] 19+ messages in thread

end of thread, other threads:[~2022-06-18 11:21 UTC | newest]

Thread overview: 19+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-06-04 17:22 David Bremner
2022-06-04 17:22 ` [PATCH v3 01/17] nmbug: promote to user tool "notmuch-git" David Bremner
2022-06-04 17:22 ` [PATCH v3 02/17] CLI/git: drop support for python < 3.2 David Bremner
2022-06-04 17:22 ` [PATCH v3 03/17] notmuch-git: add --git-dir, --tag-prefix arguments David Bremner
2022-06-04 17:23 ` [PATCH v3 04/17] CLI/git: make existance of config branch optional on clone David Bremner
2022-06-04 17:23 ` [PATCH v3 05/17] CLI/git: Add an 'init' command David Bremner
2022-06-04 17:23 ` [PATCH v3 06/17] test: initial tests for notmuch-git David Bremner
2022-06-04 17:23 ` [PATCH v3 07/17] CLI/git: rename environment variables David Bremner
2022-06-04 17:23 ` [PATCH v3 08/17] CLI/git: suppress warnings about initial branch name David Bremner
2022-06-04 17:23 ` [PATCH v3 09/17] test/git: add known broken test for tag with quotes David Bremner
2022-06-04 17:23 ` [PATCH v3 10/17] CLI/git: replace enumeration of tags with sexp query David Bremner
2022-06-04 17:23 ` [PATCH v3 11/17] CLI/git: add @timed decorator, time a few functions David Bremner
2022-06-04 17:23 ` [PATCH v3 12/17] CLI/git: cache git indices David Bremner
2022-06-04 17:23 ` [PATCH v3 13/17] doc/notmuch-git: initial documentation David Bremner
2022-06-04 17:23 ` [PATCH v3 14/17] CLI/git: change defaults for repo and prefix David Bremner
2022-06-04 17:23 ` [PATCH v3 15/17] CLI/git: support configuration for repo location / prefix David Bremner
2022-06-04 17:23 ` [PATCH v3 16/17] CLI/git: add safety checks for checkout and commit David Bremner
2022-06-04 17:23 ` [PATCH v3 17/17] debian: install notmuch-git David Bremner
2022-06-18 11:21 ` notmuch-git v3 David Bremner

Code repositories for project(s) associated with this inbox:

	notmuch.git.git (no URL configured)

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).