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

This series obsolates the WIP series at [1]. The rather long interdiff
is given below [2].

At this point I think the tool is feature complete, at least enough
for initial release. The main changes since the last series are the
addition of configuration variables and a "safe_fraction" check that
asks the user to override any operation that would change "lots" of
the database or git repo.  There is also a bunch more documentation
and tests. Finally I dropped support for old python versions (older
than 3.2, including python2). I'll try to get those into master
relatively quickly, to make reviewing this series easier.

This series needs to be applied on top of a couple of patches not yet in master [3]

[1]: id:20220423133848.3852688-1-david@tethera.net

[2]: interdiff

[3]: definitely id:20220513122929.1981190-1-david@tethera.net and
probably id:20220514122406.2220621-1-david@tethera.net

diff --git a/Makefile.local b/Makefile.local
index ca2310f4..0fadfb26 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
@@ -307,9 +307,10 @@ cppcheck:
 	@echo "No cppcheck found during configure; skipping static checking"
 endif
 
-notmuch-git: notmuch-git.in
+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/debian/notmuch-git.install b/debian/notmuch-git.install
index ca632418..2be08276 100644
--- a/debian/notmuch-git.install
+++ b/debian/notmuch-git.install
@@ -1 +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
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-config.rst b/doc/man1/notmuch-config.rst
index 36d48725..388315f6 100644
--- a/doc/man1/notmuch-config.rst
+++ b/doc/man1/notmuch-config.rst
@@ -107,6 +107,21 @@ 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.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`.
+
 .. 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 4877f22d..fa7a748e 100644
--- a/doc/man1/notmuch-git.rst
+++ b/doc/man1/notmuch-git.rst
@@ -1,23 +1,25 @@
 .. _notmuch-git(1):
 
-============
+===========
 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
 ===========
 
 Manage notmuch tags with Git.
 
-Options
+OPTIONS
 -------
 
-Supported options for **notmuch-git** include
+Supported options for `notmuch git` include
 
 .. program:: notmuch-git
 
@@ -25,77 +27,271 @@ Supported options for **notmuch-git** include
 
    show help message and exit
 
-.. option:: -C repo, --git-dir repo
+.. option:: -N, --nmbug
+
+   Set defaults for :option:`--tag-prefix` and :option:`--git-dir` suitable for the
+   :any:`notmuch` bug tracker
 
-   Operate on git repository *repo*
+.. option:: -C <repo>, --git-dir <repo>
 
-.. option::  -p prefix, --tag-prefix prefix
+   Operate on git repository *repo*. See :ref:`repo_location` for
+   defaults.
 
-   Operate only on tags with prefix *prefix*
+.. 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* {critical,error,warning,info,debug}
+.. option::   -l <level>, --log-level <level>
 
-   Log verbosity. Defaults to 'warning'.
+   Log verbosity, one of: `critical`, `error`, `warning`, `info`,
+   `debug`. Defaults to `warning`.
 
-Subcommands
+SUBCOMMANDS
 -----------
 
 For help on a particular subcommand, run: 'notmuch-git ... <command> --help'.
 
-.. option:: archive [TREE-ISH] [ARG ...]
+.. program:: notmuch-git
 
-Dump a tar archive of the current nmbug tag set using 'git archive'.
+.. option:: archive [tree-ish] [arg ...]
 
-For each tag *tag* for message with Message-Id *id* an empty file
+Dump a tar archive of a committed tag set using 'git archive'. See
+:any:`format` for details of the archive contents.
 
-  tags/encode(*id*)/encode(*tag*)
+   .. describe:: tree-ish
 
-is written to the output.
+   The tree or commit to produce an archive for. Defaults to 'HEAD'.
 
-The encoding preserves alphanumerics, and the characters
-"+-_@=.:," (not the quotes).  All other octets are replaced with
-'%%' followed by a two digit hex number.
+   .. describe:: arg
 
-positional arguments:
-  TREE-ISH    The tree or commit to produce an archive for. Defaults to
-              'HEAD'.
-  ARG         Argument passed through to 'git archive'. Set anything before
-              <tree-ish>, see any:`git-archive(1)` for details.
+   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
+.. option:: checkout [-f|--force]
 
 Update the notmuch database from Git.
 
 This is mainly useful to discard your changes in notmuch relative
 to Git.
-Create a local nmbug repository from a remote source.
 
-.. option:: clone repository
+   .. 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.
 
 This wraps 'git clone', adding some options to avoid creating a
 working tree while preserving remote-tracking branches and
 upstreams.
 
-positional arguments:
-  repository  The (possibly remote) repository to clone from. See the URLS section of git-clone(1) for more information on specifying repositories.
+    .. 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 [-f|--force] [message]
+
+Commit prefix-matching tags from the notmuch database to Git.
+
+   .. describe:: message
+
+   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.
+
+    .. 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`.
+
+- 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`.
+
+.. _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`.
+
+- Configuration item :nmconfig:`git.tag_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.
 
-    clone               Create a local nmbug repository from a remote source.
-    commit              Commit prefix-matching tags from the notmuch database to Git.
-    fetch               Fetch changes from the remote repository.
-    help                Show help for an nmbug command.
-    init                Create an empty nmbug repository.
-    log                 A simple wrapper for 'git log'.
-    merge               Merge changes from 'reference' into HEAD and load the result into notmuch.
-    pull                Pull (merge) remote repository changes to notmuch.
-    push                Push the local nmbug Git state to a remote repository.
-    status              Show pending updates in notmuch or git repo.
+.. 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
 ========
diff --git a/notmuch-git.in b/notmuch-git.in
index 261b3f85..6505c2e5 100755
--- a/notmuch-git.in
+++ b/notmuch-git.in
@@ -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
@@ -43,25 +36,17 @@ 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
 import json as _json
 
-# hopefully big enough, handle 32 bit hosts
-MAX_LASTMOD=2**32
-
 __version__ = '@NOTMUCH_VERSION@'
 
 _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}')
@@ -71,31 +56,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'.
@@ -248,7 +208,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)
 
 
@@ -281,6 +241,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(
@@ -317,7 +287,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))
@@ -399,7 +369,22 @@ 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)
+    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.
     """
@@ -410,7 +395,10 @@ def commit(treeish='HEAD', message=None):
         _LOG.warning('Nothing to commit')
         return
 
-    with CachedIndex(NMBGIT, treeish) as index:
+    if not force:
+        check_safe_fraction (status)
+
+    with CachedIndex(NOTMUCH_GIT_DIR, treeish) as index:
         try:
             _update_index(status=status)
             (_, tree, _) = _git(
@@ -467,7 +455,14 @@ 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',
+    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)
     # create an empty blob (e69de29bb2d1d6434b8b29ae775ad8c2e48c5391)
@@ -476,11 +471,11 @@ 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)
 
 
-def checkout():
+def checkout(force=None):
     """
     Update the notmuch database from Git.
 
@@ -488,6 +483,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():
@@ -682,7 +681,7 @@ def get_status():
         'deleted': {},
         'missing': {},
         }
-    with PrivateIndex(repo=NMBGIT, prefix=TAG_PREFIX) as 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(
@@ -897,6 +896,24 @@ 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()
+
+# 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
@@ -909,8 +926,11 @@ if __name__ == '__main__':
         help='Git repository to operate on.')
     parser.add_argument(
         '-p', '--tag-prefix', metavar='PREFIX',
-        default = _os.getenv('NMBPREFIX', '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(
         '-v', '--version', action='version',
         version='%(prog)s {}'.format(__version__))
@@ -960,6 +980,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',
@@ -968,6 +992,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.')
@@ -1024,35 +1051,60 @@ 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:
-        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
+        if nmbug_mode:
+            default = _os.path.join('~', '.nmbug')
+        else:
+            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))
+
+    _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 = _notmuch_config_get ('git.tag_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:
         level = getattr(_logging, args.log_level.upper())
         _LOG.setLevel(level)
 
-    (status, stdout, stderr) = _spawn(
-        args=['notmuch', 'config', 'get', 'built_with.sexp_queries'],
-        stdout=_subprocess.PIPE, wait=True)
-    if status != 0:
-        _LOG.error("failed to run notmuch")
-        sys.exit(1)
-    if stdout != "true\n":
+    # for test suite
+    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)
 
+    # 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/notmuch.c b/notmuch.c
index ac25ae18..8c53fb80 100644
--- a/notmuch.c
+++ b/notmuch.c
@@ -201,6 +201,8 @@ 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 2358690f..508615e1 100755
--- a/test/T850-git.sh
+++ b/test/T850-git.sh
@@ -2,45 +2,65 @@
 test_description='"notmuch git" to save and restore tags'
 . $(dirname "$0")/test-lib.sh || exit 1
 
-add_git_repos () {
-    notmuch git -C remote.git -p '' init
-    notmuch git -C tags.git -p '' clone remote.git
-}
-
 if [ $NOTMUCH_HAVE_SFSEXP -ne 1 ]; then
     printf "Skipping due to missing sfsexp library\n"
     test_done
 fi
 
 add_email_corpus
-add_git_repos
+
+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 "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 -C clone.git clone tags.git"
+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 "commit"
-notmuch git -C tags.git -p '' 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 -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
@@ -57,12 +77,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 -p '' 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
@@ -74,15 +94,31 @@ 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 -p '' checkout
+notmuch git -C tags.git checkout --force
 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 +126,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 --force
 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,14 +160,148 @@ 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
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 
+test_begin_subtest "environment passed through when run as 'notmuch git'"
+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 NOTMUCH_GIT_DIR = foo
+env NOTMUCH_GIT_PREFIX = bar
+env NOTMUCH_PROFILE = default
+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_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
+prefix = foo::
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+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
+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_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


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

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

So far this is just a rename. Documentation and tests to follow.
---
 Makefile.local                      | 7 +++++--
 devel/nmbug/nmbug => notmuch-git.in | 2 +-
 2 files changed, 6 insertions(+), 3 deletions(-)
 rename devel/nmbug/nmbug => notmuch-git.in (99%)

diff --git a/Makefile.local b/Makefile.local
index d8bbf3e1..ca2310f4 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
@@ -294,7 +294,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
+CLEAN := $(CLEAN) .deps notmuch-git
 
 DISTCLEAN := $(DISTCLEAN) .first-build-message Makefile.config sh.config sphinx.config
 
@@ -307,6 +307,9 @@ cppcheck:
 	@echo "No cppcheck found during configure; skipping static checking"
 endif
 
+notmuch-git: notmuch-git.in
+	sed s/@NOTMUCH_VERSION@/${VERSION}/ < notmuch-git.in > notmuch-git
+	chmod ugo+rx notmuch-git
 
 DEPS := $(SRCS:%.c=.deps/%.d)
 DEPS := $(DEPS:%.cc=.deps/%.d)
diff --git a/devel/nmbug/nmbug b/notmuch-git.in
similarity index 99%
rename from devel/nmbug/nmbug
rename to notmuch-git.in
index 043c1863..dd30b5be 100755
--- a/devel/nmbug/nmbug
+++ b/notmuch-git.in
@@ -51,7 +51,7 @@ except ImportError:  # Python 2
     from urllib import unquote as _unquote
 
 
-__version__ = '0.3'
+__version__ = '@NOTMUCH_VERSION@'
 
 _LOG = _logging.getLogger('nmbug')
 _LOG.setLevel(_logging.WARNING)
-- 
2.35.2

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

* [PATCH 02/17] CLI/git: drop support for python < 3.2
  2022-05-15 18:14 notmuch-git David Bremner
  2022-05-15 18:14 ` [PATCH 01/17] nmbug: promote to user tool "notmuch-git" David Bremner
