.. -*- coding: utf-8 -*- .. role:: sref(numref) .. role:: xref(numref) .. (abbrevx-single-replace "emacs-devel" "::ide::") .. Copyright (C) 2020, Wolfgang Scherer, .. .. This file is part of Emacs Development. .. .. Permission is granted to copy, distribute and/or modify this document .. under the terms of the GNU Free Documentation License, Version 1.3 .. or any later version published by the Free Software Foundation; .. with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. .. A copy of the license is included in the section entitled "GNU .. Free Documentation License". .. inline comments (with du_comment_role) .. role:: rem(comment) .. role:: html(raw) :format: html .. role:: shx(code) :language: sh .. role:: elisp(code) :language: elisp .. role:: defun(func) ################################################## :rem:`|||:sec:|||`\ Emacs vc ignore feature ################################################## .. >>CODD See `the components of a doctoral dissertation and their order `_ .. >>CODD Dedication .. >>CODD Epigraph .. >>CODD Abstract .. compound:: .. \|:here:| .. >>CODD Introduction .. \|:here:| ================================================== :rem:`|||:sec:|||`\ Concepts ================================================== .. glossary:: Path separator A single file system specific character which separates elements of a |file path|. For most Unix file systems, the path separator is a slash :file:`/`. For FAT and NTFS the path separator is a backslash :file:`\\`. .. |path separator| replace:: :term:`path separator` File path A file path describes the location of a file in a file system. A file path consists of several path elements separated by a |path separator|, e.g.: .. code-block:: text /path-element/path-element/path-element A file path can be relative or absolute. Generally, a file path starting with a |path separator| is considered absolute. Under Windows, an absolute file path can also start with a drive letter followed by a |path separator|, e.g. :file:`C:\\`. Cygwin maps drive letters to the directory :file:`/cygdrive` (e.g., :file:`C:` -> :file:`/cygdrive/c`) to allow the same criteria for absolute file pathes as under Unix. .. |file path| replace:: :term:`file path` .. |file pathes| replace:: :term:`file pathes ` Basename Last path element of a |file path|. In a shell environment, empty elements of |file pathes| are ignored: .. code-block:: sh >>> basename "some/" some In Emacs Lisp, empty elements are **not** ignored: .. code-block:: sh >>> (file-name-nondirectory "some/") "" There is a function :defun:`file-name-as-directory`, which adds a slash to a |file path|. .. |basename| replace:: :term:`basename` Directory name Partial |file path| before the |basename| (last path element) of a |file path|. Note, that a final directory seprator is included, which is ignored in a Unix shell, but not in Emacs Lisp. .. |directory name| replace:: :term:`directory name` Per-tree/per-directory The difference between per-tree and per-directory regarding ignore specifications is the possible scope of patterns. Per-tree ignore specifications can start anywhere in the current directory or a sub directory and end in any further sub directory. In order to match all possible |file pathes|, per-tree patterns must have an anchor mechanism. Per-directoy ignore patterns only match files in the current directory and therefore do not need an anchor mechanism. +---------------+---------------------------------+--------------------------------+---------------+ | | per-tree unanchored | per-tree anchored | per-directory | +===============+=================================+================================+===============+ | pattern scope | starting in cur dir or sub dir | starting in cur dir extending | cur dir | | | extending into further sub dirs | into sub dirs | | +---------------+---------------------------------+--------------------------------+---------------+ .. |per-tree| replace:: :term:`per-tree/per-directory` .. |per-directory| replace:: :term:`per-tree/per-directory` Escape mechanism In order to unambiguously match |file pathes| that contain characters with special meaning in a |pattern| syntax, it is necessary that the |pattern| syntax has an escape mechanism that strips the special meaning from charatcters. Note that some special |pattern| characters (for both glob(7) and regex(7)) are also valid filename characters in some file systems (``[^$.`` for NTFS, ``?*[^$.\`` for Unix). .. |escape mechanism| replace:: :term:`escape mechanism` Anchor mechanism For per-tree ignore files it is necessary that a file |pattern| can be anchored at the beginning of the start directory and at the end of a filename. E.g., fully anchored Hg regex: :samp:`^some-dir/some-file$`, Git glob: :samp:`/some-dir/some-file`. Left anchored Hg regex :samp:`^some-dir/`, Git glob: :samp:`/some-dir/**`. .. |anchor mechanism| replace:: :term:`anchor mechanism` Pattern A string matching one or more |file pathes|. A pattern is constructed according to the rules of a backend specific syntax. See also |wildcard specification|, |regex pattern|. .. |pattern| replace:: :term:`pattern` .. |patterns| replace:: :term:`patterns ` Regex pattern A |pattern| following the rules of a regular expression syntax variant, e.g. regex(7), Emacs, Perl-comaptible, Python. .. |regex pattern| replace:: :term:`regex pattern` .. |regex| replace:: :term:`regex ` Wildcard specification A |pattern| following the rules of a glob(7) syntax variant. .. |wildcard specification| replace:: :term:`wildcard specification` .. |glob| replace:: :term:`glob ` .. \|:here:| .. >>CODD Chapter ========================================================= :rem:`|||:sec:|||`\ :defun:`vc-ignore` API change request ========================================================= The status quo before Emacs 27 is: 1. Not a single :defun:`vc--ignore` implementation works correctly for ignoring files. I.e., since the current API is faulty, there is really nothing to preserve. 2. :defun:`vc-dir-ignore` calls :defun:`vc-ignore` with either an absolute or relative filename. 3. The description of :defun:`vc-ignore` (see :sref:`sec:Appendix - Current version of vc-ignore`) treats |file pathes| and file patterns as equivalent, which is strictly wrong. .. code-block:: elisp (defun vc-ignore (file &optional directory remove) "Ignore FILE under the VCS of DIRECTORY. Normally, FILE is a wildcard specification that matches the files to be ignored. When REMOVE is non-nil, remove FILE from the list of ignored files. [...]" 4. Some :defun:`vc--ignore` implementations require a |file path| and some require a pattern to work correctly (see :sref:`sec:Initial revision of vc-ignore`). +------------------------+-------------+-----------+ | function | |file path| | pattern | +========================+=============+===========+ | :defun:`vc-ignore` | strong hint | yes | +------------------------+-------------+-----------+ | :defun:`vc-dir-ignore` | mandatory | no | +------------------------+-------------+-----------+ | :defun:`vc-cvs-ignore` | no | mandatory | +------------------------+-------------+-----------+ | :defun:`vc-svn-ignore` | mandatory | no | +------------------------+-------------+-----------+ | :defun:`vc-src-ignore` | -- | -- | +------------------------+-------------+-----------+ | :defun:`vc-bzr-ignore` | no | mandatory | +------------------------+-------------+-----------+ | :defun:`vc-git-ignore` | no | mandatory | +------------------------+-------------+-----------+ | :defun:`vc-hg-ignore` | no | mandatory | +------------------------+-------------+-----------+ | :defun:`vc-mtn-ignore` | -- | -- | +------------------------+-------------+-----------+ -------------------------------------------------- :rem:`||:sec:||`\ |file pathes| are not |patterns| -------------------------------------------------- Assuming that a |file path| is always a |pattern| that matches the |file path| unambiguously is simply wrong. When ignoring a specific |file path| and nothing but that |file path|, a |pattern| must be constructed that matches the |file path| exactly. The syntax for this |pattern| is backend specific and the |pattern| is generally not the |file path| itself. In other words: it is an exception that a |file path| and the exactly matching |pattern| are identical strings. Specifically, if a |file path| contains a character with special meaning in the |pattern| syntax, then the matching |pattern| **cannot** be identical to the |file path| since the special character must be escaped. The function that maps a |file path| to a matching |pattern| is also generally not bijective, because there are always many |patterns| matching a single |file path|. Generally, a |file path| must be properly :term:`escaped ` to obtain a |pattern| for an unambiguous match. In a |per-tree| scope, the |file path| must also be properly :term:`anchored `. +-----------------+---------------------+-------------------+---------------------+------------------------+ | |file path| | |glob|\ (7) | anchored |glob| | Hg |regex| | Bzr |regex| | +=================+=====================+===================+=====================+========================+ | ``test[56].xx`` | | ``test\[56].xx`` | ``/test\[56].xx`` | ``^test\[56]\.xx$`` | ``RE:^test\[56]\.xx$`` | | | | ``test[[]56].xx`` | | | | +-----------------+---------------------+-------------------+---------------------+------------------------+ | ``simple.txt`` | | ``simple.txt`` | ``/simple.txt`` | ``^simple\.txt$`` | ``RE:^simple\.txt$`` | | | | ``simple[.]txt`` | | | | +-----------------+---------------------+-------------------+---------------------+------------------------+ .. +-----------------+---------------------+-------------------+---------------------+------------------------+ | `file path` | `glob`\ (7) | anchored `glob` | Hg `regex` | Bzr `regex` | +=================+=====================+===================+=====================+========================+ | ``test[56].xx`` | . ``test\[56].xx`` | ``/test\[56].xx`` | ``^test\[56]\.xx$`` | ``RE:^test\[56]\.xx$`` | | | . ``test[[]56].xx`` | | | | +-----------------+---------------------+-------------------+---------------------+------------------------+ | ``simple.txt`` | . ``simple.txt`` | ``/simple.txt`` | ``^simple\.txt$`` | ``RE:^simple\.txt$`` | | | . ``simple[.]txt`` | | | | +-----------------+---------------------+-------------------+---------------------+------------------------+ The correct escaping of FILE can only be determined by the backend. Therefore neither :defun:`vc-dir-ignore` nor lisp code calling :defun:`vc-ignore` can escape the FILE parameter correctly without support from the backend. This makes pattern input for FILE only useful during interactive calls. It is not generally possible to determine from the FILE parameter, whether it is a pattern or a plain filename. -------------------------------------------------- :rem:`||:sec:||`\ Use cases -------------------------------------------------- 1. A user ignoring files in `vc-dir-mode` or `dired mode` expects that the exact |file path| is excluded without other files also being ignored as a side effect. 2. A user ignoring files in `vc-dir-mode` or `dired-mode` expects that either the marked files or the current file are ignored (similar to :defun:`vc-register`). Contrary to other :mod:`vc` commands, the current file should not be automatically ignored outside `vc-dir-mode` and `dired-mode`. 3. A user entering a |file path| at the :defun:`vc-ignore` interactive prompt expects that the exact |file path| is excluded unambiguously. 4. A user entering a pattern at the :defun:`vc-ignore` interactive prompt expects that no modifications are made to the pattern. Example use cases for CVS and Hg: | VC: CVS (with latest fix) | pattern syntax: glob | default directory: /r/e/po/ +-----------------------------+------------------------+-----------+---------------------------+------------+-------------+ | :defun:`vc-ignore` use case | FILE | DIRECTORY | ignore file for DIRECTORY | req. entry | impl. good? | +=============================+========================+===========+===========================+============+=============+ | pattern | fil?.ext | nil | /r/e/po/.cvsignore | fil?.ext | yes | +-----------------------------+------------------------+-----------+---------------------------+------------+-------------+ | file: base w/wc | fil?.ext | nil | /r/e/po/.cvsignore | fil\\?.ext | no | +-----------------------------+------------------------+-----------+---------------------------+------------+-------------+ | file: base | file.ext | su/bd | /r/e/po/su/bd/.cvsignore | file.ext | yes | +-----------------------------+------------------------+-----------+---------------------------+------------+-------------+ | file: relative w/wc | su/bd/fi?e.ext | su/bd | /r/e/po/su/bd/.cvsignore | fi\\?e.ext | no | +-----------------------------+------------------------+-----------+---------------------------+------------+-------------+ | file: relative | su/bd/file.ext | su/bd | /r/e/po/su/bd/.cvsignore | file.ext | yes | +-----------------------------+------------------------+-----------+---------------------------+------------+-------------+ | file: absolute | /r/e/po/su/bd/file.ext | nil | /r/e/po/su/bd/.cvsignore | file.ext | yes | +-----------------------------+------------------------+-----------+---------------------------+------------+-------------+ | VC: Hg (with vc-hg-ignore hack) | pattern syntax: regexp | default directory: /r/e/po/ +-----------------------------+------------------------+-----------+---------------------------+------------------------+-------------+ | :defun:`vc-ignore` use case | FILE | DIRECTORY | ignore file for DIRECTORY | req. entry | impl. good? | +=============================+========================+===========+===========================+========================+=============+ | pattern | fil?.ext | nil | /r/e/po/.hgignore | fil?.ext | no | +-----------------------------+------------------------+-----------+---------------------------+------------------------+-------------+ | file: base w/wc | fil?.ext | nil | /r/e/po/.hgignore | ^fil\\?\\\\.ext$ | yes | +-----------------------------+------------------------+-----------+---------------------------+------------------------+-------------+ | file: base | file.ext | su/bd | /r/e/po/.hgignore | ^su/bd/file\\\\.ext$ | yes | +-----------------------------+------------------------+-----------+---------------------------+------------------------+-------------+ | file: relative w/wc | su/bd/fi?e.ext | su/bd | /r/e/po/.hgignore | ^su/bd/fi\\?e\\\\.ext$ | yes | +-----------------------------+------------------------+-----------+---------------------------+------------------------+-------------+ | file: relative | su/bd/file.ext | su/bd | /r/e/po/.hgignore | ^su/bd/file\\\\.ext$ | yes | +-----------------------------+------------------------+-----------+---------------------------+------------------------+-------------+ | file: absolute | /r/e/po/su/bd/file.ext | nil | /r/e/po/.hgignore | ^su/bd/file\\\\.ext$ | yes | +-----------------------------+------------------------+-----------+---------------------------+------------------------+-------------+ Use cases "pattern" and "file: base w/wc" in the tables above require that :defun:`vc-ignore` produce different output for the same input, which is inherently impossible for deterministic functions. Therefore, the current API of :defun:`vc-ignore` cannot be fully preserved. -------------------------------------------------- :rem:`||:sec:||`\ API change -------------------------------------------------- Function :defun:`vc-ignore` is modified to handle both the "pattern" and "file path" use cases. - The parameter FILE of :defun:`vc-ignore` is renamed to PATTERN-OR-FILE to clarify its dual purpose. - The additional optional parameter `IS-FILE` is introduced to determine how the PATTERN-OR-FILE argument should be treated. If it is nil (the default) the behavior is unchanged. Otherwise, a file path is expected, which is transformed to fit the ignore file location and then escaped according to the VC pattern syntax. - The documenation string of :defun:`vc-ignore` is clarified to avoid possible misconceptions, like a file somehow being equivalent to a wildcard, or that glob(7) patterns are the only type of patterns for version control systems. - For the "file path" use case, users have come to expect that they can mark several file in :defun:`vc-dir-mode` and :defun:`dired-mode` to perform VC operations on that fileset. This is implemented in function :defun:`vc-ignore-fileset`. in :defun:`dired-mode` the user is prompted before making any changes (similar to a delete operation). - The new function :defun:`vc-ignore-pattern` is an alias for :defun:`vc-ignore` to allow for a clear and unambiguous documentation string that does not mix use cases. - The new function :defun:`vc-ignore-file`, when called interactively, either operates on the current fileset in :defun:`vc-dir-mode` and :defun:`dired-mode` or prompts for a file to ignore. - The function :defun:`vc-dir-ignore` is modified to call :defun:`vc-ignore-pattern` interactively. After processing, a message is displayed in the echo area with a hint to press "F" for ignoring files. -------------------------------------------------- :rem:`||:sec:||`\ Proposed keyboard shortcuts -------------------------------------------------- | :kbd:`C-x v F` => :defun:`vc-ignore-file` | :kbd:`C-x v G` => :defun:`vc-ignore-pattern` ; functionality unchanged in :defun:`vc-dir-mode`: | :kbd:`F` => :defun:`vc-ignore-file` | :kbd:`G` => :defun:`vc-dir-ignore` ; functionality was broken -------------------------------------------------- :rem:`||:sec:||`\ User-visible changes -------------------------------------------------- The only user-visible change in the current features is the keyboard shortcut "G" in :defun:`vc-dir-mode`. It does no longer write an invalid absolute file path into the ignore file, but prompts for a pattern instead. An additional hint to press "F" for ignoring files is displayed afterwards. The only new feature is :defun:`vc-ignore-file` for ignoring an unescaped |file path|. -------------------------------------------------- :rem:`||:sec:||`\ Other Problems -------------------------------------------------- For per-tree semantics, it is not trivial to find the correct ignore file. It is not necessarily the one at :defun:`vc-root-dir`, since there could be other ignore files in sub directories. E.g.: .. code-block:: text /repo/.hg /repo/.hgignore /repo/sub-dir/.hgignore /repo/sub-dir/ignore-me But also finding the correct backend and root directory needs some thought. It is not possible to just start at the end of the path. E.g.: .. code-block:: text /repo/.hg /repo/sub-dir/.src /repo/sub-dir/ignore-me .. code-block:: elisp (setq default-directory "/repo/") (setq file "sub-dir/ignore-me") (setq dir "/repo/") (vc-ignore file dir) (setq file (expand-filename file dir)) If the starting directory is unmodified after file name expansion, the backend is Hg .. code-block:: elisp (vc-backend file) ;; Hg If the starting directory is set from the expanded file name, before the backend is determined, the backend is SRC. .. code-block:: elisp (setq default-directory (file-name-directory file)) (vc-backend file) ;; SRC .. \||||:here:|||| ============================================================================ :rem:`|||:sec:|||`\ Implementation of :defun:`vc-ignore` parameter extension ============================================================================ This feature is part of `x-vc-repair.el <_static/x-vc-repair.el>`_, which can be used to upgrade older versions of :mod:`vc`: The actual implementation is in a single function :defun:`vc-ignore`. The use cases "pattern" and "file path" are selected with the flag IS-FILE. For interactive use, there are 2 separate commands :defun:`vc-ignore-file` and :defun:`vc-ignore-pattern`, both of which just call :defun:`vc-ignore` with a different value for the flag IS-FILE. This decision has been made, because the prefix argument as usual choice to specify a flag parameter interactively is already appropriated for the flag REMOVE. An interactive y/n question for each invocation is too annoying. Two separate functions further facilitate keymap definitions. It also has the advantage that only the relevant information for each use case can be presented in the doc string. Granted that typing :kbd:`C-x v F` instead of :kbd:`C-x v G` or :kbd:`F` instead of :kbd:`G` is also a decision, but it does not necessarily have to be made each time, especially if a user only cares about a single use case. That concludes the argument for 2 separate interactive commands as user interface. The implementation consists of a linear call chain with two short code path variations. Besides backend specific implementations for some functions, there are **never** 2 separate functions in the entire chain. .. \|:here:| .. uml:: :caption: :defun:`vc-ignore` call chain @startuml box "UI" #White participant "vc-ignore-\npattern" as VCIP participant "vc-ignore-\nfile" as VCIF end box box "frontend" #WhiteSmoke participant "vc-ignore" as VCI participant "vc-ignore\nfileset" as VCIFS end box box "backend determined from DIRECTORY" #Gainsboro participant "vc-default-\nignore" as VCDI participant "vc-default-\nget-ignore-file-\nand-pattern" as VCDIAP participant "vc-backend-\nignore-param" as VCBIP participant "vc-backend-\nfind-ignore-file" as VCBFIF participant "vc-default-\nmodify-ignores" as VCDMI end box == Ignore a pattern == VCIP -> VCI ++ : PATTERN DIR IS-FILE = nil VCI -> VCDI ++ : PATTERN DIR IS-FILE = nil return return == Ignore a file path == alt not vc-dir-mode or dired-mode VCIF -> VCI ++ : FILE DIR\nIS-FILE = t VCI -> VCDI ++ : FILE DIR IS-FILE = t return return else vc-dir-mode or dired-mode VCIF -> VCIFS ++ : DIR IS-FILE = t VCIFS -> VCIFS : collect fileset alt dired-mode VCIFS -> VCIFS : confirm note right show files and ask for confirmation end note end loop for each file in fileset VCIFS -> VCDI ++ : FILE DIR\nIS-FILE = t return end return end == Common ignore file handling == VCDI -> VCDIAP ++ : (vc-call-backend\n PATTERN-OR-FILE\n DIR IS-FILE REMOVE) note left of VCDIAP (A) File path must be intact (unescaped). end note alt IS-FILE is non-nil VCDIAP -> VCDIAP : absolute FILE-PATH\nnormalize DIR end VCDIAP -> VCBFIF ++ : (vcb DIR) note left (B) No reliable detection of ignore file for file path before this point. Locating the correct ignore file for a pattern is inherently impossible. end note return IGNORE-FILE alt IS-FILE is nil VCDIAP -> VCDIAP : null ignore spec\nprocessing\nparameters else IS-FILE is non-nil VCDIAP -> VCDIAP : relative FILE-PATH VCDIAP -> VCBIP ++ : (vcb IGNORE-FILE) return ignore spec\nprocessing\nparameters end VCDIAP -> VCDIAP : escape/anchor\npattern note left of VCDIAP (C) Only escaped patterns after this point. File path examination does not work anymore. end note return '(PATTERN\nIGNORE-FILE\nREMOVE) VCDI -> VCDMI ++ : (vcb PATTERN IGNORE-FILE REMOVE) return @enduml .. \|:here:| The code path variations based on the IS-FILE flag are confined to :defun:`vc-default-get-ignore-file-and-pattern`. First, if IS-FILE is nil, the PATTERN-OR-FILE argument is expanded to an absolute FILE-PATH. DIRECTORY is set to the immediate parent directory of :elisp:`(vc-no-final-slash FILE-PATH)`. This normalization is necessary, because the search for an ignore file starts at DIRECTORY. .. code-block:: elisp (when is-file (setq pattern-or-file (expand-file-name pattern-or-file directory)) (setq directory (file-name-directory (vc-no-final-slash pattern-or-file)))) Second, if IS-FILE is non-nil, the parameters for escaping and anchoring an ignore pattern are set to an identity function. If IS-FILE is nil, FILE-PATH is made relative to the path of the directory where the pattern will be stored. Parameters for escaping and anchoring an ignore pattern are obtained from the VC backend. .. code-block:: elisp (if (not is-file) (setq ignore-param vc-ignore-param-none) (when (not (string= (substring pattern-or-file 0 (length ignore-dir)) ignore-dir)) (error "Ignore spec %s is not below project root %s" pattern-or-file ignore-dir)) (setq is-dir (or (file-directory-p pattern-or-file) (vc-has-final-slash pattern-or-file))) (setq pattern-or-file (vc-no-final-slash (substring pattern-or-file (length ignore-dir)))) (setq ignore-param (vc-call-backend backend 'ignore-param ignore-file))) E.g. an invocation of :elisp:`(vc-ignore "some/rel/path/" "/re/po")` translates to: .. code-block:: elisp ;; normalize FILE-PATH and DIRECTORY (setq pattern-or-file "/re/po/some/rel/path/" ) (setq directory "/re/po/some/rel/" ) ;; determine, whether file path is a directory (setq is-dir t) ;; prepare pattern as relative file path to directory of ignore file (setq pattern-or-file "path" ) ;; obtain parameters for escaping and anchoring an ignore pattern from VC backend All further processing is identical for verbatim patterns and for file paths. If you insist on calling a code path difference of 3 lines versus 13 lines two independent functions, then technically you are correct and we should just remove the extra 13 lines of code (and also the two separate interactive commands) to get a **properly working** implementation of the pattern use case for **all** VC backends, which is still needed, because right now, the vc ignore feature is incomplete and very buggy. .. \||||:here:|||| A detailed abstraction of ignore parameters is provided in a property list: .. literalinclude:: _static/x-vc-repair.el :language: elisp :linenos: :start-after: ;; .:lst:. start generic ignore :end-before: ;; .:lst:. end generic ignore The user interface is provided in functions :defun:`vc-ignore-pattern` and :defun:`vc-ignore-file`. :defun:`vc-ignore-file` is suitable to replace :defun:`vc-dir-ignore` with a more consistent interface in `vc-dir-mode`. .. literalinclude:: _static/x-vc-repair.el :language: elisp :linenos: :start-after: ;; .:lst:. start ui :end-before: ;; .:lst:. end ui The user interface functions are shallow interactive command wrappers around frontend function :defun::defun:`vc-ignore`, which is no longer interactive. `vc-ignore-fileset` is used to provide a consistent `vc` interface for ignoring files. .. literalinclude:: _static/x-vc-repair.el :language: elisp :linenos: :start-after: ;; .:lst:. start frontend :end-before: ;; .:lst:. end frontend The function :defun:`vc-default-ignore` implements per-tree (and implicitely per-directory) ignore pattern support controlled by a plist of parameters: .. literalinclude:: _static/x-vc-repair.el :language: elisp :linenos: :start-after: ;; .:lst:. start default :end-before: ;; .:lst:. end default The backend specific ignore spec processing for CVS, Bzr, Git, Hg is implemented generically with a set of specialized ignore parameters. For CVS the functions :defun:`vc-cvs-ignore` and :defun:`vc-cvs-append-to-ignore` are obsolete and can therefore be removed: .. literalinclude:: _static/x-vc-repair.el :language: elisp :linenos: :start-after: ;; .:lst:. start cvs ignore :end-before: ;; .:lst:. end cvs ignore Bzr glob wildcards cannot be escaped. Therefore regular expression syntax is used: .. literalinclude:: _static/x-vc-repair.el :language: elisp :linenos: :start-after: ;; .:lst:. start bzr ignore :end-before: ;; .:lst:. end bzr ignore Git only requires the backend function :defun:`vc-git-ignore-param`: .. literalinclude:: _static/x-vc-repair.el :language: elisp :linenos: :start-after: ;; .:lst:. start git ignore :end-before: ;; .:lst:. end git ignore Hg is a little more complex, due to Python regexp quoting and multiple syntax options: .. literalinclude:: _static/x-vc-repair.el :language: elisp :linenos: :start-after: ;; .:lst:. start hg ignore :end-before: ;; .:lst:. end hg ignore The parameter extension can easily be integrated: .. literalinclude:: _static/x-vc-repair.el :language: elisp :linenos: :start-after: ;; .:lst:. start integration :end-before: ;; .:lst:. end integration ================================================== :rem:`|||:sec:|||`\ Repair bugs ================================================== Bugs before Emacs 27 are repaired, so `x-vc-repair.el <_static/x-vc-repair.el>`_ can be used to upgrade older versions of :mod:`vc`: .. literalinclude:: _static/x-vc-repair.el :language: elisp :linenos: :start-after: ;; .:lst:. start repair :end-before: ;; .:lst:. end repair .. \|:here:| .. >>CODD Conclusion .. \|:here:| .. >>CODD Appendix A ====================================================== :rem:`|||:sec:|||`\ Appendix - Version control systems ====================================================== SRC resides at https://gitlab.com/esr/src\ . Clone with: .. code-block:: sh git clone https://gitlab.com/esr/src.git =========================================================== :rem:`|||:sec:|||`\ Appendix - Git globbing on command line =========================================================== Git comes with built-in globbing, which is also recursive. File names containing wildcard characters must therefore be escaped! See also `shell - Escaping characters in glob patterns in Git - Stack Overflow`_. .. _`shell - Escaping characters in glob patterns in Git - Stack Overflow`: https://stackoverflow.com/questions/20992459/escaping-characters-in-glob-patterns-in-git ================================================== :rem:`|||:sec:|||`\ Appendix - Ignore file syntax ================================================== Mostly glob wildcards are used. Mercurial supports regular expressions by default. Glob syntax can be activated with: .. code-block:: ini syntax: glob Mercurial glob syntax is unanchored, which ultimately makes it unfeasible (rootglob: feature is only available in Mercurial 4.9, `mercurial - hg - Ignore directory at root only - Stack Overflow`_). .. _`mercurial - hg - Ignore directory at root only - Stack Overflow`: https://stackoverflow.com/a/55146253/2127439 -------------------------------------------------- :rem:`||:sec:||`\ glob(7) -------------------------------------------------- From the man page of glob(7): Wildcard matching A string is a wildcard pattern if it contains one of the characters '?', '*' or '['. Globbing is the operation that expands a wildcard pattern into the list of pathnames matching the pattern. Matching is defined by: A '?' (not between brackets) matches any single character. A '*' (not between brackets) matches any string, including the empty string. One can remove the special meaning of '?', '*' and '[' by preceding them by a backslash, or, in case this is part of a shell command line, enclosing them in quotes. Between brackets these charac‐ ters stand for themselves. Thus, "[[?*\\]" matches the four characters '[', '?', '*' and '\\'. -------------------------------------------------- :rem:`||:sec:||`\ SRC -------------------------------------------------- Citing the man page of src(1): If your directory contains a file named ".srcignore", each line that is neither blank nor begins with a "#" is interpreted as an ignore pattern. It is expanded with glob(3), and files in the expansion are omitted from src status - unless the file is named as an argument, of the status command, in which case its status is "I". Thus, for example, a line reading "\*.html" will cause all files with an HTML extension to be omitted from the output of "src status", but the output of :samp:`src status *` will list them with status "I". Oddly enough, SRC does not support escaping the wildcards with backslash and backslash is not special itself. However, it is still possible to escape a wildcard pattern with character classes. +----------+---------+ | wildcard | escape | +==========+=========+ | ``?`` | ``[?]`` | +----------+---------+ | ``*`` | ``[*]`` | +----------+---------+ | ``[`` | ``[[]`` | +----------+---------+ -------------------------------------------------- :rem:`||:sec:||`\ CVS -------------------------------------------------- Citing from [VESP]_ section 6.5.3.2 of chapter `6.5 CVSROOT Files `_: The cvsignore file is space-separated, so it is difficult to ignore filenames that include spaces. You can attempt to work around this problem using the pattern-match syntax foo?bar, but that not only matches the file foo bar, it also matches fooxbar and foombar. Unfortunately, there is no perfect solution for ignoring filenames that contain spaces. Since "foo bar" matches only files "foo" and "bar" which have nothing to do with the file to be ignored, using "foo?bar" is the better solution. Citing from [VESP]_ section 11.7.1 of chapter `11.7 Pattern Matching `_: Wildcards are used by most CVS functions, including wrappers and ignore files. The wildcards are evaluated by a version of the fnmatch standard function library distributed with CVS. The wildcards are sh-style, and the symbols used in CVS include: +-------+---------------------------------------------------------------+ | ? | Matches any single character. | +-------+---------------------------------------------------------------+ | \\ | Escapes the special symbols, so they can be used as literals. | +-------+---------------------------------------------------------------+ | \* | Matches any string, including the empty string. | +-------+---------------------------------------------------------------+ | [ ] | Matches any one of the enclosed characters. Within the | | | brackets, the following symbols are used: | | | | | . | ! or ^ | | | If either of these characters is the first character | | | after the open bracket, the brackets match anything | | | that is not included in the brackets | | | | | | char1-char2 | | | Denotes the range of characters between char1 and char2. | +-------+---------------------------------------------------------------+ ================================================== :rem:`|||:sec:|||`\ Per-tree and per-directory ================================================== A search of the `vc` source files for `per-\(dir\|tree\)` gives a hint that the concept of per-directory and per-tree is VCS specific: | vc-hooks.el\0110-(defcustom vc-handled-backends '(RCS CVS SVN SCCS SRC Bzr Git Hg Mtn) | vc-hooks.el\0111: ;; RCS, CVS, SVN, SCCS, and SRC come first because they are per-dir | vc-hooks.el\0112: ;; rather than per-tree. RCS comes first because of the multibackend In regard to ignore file specifications, per-directory means that the file specification is a plain basename glob pattern without directory parts, the scope of the specification is only the corresponding directory, no anchoring is necessary. Per-tree means that the file specification pattern may include directory pathes. Patterns can be anchored or unanchored. Anchored patterns are only applied at the level of the corresponding ignore file. Unanchored patterns are applied in all sub-directories. .. _`sec:Per-subtree ignore files`: -------------------------------------------------- :rem:`||:sec:||`\ Per-subtree ignore files -------------------------------------------------- Git implicitely supports ignore files in sub-trees. See `Git - gitignore Documentation`_: Patterns read from a .gitignore file in the same directory as the path, or in any parent directory, with patterns in the higher level files (up to the toplevel of the work tree) being overridden by those in lower level files down to the directory containing the file. These patterns match relative to the location of the .gitignore file. A project normally includes such .gitignore files in its repository, containing patterns for files generated as part of the project build. Hg supports ignore files in sub-trees explicitly. See `hgignore (5)`_: Subdirectories can have their own .hgignore settings by adding subinclude:path/to/subdir/.hgignore to the root .hgignore. See hg help patterns for details on subinclude: and include:. .. _`Git - gitignore Documentation`: https://git-scm.com/docs/gitignore .. _`hgignore (5)`: https://www.selenic.com/mercurial/hgignore.5.html: ==================================================================== :rem:`|||:sec:|||`\ Appendix - History of per-directory vs. per-tree ==================================================================== Originally, `vc` only supoprted SCCS, RCS, CVS, which all have per-directory semantics (see A. VC Backend History). It is not until 2004-03-15, when Arch is added, that per-tree semantics is mentioned. Starting 2007-07-30 GIT, HG, BZR, Mtn per-tree VCS are added. Finally 2014-11-20 SRC is added to the current list of supported backends: .. code-block:: elisp (defcustom vc-handled-backends '(RCS CVS SVN SCCS SRC Bzr Git Hg Mtn) ;; RCS, CVS, SVN, SCCS, and SRC come first because they are per-dir ;; rather than per-tree. RCS comes first because of the multibackend However, the per-directory paradigm still bleeds through the UI. With changes as late as 2015, when `vc-root` became the default for `vc-dir`. And Bug#39380, which I just discovered. ================================================== :rem:`|||:sec:|||`\ Appendix - VC backend history ================================================== 2015-01-19: (vc-root) instead of default-directory as default for `vc-dir` 2014-12-08: Arch removed -(defcustom vc-handled-backends '(RCS CVS SVN SCCS SRC Bzr Git Hg Mtn Arch) +(defcustom vc-handled-backends '(RCS CVS SVN SCCS SRC Bzr Git Hg Mtn) 2014-11-20: SRC added -(defcustom vc-handled-backends '(RCS CVS SVN SCCS Bzr Git Hg Mtn Arch) - ;; RCS, CVS, SVN and SCCS come first because they are per-dir +(defcustom vc-handled-backends '(RCS CVS SVN SCCS SRC Bzr Git Hg Mtn Arch) + ;; RCS, CVS, SVN, SCCS, and SRC come first because they are per-dir 2008-05-07: MCVS removed -(defcustom vc-handled-backends '(RCS CVS SVN SCCS Bzr Git Hg Mtn Arch MCVS) +(defcustom vc-handled-backends '(RCS CVS SVN SCCS Bzr Git Hg Mtn Arch) 2007-09-14: Mtn added -(defcustom vc-handled-backends '(RCS CVS SVN SCCS Bzr Git Hg Arch MCVS) +(defcustom vc-handled-backends '(RCS CVS SVN SCCS Bzr Git Hg Mtn Arch MCVS) 2007-07-31: BZR added -(defcustom vc-handled-backends '(RCS CVS SVN SCCS GIT HG Arch MCVS) +(defcustom vc-handled-backends '(RCS CVS SVN SCCS GIT HG BZR Arch MCVS) 2007-07-30: GIT, HG added -(defcustom vc-handled-backends '(RCS CVS SVN SCCS Arch MCVS) +(defcustom vc-handled-backends '(RCS CVS SVN SCCS GIT HG Arch MCVS) 2004-03-15: Arch added, first mention of per-tree -(defcustom vc-handled-backends '(RCS CVS SVN MCVS SCCS) +(defcustom vc-handled-backends '(RCS CVS SVN SCCS Arch MCVS) + ;; Arch and MCVS come last because they are per-tree rather than per-dir. 2003-05-07: SVN MCVS added -(defcustom vc-handled-backends '(RCS CVS SCCS) +(defcustom vc-handled-backends '(RCS CVS SVN MCVS SCCS) 2000-09-04: vc-handled-backends introduced +(defcustom vc-handled-backends '(RCS CVS SCCS) 1994 CVS support '(RCS SCCS CVS) 1992 first version of vc.el '(RCS SCCS) .. _`sec:Appendix - Current version of vc-ignore`: ============================================================== :rem:`|||:sec:|||`\ Appendix - Current version of ‘vc-ignore’ ============================================================== .. code-block:: elisp (defun vc-ignore (file &optional directory remove) "Ignore FILE under the VCS of DIRECTORY. Normally, FILE is a wildcard specification that matches the files to be ignored. When REMOVE is non-nil, remove FILE from the list of ignored files. DIRECTORY defaults to `default-directory' and is used to determine the responsible VC backend. When called interactively, prompt for a FILE to ignore, unless a prefix argument is given, in which case prompt for a file FILE to remove from the list of ignored files." (interactive (list (if (not current-prefix-arg) (read-file-name "File to ignore: ") (completing-read "File to remove: " (vc-call-backend (or (vc-responsible-backend default-directory) (error "Unknown backend")) 'ignore-completion-table default-directory))) nil current-prefix-arg)) (let* ((directory (or directory default-directory)) (backend (or (vc-responsible-backend default-directory) (error "Unknown backend")))) (vc-call-backend backend 'ignore file directory remove))) ========================================================================== :rem:`|||:sec:|||`\ Appendix - vc-ignore is a quick and dirty afterthought ========================================================================== Since `vc` started out with RCS and SCCS support only, there was no need for ignore file support. Since `pcl-cvs` was more popular than `vc` for CVS, the ignore file support was obviously never missed enough. The first version of `cvs-ignore` in `vc` appeared in 2013 (see B. Initial revision of vc-ignore). It imported `cvs-append-to-ignore` from `pcl-cvs` and added some quick and dirty backend implementations for other VCS. Especially, ignoring files in `vc-dir-mode` is seriously broken. .. _`sec:Initial revision of vc-ignore`: -------------------------------------------------- :rem:`||:sec:||`\ Initial revision of vc-ignore -------------------------------------------------- When attemptinig to reverse engineer the design of the :mod:`vc` ignore feature, it becomes clear that the misguided interchangeable use of |file path| and ignore pattern is built-in from the get go. +------------------------+-------------+-----------+ | function | |file path| | pattern | +========================+=============+===========+ | :defun:`vc-ignore` | strong hint | yes | +------------------------+-------------+-----------+ | :defun:`vc-dir-ignore` | mandatory | no | +------------------------+-------------+-----------+ | :defun:`vc-cvs-ignore` | no | mandatory | +------------------------+-------------+-----------+ | :defun:`vc-svn-ignore` | mandatory | no | +------------------------+-------------+-----------+ | :defun:`vc-src-ignore` | -- | -- | +------------------------+-------------+-----------+ | :defun:`vc-bzr-ignore` | no | mandatory | +------------------------+-------------+-----------+ | :defun:`vc-git-ignore` | no | mandatory | +------------------------+-------------+-----------+ | :defun:`vc-hg-ignore` | no | mandatory | +------------------------+-------------+-----------+ | :defun:`vc-mtn-ignore` | -- | -- | +------------------------+-------------+-----------+ .. \|:here:| The initial commit of the :mod:`vc` ignore feature happended on 2013-07-30. .. code-block:: text commit 7aa7fff0c8860b72a2c7cdc7d4d0845245754d43 Author: Xue Fuqiao Date: Tue Jul 30 08:25:31 2013 +0800 Add vc-ignore. * lisp/vc/vc.el (vc-ignore): New function. * lisp/vc/vc-svn.el (vc-svn-ignore): New function. * lisp/vc/vc-hg.el (vc-hg-ignore): New function. * lisp/vc/vc-git.el (vc-git-ignore): New function. * lisp/vc/vc-dir.el (vc-dir-mode-map): Add key binding for vc-dir-ignore (vc-dir-ignore): New function. * lisp/vc/vc-cvs.el (vc-cvs-ignore): New function. (cvs-append-to-ignore): Moved from pcvs.el. * lisp/vc/vc-bzr.el (vc-bzr-ignore): New function. * lisp/vc/pcvs.el (vc-cvs): Require 'vc-cvs. .. \|:here:| Using the name FILE for the argument of :defun:`vc-ignore` hints at the intended input format to be a plain file. So does the interactive prompt specification ``"fIgnore file: "``. .. code-block:: elisp (defun vc-ignore (file) "Ignore FILE under the current VCS." (interactive "fIgnore file: ") (let ((backend (vc-backend file))) (vc-call-backend backend 'ignore file))) Passing on the result of :defun:`vc-dir-current-file`, which is an absolute |file path|, enhances the interpretation, that :defun:`vc-ignore` is meant to handle real |file pathes| instead of patterns. .. code-block:: elisp (defun vc-dir-ignore () "Ignore the current file." (interactive) (vc-ignore (vc-dir-current-file))) :defun:`vc-cvs-ignore` is a thin wrapper around :defun:`cvs-append-to-ignore`, which was imported from :mod:`pcvs`. Here the expected parameter STR was never a |file path| but a valid ignore pattern. .. code-block:: elisp (defun vc-cvs-ignore (file) "Ignore FILE under CVS." (interactive) (cvs-append-to-ignore (file-name-directory file) file)) (defun cvs-append-to-ignore (dir str &optional old-dir) "In DIR, add STR to the .cvsignore file. If OLD-DIR is non-nil, then this is a directory that we don't want to hear about anymore." (with-current-buffer (find-file-noselect (expand-file-name ".cvsignore" dir)) (when (ignore-errors (and buffer-read-only (eq 'CVS (vc-backend buffer-file-name)) (not (vc-editable-p buffer-file-name)))) ;; CVSREAD=on special case (vc-checkout buffer-file-name t)) (goto-char (point-max)) (unless (bolp) (insert "\n")) (insert str (if old-dir "/\n" "\n")) (if cvs-sort-ignore-file (sort-lines nil (point-min) (point-max))) (save-buffer))) (define-key map "I" 'vc-dir-ignore) The initial :defun:`vc-svn-ignore` is a placeholder which does not work at all. .. code-block:: elisp (defun vc-svn-ignore (file) "Ignore FILE under Subversion." (interactive) (vc-svn-command (get-buffer-create "*vc-ignore*") 0 file "propedit" "svn:ignore")) The last version: .. code-block:: text commit c799337f12e84b4ca88f509ecea3a7e55ff4c768 Author: Dmitry Gutov Date: Fri Oct 3 17:15:05 2014 +0400 manipulates argument FILE with :defun:`file-relative-name`, which requires FILE to be an actual |file path|. .. code-block:: elisp (defun vc-svn-ignore (file &optional directory remove) "Ignore FILE under Subversion. FILE is a file wildcard, relative to the root directory of DIRECTORY." (let* ((ignores (vc-svn-ignore-completion-table directory)) (file (file-relative-name file directory)) (ignores (if remove (delete file ignores) (push file ignores)))) (vc-svn-command nil 0 nil nil "propset" "svn:ignore" (mapconcat #'identity ignores "\n") (expand-file-name directory)))) SRC does not have explicit ignore file support. Bzr is not equipped to handle |file pathes|. Passing an absolute |file path| to :samp:`bzr ignore` leads to an error: .. code-block:: text bzr: ERROR: NAME_PATTERN should not be an absolute path So :defun:`vc-bzr-ignore` really expects a valid NAME_PATTERN. .. code-block:: elisp (defun vc-bzr-ignore (file) "Ignore FILE under Bazaar." (interactive) (vc-bzr-command "ignore" (get-buffer-create "*vc-ignore*") 0 file)) Although :defun:`vc-git-ignore` states that a FILE will be ignored, only a valid pattern works. .. code-block:: elisp (defun vc-git-ignore (file) "Ignore FILE under Git." (interactive) (with-temp-buffer (insert-file-contents (let (gitignore (concat (file-name-as-directory (vc-git-root default-directory)) ".gitignore")) (unless (search-forward file nil t) (goto-char (point-max)) (insert (concat "\n" file "\n")) (write-region 1 (point-max) gitignore)))))) :defun:`vc-hg-ignore` is a cut and paste of :defun:`vc-git-ignore` (later consolidated into :defun:`vc-default-ignore`) and therefore needs a pattern to work. .. code-block:: elisp (defun vc-hg-ignore (file) "Ignore FILE under Mercurial." (interactive) (with-temp-buffer (insert-file-contents (let (hgignore (concat (file-name-as-directory (vc-hg-root default-directory)) ".hgignore")) (unless (search-forward file nil t) (goto-char (point-max)) (insert (concat "\n" file "\n")) (write-region 1 (point-max) hgignore)))))) Mtn does not have explicit ignore file support. ======================================================================================= :rem:`|||:sec:|||`\ Appendix - My standard use case for ignore files under per-tree VCS ======================================================================================= My most frequent use case is ignoring a couple of marked files in `vc-dir-mode` under Mercurial. I almost never add (or remove) patterns with `M-x vc-ignore`. The following use cases are implemented in :defun:`vc-ignore`, `vc-hg-ignore`: 1. :defun:`vc-dir-ignore` without a prefix argument shall call :defun:`vc-ignore` with the result of (vc-dir-current-file), which is an absolute path name. 2. :defun:`vc-dir-ignore` with a prefix argument shall call :defun:`vc-ignore` for each marked file with the result of (vc-dir-fileinfo->name filearg), which is a path relative to (vc-root). 3. The argument FILE of :defun:`vc-ignore` is either a pattern, an absolute |file path| or a relative |file path|. 4. The argument FILE of `vc-hg-ignore` is either a pattern or a file name. a. If FILE matches the regular expression `vc-hg-ignore-detect-wildcard` ``"[*^$]"``, it is considered a pattern and is written unmodified into the ignore file, b. Otherwise, FILE is expanded to an absolute file name, which is then made relative to the ignore file directory. The relative |file path| is then escaped according to the active ignore syntax and written into the ignore file. Examples (Mercurial repository): * `M-x vc-ignore` in sub directory "below1/below2": "/home/repo/below1/below2/file.name" => "^below1/below2/file\.name$" * `M-x vc-ignore` in sub directory "below1/below2": "file.name" => "^below1/below2/file\.name$" * `M-x vc-ignore` in sub directory "some/where/in": "\.ext$" => "\.ext$" * `G` in `vc-dir-mode` on "below1/below2/file.name": "/home/repo/below1/below2/file.name" => "^below1/below2/file\.name$" * `G` in `vc-dir-mode` marked "below1/below2/file.name": "below1/below2/file.name" => "^below1/below2/file\.name$" ================================================================================ :rem:`|||:sec:|||`\ Appendix - My personal experience with Emacs version control ================================================================================ I have been using `vc` for RCS and `pcl-cvs` (and later `vc`) for CVS since the very beginning in the 90's. `pcl-cvs` already had support for ignoring files (`cvs-mode-ignore`), which I always missed in `vc`. I stopped using CVS in favour of Mercurial for new projects around 2007. Due to the lack of support for Mercurial in `vc`, I used the `dvc` package until recently. The `dvc` package always presents the entire repository tree. Marking some files in the `*xhg-status*` buffer and invoking the ignore function properly escapes the filenames relative to the repository root and puts them into the `.hgignore` file at the repository root. Both, `pcl-cvs` and `dvc` work the same way, with `pcl-cvs` placing the ignored files into the appropriate per-directory ignore files and `dvc` adding them properly escaped to the per-tree ignore file. I recently started using `vc` again for Mercurial and Git. When trying to phase out `dvc` by covering all use cases, I stumbled over the :defun:`vc-ignore` problems. First for Mercurial, then CVS and SVN. .. \|:here:| .. >>CODD Notes .. ================================================== .. :rem:`|||:sec:|||`\ Footnotes .. ================================================== :html:`
` .. \[#] .. include:: doc_defs.inc .. include:: abbrev_defs.inc .. include:: doc_defs_combined.inc .. .. \||<-snap->|| doc_standalone .. include:: doc/doc_defs_secret.inc .. \||<-snap->|| doc_standalone .. \||<-snap->|| not_doc_standalone .. include:: doc_defs_secret.inc .. \||<-snap->|| not_doc_standalone .. _`Wolfgang Scherer`: wolfgang.scherer@gmx.de