@ 2022-05-15 18:14 ` David Bremner
  2022-05-15 18:14 ` [PATCH 03/17] notmuch-git: add --git-dir, --tag-prefix arguments David Bremner
                   ` (14 subsequent siblings)
  16 siblings, 0 replies; 18+ messages in thread
From: David Bremner @ 2022-05-15 18:14 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.in | 34 ++--------------------------------
 1 file changed, 2 insertions(+), 32 deletions(-)

diff --git a/notmuch-git.in b/notmuch-git.in
index dd30b5be..5d6e29f0 100755
--- a/notmuch-git.in
+++ b/notmuch-git.in
@@ -43,13 +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
 
 __version__ = '@NOTMUCH_VERSION@'
 
@@ -71,31 +66,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 related	[flat|nested] 18+ messages in thread

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

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

diff --git a/notmuch-git.in b/notmuch-git.in
index 5d6e29f0..4b94046d 100755
--- a/notmuch-git.in
+++ b/notmuch-git.in
@@ -52,13 +52,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>[^/]*)')
@@ -79,10 +75,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.
@@ -688,6 +680,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(
         '-v', '--version', action='version',
         version='%(prog)s {}'.format(__version__))
@@ -800,6 +799,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 related	[flat|nested] 18+ messages in thread

* [PATCH 04/17] CLI/git: make existance of config branch optional on clone
  2022-05-15 18:14 notmuch-git David Bremner
                   ` (2 preceding siblings ...)
  2022-05-15 18:14 ` [PATCH 03/17] notmuch-git: add --git-dir, --tag-prefix arguments David Bremner
@ 2022-05-15 18:14 ` David Bremner
  2022-05-15 18:14 ` [PATCH 05/17] CLI/git: Add an 'init' command David Bremner
                   ` (12 subsequent siblings)
  16 siblings, 0 replies; 18+ messages in thread
From: David Bremner @ 2022-05-15 18:14 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.in | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/notmuch-git.in b/notmuch-git.in
index 4b94046d..57b5a0ec 100755
--- a/notmuch-git.in
+++ b/notmuch-git.in
@@ -273,7 +273,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 related	[flat|nested] 18+ messages in thread

* [PATCH 05/17] CLI/git: Add an 'init' command
  2022-05-15 18:14 notmuch-git David Bremner
                   ` (3 preceding siblings ...)
  2022-05-15 18:14 ` [PATCH 04/17] CLI/git: make existance of config branch optional on clone David Bremner
@ 2022-05-15 18:14 ` David Bremner
  2022-05-15 18:14 ` [PATCH 06/17] test: initial tests for notmuch-git David Bremner
                   ` (11 subsequent siblings)
  16 siblings, 0 replies; 18+ messages in thread
From: David Bremner @ 2022-05-15 18:14 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.in | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/notmuch-git.in b/notmuch-git.in
index 57b5a0ec..71a4df72 100755
--- a/notmuch-git.in
+++ b/notmuch-git.in
@@ -353,6 +353,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.
@@ -716,6 +735,7 @@ if __name__ == '__main__':
             'commit',
             'fetch',
             'help',
+            'init',
             'log',
             'merge',
             'pull',
-- 
2.35.2

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

* [PATCH 06/17] test: initial tests for notmuch-git
  2022-05-15 18:14 notmuch-git David Bremner
                   ` (4 preceding siblings ...)
  2022-05-15 18:14 ` [PATCH 05/17] CLI/git: Add an 'init' command David Bremner
@ 2022-05-15 18:14 ` David Bremner
  2022-05-15 18:14 ` [PATCH 07/17] CLI/git: rename environment variables David Bremner
                   ` (10 subsequent siblings)
  16 siblings, 0 replies; 18+ messages in thread
From: David Bremner @ 2022-05-15 18:14 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.in   |  4 +++
 test/T850-git.sh | 93 ++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 97 insertions(+)
 create mode 100755 test/T850-git.sh

diff --git a/notmuch-git.in b/notmuch-git.in
index 71a4df72..3a16914d 100755
--- a/notmuch-git.in
+++ b/notmuch-git.in
@@ -841,6 +841,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 related	[flat|nested] 18+ messages in thread

* [PATCH 07/17] CLI/git: rename environment variables.
  2022-05-15 18:14 notmuch-git David Bremner
                   ` (5 preceding siblings ...)
  2022-05-15 18:14 ` [PATCH 06/17] test: initial tests for notmuch-git David Bremner
@ 2022-05-15 18:14 ` David Bremner
  2022-05-15 18:14 ` [PATCH 08/17] CLI/git: suppress warnings about initial branch name David Bremner
                   ` (9 subsequent siblings)
  16 siblings, 0 replies; 18+ messages in thread
From: David Bremner @ 2022-05-15 18:14 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.in   | 35 ++++++++++++++---------------------
 test/T850-git.sh |  6 +++---
 2 files changed, 17 insertions(+), 24 deletions(-)

diff --git a/notmuch-git.in b/notmuch-git.in
index 3a16914d..f27591c9 100755
--- a/notmuch-git.in
+++ b/notmuch-git.in
@@ -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
@@ -52,7 +45,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}')
@@ -200,7 +193,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)
 
 
@@ -268,7 +261,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))
@@ -360,7 +353,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)
@@ -368,7 +361,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)
 
 
@@ -591,7 +584,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(
@@ -710,7 +703,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(
         '-v', '--version', action='version',
@@ -826,13 +819,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 ':'
@@ -842,7 +835,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 related	[flat|nested] 18+ messages in thread

* [PATCH 08/17] CLI/git: suppress warnings about initial branch name
  2022-05-15 18:14 notmuch-git David Bremner
                   ` (6 preceding siblings ...)
  2022-05-15 18:14 ` [PATCH 07/17] CLI/git: rename environment variables David Bremner
@ 2022-05-15 18:14 ` David Bremner
  2022-05-15 18:14 ` [PATCH 09/17] test/git: add known broken test for tag with quotes David Bremner
                   ` (8 subsequent siblings)
  16 siblings, 0 replies; 18+ messages in thread
From: David Bremner @ 2022-05-15 18:14 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.in | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/notmuch-git.in b/notmuch-git.in
index f27591c9..78e4b140 100755
--- a/notmuch-git.in
+++ b/notmuch-git.in
@@ -353,7 +353,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 related	[flat|nested] 18+ messages in thread

* [PATCH 09/17] test/git: add known broken test for tag with quotes.
  2022-05-15 18:14 notmuch-git David Bremner
                   ` (7 preceding siblings ...)
  2022-05-15 18:14 ` [PATCH 08/17] CLI/git: suppress warnings about initial branch name David Bremner
@ 2022-05-15 18:14 ` David Bremner
  2022-05-15 18:14 ` [PATCH 10/17] CLI/git: replace enumeration of tags with sexp query David Bremner
                   ` (7 subsequent siblings)
  16 siblings, 0 replies; 18+ messages in thread
From: David Bremner @ 2022-05-15 18:14 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 related	[flat|nested] 18+ messages in thread

* [PATCH 10/17] CLI/git: replace enumeration of tags with sexp query.
  2022-05-15 18:14 notmuch-git David Bremner
                   ` (8 preceding siblings ...)
  2022-05-15 18:14 ` [PATCH 09/17] test/git: add known broken test for tag with quotes David Bremner
@ 2022-05-15 18:14 ` David Bremner
  2022-05-15 18:14 ` [PATCH 11/17] CLI/git: add @timed decorator, time a few functions David Bremner
                   ` (6 subsequent siblings)
  16 siblings, 0 replies; 18+ messages in thread
From: David Bremner @ 2022-05-15 18:14 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.in   | 26 +++++++++++++++++++-------
 test/T850-git.sh |  6 +++++-
 2 files changed, 24 insertions(+), 8 deletions(-)

diff --git a/notmuch-git.in b/notmuch-git.in
index 78e4b140..1d2dcd53 100755
--- a/notmuch-git.in
+++ b/notmuch-git.in
@@ -221,16 +221,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=()):
     """
@@ -586,13 +587,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'],
@@ -692,6 +692,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
@@ -839,6 +847,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 related	[flat|nested] 18+ messages in thread

* [PATCH 11/17] CLI/git: add @timed decorator, time a few functions
  2022-05-15 18:14 notmuch-git David Bremner
                   ` (9 preceding siblings ...)
  2022-05-15 18:14 ` [PATCH 10/17] CLI/git: replace enumeration of tags with sexp query David Bremner
@ 2022-05-15 18:14 ` David Bremner
  2022-05-15 18:14 ` [PATCH 12/17] CLI/git: cache git indices David Bremner
                   ` (5 subsequent siblings)
  16 siblings, 0 replies; 18+ messages in thread
From: David Bremner @ 2022-05-15 18:14 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.in | 19 ++++++++++++++++++-
 1 file changed, 18 insertions(+), 1 deletion(-)

diff --git a/notmuch-git.in b/notmuch-git.in
index 1d2dcd53..0aabfb60 100755
--- a/notmuch-git.in
+++ b/notmuch-git.in
@@ -91,6 +91,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):
@@ -323,6 +337,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'],
@@ -563,6 +578,7 @@ def _is_unmerged(ref='@{upstream}'):
     return base != fetch_head
 
 
+@timed
 def get_status():
     status = {
         'deleted': {},
@@ -583,7 +599,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')
@@ -632,6 +648,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 related	[flat|nested] 18+ messages in thread

* [PATCH 12/17] CLI/git: cache git indices
  2022-05-15 18:14 notmuch-git David Bremner
                   ` (10 preceding siblings ...)
  2022-05-15 18:14 ` [PATCH 11/17] CLI/git: add @timed decorator, time a few functions David Bremner
@ 2022-05-15 18:14 ` David Bremner
  2022-05-15 18:14 ` [PATCH 13/17] doc/notmuch-git: initial documentation David Bremner
                   ` (4 subsequent siblings)
  16 siblings, 0 replies; 18+ messages in thread
From: David Bremner @ 2022-05-15 18:14 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.in   | 318 ++++++++++++++++++++++++++++++++++-------------
 test/T850-git.sh |  41 ++++++
 2 files changed, 274 insertions(+), 85 deletions(-)

diff --git a/notmuch-git.in b/notmuch-git.in
index 0aabfb60..709d3d07 100755
--- a/notmuch-git.in
+++ b/notmuch-git.in
@@ -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
 
 __version__ = '@NOTMUCH_VERSION@'
 
@@ -301,41 +302,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):
@@ -584,50 +642,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):
     """
@@ -648,26 +816,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 related	[flat|nested] 18+ messages in thread

* [PATCH 13/17] doc/notmuch-git: initial documentation
  2022-05-15 18:14 notmuch-git David Bremner
                   ` (11 preceding siblings ...)
  2022-05-15 18:14 ` [PATCH 12/17] CLI/git: cache git indices David Bremner
@ 2022-05-15 18:14 ` David Bremner
  2022-05-15 18:14 ` [PATCH 14/17] CLI/git: change defaults for repo and prefix David Bremner
                   ` (3 subsequent siblings)
  16 siblings, 0 replies; 18+ messages in thread
From: David Bremner @ 2022-05-15 18:14 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.

Define notmuch-git as a known external command.  This will allow
"notmuch help git" to invoke man notmuch-git, rather than erroring
out.
---
 doc/conf.py              |   4 +
 doc/index.rst            |   1 +
 doc/man1/notmuch-git.rst | 271 +++++++++++++++++++++++++++++++++++++++
 notmuch.c                |   2 +
 4 files changed, 278 insertions(+)
 create mode 100644 doc/man1/notmuch-git.rst

diff --git a/doc/conf.py b/doc/conf.py
index 6afeac06..8cc957b5 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)`
diff --git a/notmuch.c b/notmuch.c
index ac25ae18..8c53fb80 100644
--- a/notmuch.c
+++ b/notmuch.c
@@ -201,6 +201,8 @@ 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." }
 };
-- 
2.35.2

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

* [PATCH 14/17] CLI/git: change defaults for repo and prefix
  2022-05-15 18:14 notmuch-git David Bremner
                   ` (12 preceding siblings ...)
  2022-05-15 18:14 ` [PATCH 13/17] doc/notmuch-git: initial documentation David Bremner
@ 2022-05-15 18:14 ` David Bremner
  2022-05-15 18:14 ` [PATCH 15/17] CLI/git: support configuration for repo location / prefix David Bremner
                   ` (2 subsequent siblings)
  16 siblings, 0 replies; 18+ messages in thread
From: David Bremner @ 2022-05-15 18:14 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           |   5 +-
 doc/Makefile.local       |   1 +
 doc/man1/notmuch-git.rst |  21 +++++++-
 notmuch-git.in           |  62 +++++++++++++++++++----
 test/T850-git.sh         | 103 +++++++++++++++++++++++++++++++--------
 5 files changed, 160 insertions(+), 32 deletions(-)

diff --git a/Makefile.local b/Makefile.local
index ca2310f4..0fadfb26 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
@@ -307,9 +307,10 @@ cppcheck:
 	@echo "No cppcheck found during configure; skipping static checking"
 endif
 
-notmuch-git: notmuch-git.in
+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/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.in b/notmuch-git.in
index 709d3d07..35820eb2 100755
--- a/notmuch-git.in
+++ b/notmuch-git.in
@@ -427,6 +427,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)
@@ -863,9 +870,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
 
@@ -877,8 +894,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(
         '-v', '--version', action='version',
         version='%(prog)s {}'.format(__version__))
@@ -992,16 +1012,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:
@@ -1014,12 +1054,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 related	[flat|nested] 18+ messages in thread

* [PATCH 15/17] CLI/git: support configuration for repo location / prefix
  2022-05-15 18:14 notmuch-git David Bremner
                   ` (13 preceding siblings ...)
  2022-05-15 18:14 ` [PATCH 14/17] CLI/git: change defaults for repo and prefix David Bremner
@ 2022-05-15 18:14 ` David Bremner
  2022-05-15 18:14 ` [PATCH 16/17] CLI/git: add safety checks for checkout and commit David Bremner
  2022-05-15 18:14 ` [PATCH 17/17] debian: install notmuch-git David Bremner
  16 siblings, 0 replies; 18+ messages in thread
From: David Bremner @ 2022-05-15 18:14 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.
---
 doc/man1/notmuch-config.rst |  8 +++++
 doc/man1/notmuch-git.rst    |  4 +++
 notmuch-git.in              |  6 ++--
 test/T850-git.sh            | 67 ++++++++++++++++++++++++++++++++++++-
 4 files changed, 82 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.in b/notmuch-git.in
index 35820eb2..3e5205e8 100755
--- a/notmuch-git.in
+++ b/notmuch-git.in
@@ -1024,7 +1024,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))
 
@@ -1038,7 +1040,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..8f91b612 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"
 
@@ -175,6 +182,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 +215,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 +254,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 related	[flat|nested] 18+ messages in thread

* [PATCH 16/17] CLI/git: add safety checks for checkout and commit
  2022-05-15 18:14 notmuch-git David Bremner
                   ` (14 preceding siblings ...)
  2022-05-15 18:14 ` [PATCH 15/17] CLI/git: support configuration for repo location / prefix David Bremner
@ 2022-05-15 18:14 ` David Bremner
  2022-05-15 18:14 ` [PATCH 17/17] debian: install notmuch-git David Bremner
  16 siblings, 0 replies; 18+ messages in thread
From: David Bremner @ 2022-05-15 18:14 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.in              | 43 +++++++++++++++++++++++++++++++++++--
 test/T850-git.sh            | 39 ++++++++++++++++++++++++++++-----
 4 files changed, 94 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.in b/notmuch-git.in
index 3e5205e8..6505c2e5 100755
--- a/notmuch-git.in
+++ b/notmuch-git.in
@@ -241,6 +241,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(
@@ -359,7 +369,22 @@ 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)
+    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.
     """
@@ -370,6 +395,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)
@@ -447,7 +475,7 @@ def init(remote=None):
         wait=True)
 
 
-def checkout():
+def checkout(force=None):
     """
     Update the notmuch database from Git.
 
@@ -455,6 +483,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():
@@ -948,6 +980,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',
@@ -956,6 +992,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 8f91b612..508615e1 100755
--- a/test/T850-git.sh
+++ b/test/T850-git.sh
@@ -25,12 +25,25 @@ 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 "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 +77,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 +94,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
 
@@ -111,7 +140,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 related	[flat|nested] 18+ messages in thread

* [PATCH 17/17] debian: install notmuch-git
  2022-05-15 18:14 notmuch-git David Bremner
                   ` (15 preceding siblings ...)
  2022-05-15 18:14 ` [PATCH 16/17] CLI/git: add safety checks for checkout and commit David Bremner
@ 2022-05-15 18:14 ` David Bremner
  16 siblings, 0 replies; 18+ messages in thread
From: David Bremner @ 2022-05-15 18:14 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 related	[flat|nested] 18+ messages in thread

end of thread, other threads:[~2022-05-15 18:24 UTC | newest]

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

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

	https://yhetil.org/notmuch.git/

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).