5. Emacs vc ignore feature

5.1. Concepts

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 /. For FAT and NTFS the path separator is a backslash \.

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

/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. C:\. Cygwin maps drive letters to the directory /cygdrive (e.g., C: -> /cygdrive/c) to allow the same criteria for absolute file pathes as under Unix.

Basename

Last path element of a file path.

In a shell environment, empty elements of file pathes are ignored:

>>> basename "some/"
some

In Emacs Lisp, empty elements are not ignored:

>>> (file-name-nondirectory "some/")
""

There is a function file-name-as-directory(), which adds a slash to a file path.

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.
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 extending into further sub dirs starting in cur dir extending into sub dirs cur dir
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).

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: ^some-dir/some-file$, Git glob: /some-dir/some-file. Left anchored Hg regex ^some-dir/, Git glob: /some-dir/**.

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.
Regex pattern
A pattern following the rules of a regular expression syntax variant, e.g. regex(7), Emacs, Perl-comaptible, Python.
Wildcard specification
A pattern following the rules of a glob(7) syntax variant.

5.2. vc-ignore() API change request

The status quo before Emacs 27 is:

  1. Not a single vc-<backend>-ignore() implementation works correctly for ignoring files. I.e., since the current API is faulty, there is really nothing to preserve.

  2. vc-dir-ignore() calls vc-ignore() with either an absolute or relative filename.

  3. The description of vc-ignore() (see section 5.11, Appendix - Current version of ‘vc-ignore’) treats file pathes and file patterns as equivalent, which is strictly wrong.

    (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 vc-<backend>-ignore() implementations require a file path and some require a pattern to work correctly (see section 5.12.1, Initial revision of vc-ignore).

    function file path pattern
    vc-ignore() strong hint yes
    vc-dir-ignore() mandatory no
    vc-cvs-ignore() no mandatory
    vc-svn-ignore() mandatory no
    vc-src-ignore()
    vc-bzr-ignore() no mandatory
    vc-git-ignore() no mandatory
    vc-hg-ignore() no mandatory
    vc-mtn-ignore()

5.2.1. 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 escaped to obtain a pattern for an unambiguous match. In a per-tree/per-directory scope, the file path must also be properly anchored.

file path glob(7) anchored glob Hg regex Bzr regex
test[56].xx
test\[56].xx
test[[]56].xx
/test\[56].xx ^test\[56]\.xx$ RE:^test\[56]\.xx$
simple.txt
simple.txt
simple[.]txt
/simple.txt ^simple\.txt$ RE:^simple\.txt$

The correct escaping of FILE can only be determined by the backend. Therefore neither vc-dir-ignore() nor lisp code calling 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.

5.2.2. 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 vc-register()). Contrary to other 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 vc-ignore() interactive prompt expects that the exact file path is excluded unambiguously.
  4. A user entering a pattern at the 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/
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/
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 vc-ignore() produce different output for the same input, which is inherently impossible for deterministic functions. Therefore, the current API of vc-ignore() cannot be fully preserved.

5.2.3. API change

Function vc-ignore() is modified to handle both the “pattern” and “file path” use cases.

  • The parameter FILE of 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 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 vc-dir-mode() and dired-mode() to perform VC operations on that fileset. This is implemented in function vc-ignore-fileset(). in dired-mode() the user is prompted before making any changes (similar to a delete operation).
  • The new function vc-ignore-pattern() is an alias for vc-ignore() to allow for a clear and unambiguous documentation string that does not mix use cases.
  • The new function vc-ignore-file(), when called interactively, either operates on the current fileset in vc-dir-mode() and dired-mode() or prompts for a file to ignore.
  • The function vc-dir-ignore() is modified to call vc-ignore-pattern() interactively. After processing, a message is displayed in the echo area with a hint to press “F” for ignoring files.

5.2.4. Proposed keyboard shortcuts

C-x v F => vc-ignore-file()
C-x v G => vc-ignore-pattern() ; functionality unchanged

in vc-dir-mode():

F => vc-ignore-file()
G => vc-dir-ignore() ; functionality was broken

5.2.5. User-visible changes

The only user-visible change in the current features is the keyboard shortcut “G” in 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 vc-ignore-file() for ignoring an unescaped file path.

5.2.6. Other Problems

For per-tree semantics, it is not trivial to find the correct ignore file. It is not necessarily the one at vc-root-dir(), since there could be other ignore files in sub directories.

E.g.:

/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.:

/repo/.hg
/repo/sub-dir/.src
/repo/sub-dir/ignore-me
(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

(vc-backend file) ;; Hg

If the starting directory is set from the expanded file name, before the backend is determined, the backend is SRC.

(setq default-directory (file-name-directory file))
(vc-backend file) ;; SRC

5.3. Implementation of vc-ignore() parameter extension

This feature is part of x-vc-repair.el, which can be used to upgrade older versions of vc:

The actual implementation is in a single function vc-ignore(). The use cases “pattern” and “file path” are selected with the flag IS-FILE.

For interactive use, there are 2 separate commands vc-ignore-file() and vc-ignore-pattern(), both of which just call 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 C-x v F instead of C-x v G or F instead of 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.

 @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

figure 5.1 vc-ignore() call chain

The code path variations based on the IS-FILE flag are confined to 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 (vc-no-final-slash FILE-PATH). This normalization is necessary, because the search for an ignore file starts at DIRECTORY.

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

(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 (vc-ignore “some/rel/path/” “/re/po”) translates to:

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

A detailed abstraction of ignore parameters is provided in a property list:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
;; --------------------------------------------------
;; |||:sec:||| Generic ignore parameters
;; --------------------------------------------------

(defvar vc-ignore-param-none
  '(:escape: identity :anchor: "" :trailer: "" :dir-trailer: "")
  "Property list of ignore parameters for plain strings.

All properties are optional.

Property :escape: is a function that takes a pattern string as parameter
and returns an escaped pattern (default is ‘identity’).

Property :anchor: is a string that is prepended to the ignore
pattern (default is an empty string).

Property :trailer: is a string that is appended to non-directory
ignore patterns (default is an empty string).

Property :dir-trailer: is a string that is appended to directory
ignore patterns (default is an empty string).")

(defvar vc-ignore-param-glob
  '(:escape: vc-glob-escape :anchor: "" :trailer: "" :dir-trailer: "")
  "Ignore parameters for unanchored glob wildcards.")

(defvar vc-ignore-param-glob-anchored
  '(:escape: vc-glob-escape :anchor: "/" :trailer: "" :dir-trailer: "/")
  "Ignore parameters for anchored glob wildcards.")

(defvar vc-ignore-param-regexp
  '(:escape: regexp-quote :anchor: "^" :trailer: "$" :dir-trailer: "/")
  "Ignore parameters for anchored regular expressions.")

(defun vc-default-ignore-param (_backend &optional _ignore-file)
  "Default ignore parameters for IGNORE-FILE."
  vc-ignore-param-glob)

(defun vc-glob-escape (string)
  "Escape special glob characters in STRING."
  (save-match-data
    (if (string-match "[\\?*[]" string)
        (mapconcat (lambda (c)
                     (pcase c
                       (?\\ "\\\\")
                       (?? "\\?")
                       (?* "\\*")
                       (?\[ "\\[")
                       (_ (char-to-string c))))
                   string "")
      string)))
;; (vc-glob-escape "full[glo]?\\b*")

;; optimized code Python >= v3.7
;; # SPECIAL_CHARS
;; # closing ')', '}' and ']'
;; # '-' (a range in character set)
;; # '&', '~', (extended character set operations)
;; # '#' (comment) and WHITESPACE (ignored) in verbose mode
;; _special_chars_map = {i: '\\' + chr(i) for i in b'()[]{}?*+-|^$\\.&~# \t\n\r\v\f'}

(defvar vc--py-regexp-special-chars
  (mapcar
   (function
    (lambda (c)
      (cons c (concat "\\" (char-to-string c)))))
   "()[]{}?*+-|^$\\.&~# \t\n\r\v\f")
  "Characters that have special meaning in Python regular expressions.")
;; (cdr (assq ?/ vc--py-regexp-special-chars))
;; (cdr (assq ?\( vc--py-regexp-special-chars))

(defun vc-py-regexp-quote (string)
  "Python regexp to match exactly STRING and nothing else.
Ported from Python v3.7"
  (mapconcat
   (function
    (lambda (c)
      (or (cdr (assq c vc--py-regexp-special-chars))
          (char-to-string c))))
   string ""))
;; (insert (format " ;; %S" (vc-py-regexp-quote "abc+.?.\\g'\"hi\030|()"))) ;; "abc\\+\\.\\?\\.\\\\g'\"hi\\|\\(\\)"
;; (insert (format " ;; %S" (regexp-quote       "abc+.?.\\g'\"hi\030|()"))) ;; "abc\\+\\.\\?\\.\\\\g'\"hi|()"

The user interface is provided in functions vc-ignore-pattern() and vc-ignore-file(). vc-ignore-file() is suitable to replace vc-dir-ignore() with a more consistent interface in vc-dir-mode.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
;; --------------------------------------------------
;; |||:sec:||| User Interface
;; --------------------------------------------------

(defalias 'vc-ignore-pattern 'vc-ignore
  "Ignore PATTERN under VCS of DIRECTORY.

DIRECTORY defaults to `default-directory' and is used to
determine the responsible VC backend.

PATTERN is an expression following the rules of the backend
pattern syntax, matching the files to be ignored.  When REMOVE is
non-nil, remove PATTERN from the list of ignored files.

When called interactively, prompt for a PATTERN to ignore, unless
a prefix argument is given, in which case prompt for a PATTERN to
remove. The completion collection contains the currently defined
patterns from the ignore file.")

(defun vc-ignore-file (file &optional directory remove prompt)
  "Ignore FILE under VCS of DIRECTORY.

DIRECTORY defaults to `default-directory' and is used to
determine the responsible VC backend.

If FILE is nil, ‘vc-ignore-fileset’ is called.

Otherwise, FILE is an unescaped file path.  The directory name of
FILE expanded against DIRECTORY is used to determine the ignore
file.  The effective pattern consists of the file path relative
to the directory of the ignore file, properly escaped and
anchored by the VC backend.

The effective pattern is added to the list of ignored files,
unless REMOVE is non-nil, in which case it is removed.

When called interactively and the mode is neither ‘vc-dir-mode’
nor ‘dired-mode’, prompt for a FILE to ignore, unless a prefix
argument is given, in which case prompt for a FILE to remove from
the list of ignored files.

PROMPT is passed on to ‘vc-ignore-fileset’. When called
interatively, PROMPT is set to ‘t’."
  (interactive
   (list
    (unless (or (derived-mode-p 'vc-dir-mode) (derived-mode-p 'dired-mode))
      (read-file-name
       (concat "File to "
               (if (not current-prefix-arg) "ignore" "remove") ": ")))
    nil current-prefix-arg (derived-mode-p 'dired-mode)))
  (if file
      (vc-ignore file directory remove t)
    (vc-ignore-fileset nil remove prompt)))

The user interface functions are shallow interactive command wrappers around frontend function :defun:vc-ignore(), which is no longer interactive. vc-ignore-fileset is used to provide a consistent vc interface for ignoring files.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
;; --------------------------------------------------
;; |||:sec:||| Frontend
;; --------------------------------------------------

(defun vc-ignore (pattern-or-file &optional directory remove is-file)
  "Ignore PATTERN-OR-FILE under VCS of DIRECTORY.

DIRECTORY defaults to `default-directory' and is used to
determine the responsible VC backend.

When REMOVE is non-nil, remove PATTERN-OR-FILE from the list of
ignored files.

If IS-FILE is nil, PATTERN-OR-FILE is considered a pattern that
should not be modified.  DIRECTORY is used to determine the
ignore file.

If IS-FILE is non-nil, PATTERN-OR-FILE is a considered a file
path that must be escaped and anchored.  The directory name of
PATTERN-OR-FILE expanded against DIRECTORY is used to determine
the ignore file.  The effective pattern consists of the file path
relative to the directory of the ignore file, properly escaped
and anchored by the VC backend."
  (interactive
   (let* ((dir default-directory)
          (backend (or (vc-responsible-backend dir)
                       (error "Unknown backend")))
          (is-dired-mode (derived-mode-p 'dired-mode))
          (is-vc-dir-mode (derived-mode-p 'vc-dir-mode))
          (ignore-param
           (if (not (or is-dired-mode is-vc-dir-mode))
               (list nil nil (vc-call-backend backend 'find-ignore-file dir) nil)
             (let* ((cur-file (or
                               (and is-vc-dir-mode (vc-dir-current-file))
                               (and is-dired-mode (car (dired-get-marked-files nil t)))))
                    ;; (vc-default-get-ignore-file-and-pattern 'Hg "" nil t nil)
                    (ip (vc-call-backend backend 'get-ignore-file-and-pattern cur-file dir t nil))
                    (cur-file-rel (file-relative-name cur-file (file-name-directory (cadr ip)))))
               (cons cur-file-rel ip))))
          (default-pattern (cadr ignore-param))
          (ignore-file (nth 2 ignore-param))
          (ignore-completion-table
           (delq nil (append  (list (and default-pattern "") (car ignore-param))
                              (vc-call-backend backend 'ignore-completion-table dir))))
          (remove current-prefix-arg))
     (list
      (completing-read
       (format "%s pattern verbatim %s %s: "
               (if remove "Remove" "Add")
               (if remove "from" "to")
               (file-relative-name ignore-file dir))
       ignore-completion-table nil nil
       default-pattern)
      nil remove nil)))
  (setq directory (or directory default-directory))
  (vc-call-backend (or (vc-responsible-backend directory)
                       (error "Unknown backend"))
                   'ignore pattern-or-file directory remove is-file))

(defvar vc--ignore-fileset-po
  '(("Remove" "Removing" "removed from ignored files" " from ignored files")
    ("Ignore" "Ignoring" "ignored" ""))
  "Alternate message strings for ‘vc-ignore-fileset’.")

(defun vc-ignore-fileset (&optional vc-fileset remove prompt)
  "Ignore file set under a version control system..

If VC-FILESET is not given, it is deduced with
‘vc-deduce-fileset’.

When REMOVE is non-nil, remove the files from the list of ignored
files.

If PROMPT is non-nil, confirm the operation. If the confirmation
is negative, do not perform the ignore operation."
  (let* ((fileset-arg (or vc-fileset (vc-deduce-fileset t t)))
         (backend (car fileset-arg))
         (files (delq nil (nth 1 fileset-arg)))
         (msg-strings (if remove
                          (car vc--ignore-fileset-po)
                        (cadr vc--ignore-fileset-po)))
         (msg (concat "No files " (nth 2 msg-strings))))
    (when (and files
               (or (not prompt)
                   (let ((files (nreverse
                                 (mapcar #'dired-make-relative files))))
                     (dired-mark-pop-up
                      " *Ignored files*" 'ignore files #'y-or-n-p
                      (format "%s %s%s "
                              (car msg-strings)
                              (dired-mark-prompt nil files)
                              (nth 3 msg-strings)
                              )))))
      (setq msg (concat (message "%s %s%s... " (nth 1 msg-strings) files
                                 (nth 3 msg-strings)) "done"))
      (mapc
       (lambda (file)
         (vc-call-backend backend 'ignore file nil remove t)
         (vc-dir-resynch-file file))
       files))
    (when (derived-mode-p 'vc-dir-mode)
      (vc-dir-move-to-goal-column))
    (message msg)))

The function vc-default-ignore() implements per-tree (and implicitely per-directory) ignore pattern support controlled by a plist of parameters:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
;; --------------------------------------------------
;; |||:sec:||| Default
;; --------------------------------------------------

(defun vc-default-ignore (backend pattern-or-file &optional directory remove is-file)
  ;; implements ‘vc-ignore’ generically
  (apply #'vc-call-backend backend 'modify-ignores
         (vc-call-backend backend 'get-ignore-file-and-pattern
                          pattern-or-file directory is-file remove)))

(defun vc-expand-file-name (file &optional directory)
  " Call ‘expand-file-name’ with normalized FILE and DIRECTORY.

Avoids removing the final slash of directories from the
expansion, if FILE does not have a trailing slash."
  (if (or (string= file "")
          (string= file ".")
          (string= file "..")
          (and (>= (length file) 2)
               (or (string= (substring file -2) "/.")
                   (and (>= (length file) 3) (string= (substring file -3) "/..")))))
      (setq file (file-name-as-directory file)))
  (setq file (expand-file-name file directory))
  (if (and (not (vc-has-final-slash file))
           (file-directory-p file))
      (setq file (file-name-as-directory file)))
  file)

;; (insert (format " ;; => %S" (vc-expand-file-name "xx/"    "/some/dir/"))) ;; => "/some/dir/xx/"
;; (insert (format " ;; => %S" (vc-expand-file-name ""       "/some/dir/"))) ;; => "/some/dir/"
;; (insert (format " ;; => %S" (vc-expand-file-name "."      "/some/dir/"))) ;; => "/some/dir/"
;; (insert (format " ;; => %S" (vc-expand-file-name ".."     "/some/dir/"))) ;; => "/some/"
;; (insert (format " ;; => %S" (vc-expand-file-name "zz/./"  "/some/dir/"))) ;; => "/some/dir/zz/"
;; (insert (format " ;; => %S" (vc-expand-file-name "zz/."   "/some/dir/"))) ;; => "/some/dir/zz/"
;; (insert (format " ;; => %S" (vc-expand-file-name "zz/.."  "/some/dir/"))) ;; => "/some/dir/"

;; (insert (format " ;; => %S" (vc-expand-file-name "/usr/local"  "/some/dir/"))) ;; => "/usr/local/"

;; don't worry about the final slash of DIRECTORY, results are identical:
;; (insert (format " ;; => %s" (equal (expand-file-name "xx/" "/some/dir/") (expand-file-name "xx/" "/some/dir")))) ;; => t

;; Here are various effects with ‘expand-file-name’:
;; (insert (format " ;; => %S" (expand-file-name "xx/" "/some/dir/"))) ;; => "/some/dir/xx/"
;; (insert (format " ;; => %S" (expand-file-name "xx"  "/some/dir/"))) ;; => "/some/dir/xx"
;; (insert (format " ;; => %S" (expand-file-name ""    "/some/dir/"))) ;; => "/some/dir"
;; (insert (format " ;; => %S" (expand-file-name "."   "/some/dir/"))) ;; => "/some/dir"
;; (insert (format " ;; => %S" (expand-file-name ".."  "/some/dir/"))) ;; => "/some"
;; (insert (format " ;; => %S" (expand-file-name "/"   "/some/dir/"))) ;; => "/"
;; (insert (format " ;; => %S" (expand-file-name "./"  "/some/dir/"))) ;; => "/some/dir/"
;; (insert (format " ;; => %S" (expand-file-name "../" "/some/dir/"))) ;; => "/some/"

;; and empty string results in "./"
;; (insert (format " ;; => %S" (file-name-as-directory "" ))) ;; => "./"

(defun vc-default-get-ignore-file-and-pattern (backend pattern-or-file &optional directory is-file remove)
  "Determine ignore file and pattern for BACKEND from PATTERN-OR-FILE.
Implements API of ‘vc-ignore’ for PATTERN-OR-FILE, DIRECTORY and IS-FILE.
REMOVE is passed through without evaluation.
Returns (pattern ignore-file remove) suitable for calling
‘vc-default-modify-ignores’."

  (if (null pattern-or-file) (setq pattern-or-file ""))
  (setq directory (or directory default-directory))
  (when is-file
    (setq pattern-or-file (vc-expand-file-name pattern-or-file directory))
    ;; apply directory-as-file-name, otherwise, if pattern-or-file was
    ;; a sub-repository, find-ignore-file would return the wrong
    ;; ignore file:
    ;; (vc-cvs-find-ignore-file "/re/po/dir/") => /re/po/dir/.cvsignore
    ;; (vc-cvs-find-ignore-file "/re/po/dir") => /re/po/.cvsignore
    (if (not (string= pattern-or-file directory))
        (setq directory (file-name-directory (directory-file-name pattern-or-file)))))

  (let* ((ignore-file (vc-call-backend backend 'find-ignore-file directory))
         (ignore-dir (file-name-directory ignore-file))
         is-dir ignore-param pattern)
    (if (not is-file)
        (setq ignore-param vc-ignore-param-none)

      ;; prepare file pattern
      (let* ((ignore-dir-len (length ignore-dir))
             (file-len (length pattern-or-file)))
        (unless (cond
                 ((>= file-len ignore-dir-len)
                  (string= (substring pattern-or-file 0 ignore-dir-len) ignore-dir))
                 ((= (1- ignore-dir-len) file-len)
                  (string= pattern-or-file (substring ignore-dir 0 file-len))))
          (error "Ignore spec %s is not below project root %s"
                 pattern-or-file ignore-dir))
        ;; directory may not yet exist
        (setq is-dir (or (vc-has-final-slash pattern-or-file)
                         (file-directory-p pattern-or-file)))
        (setq pattern-or-file
              (directory-file-name
               (substring (if is-dir
                              (file-name-as-directory pattern-or-file)
                            pattern-or-file)
                          ignore-dir-len)))
        ;; (setq debug-on-next-call t) ;; |||:here:|||
        (if (string= pattern-or-file "") (setq is-dir nil))
        (setq ignore-param (vc-call-backend backend 'ignore-param ignore-file))))
    (setq pattern
          (concat
           (plist-get ignore-param :anchor:)
           (funcall (or (plist-get ignore-param :escape:) #'identity)
                    pattern-or-file)
           (or (and is-dir (plist-get ignore-param :dir-trailer:))
               (plist-get ignore-param :trailer:))))
    (list pattern ignore-file remove)))

(defun vc-default-modify-ignores (_backend pattern ignore-file remove)
  "Add PATTERN to IGNORE-FILE, if REMOVE is nil..
Otherwise remove PATTERN from IGNORE-FILE."
  (if remove
      (vc--remove-regexp
       (concat "^" (regexp-quote pattern) "\\(\n\\|$\\)") ignore-file)
    (vc--add-line pattern ignore-file)))

(defun vc-file-name-directory (file &optional dir dir-as-file)
  "Get directory name for FILE.
FILE is expanded against DIR.  If FILE is a directory and DIR-AS-FILE
is non-nil, its parent directory is returned."
  (and file
       (let* ((path (expand-file-name file dir)))
         (file-name-directory
          (if dir-as-file
              (directory-file-name path)
            path)))))

(defun vc-file-relative-name (file &optional dir dir-is-empty)
  "Get relative file name for FILE against DIR.
If FILE is a directory and DIR-IS-EMPTY is non-nil, nil is returned.
Otherwise, if FILE is a directory, the final slash is removed."
  (and (not (and dir-is-empty (file-directory-p file)))
       (directory-file-name (file-relative-name file dir))))

(defun vc-has-final-slash (s)
  ;"Return index of final slash in string S or nil."
  (let ((l (1- (length s))))
    (and (> l 0) (eq (aref s l) ?/) l)))

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 vc-cvs-ignore() and vc-cvs-append-to-ignore() are obsolete and can therefore be removed:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
;; --------------------------------------------------
;; |||:sec:||| CVS specialized parameters
;; --------------------------------------------------

;; (require 'vc-cvs)
(fmakunbound 'vc-cvs-ignore)
(fmakunbound 'vc-cvs-append-to-ignore)
(put 'CVS 'vc-functions nil)

(defun vc-cvs-find-ignore-file (file)
  "Return the ignore file for FILE."
  (expand-file-name ".cvsignore" (if file (file-name-directory file))))

(defvar vc-cvs-ignore-param-glob
  '(:escape: vc-cvs-glob-escape :anchor: "" :trailer: "" :dir-trailer: "/")
  "Ignore parameters for CVS partially anchored glob wildcards.")

(defun vc-cvs-ignore-param (&optional _ignore-file)
  "Appropriate CVS ignore parameters for IGNORE-FILE."
  vc-cvs-ignore-param-glob)

(defun vc-cvs-glob-escape (string)
  "Escape special glob characters and spaces in STRING."
  (replace-regexp-in-string " " "?" (vc-glob-escape string) t))

Bzr glob wildcards cannot be escaped. Therefore regular expression syntax is used:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
;; --------------------------------------------------
;; |||:sec:||| Bzr specialized parameters
;; --------------------------------------------------

(put 'Bzr 'vc-functions nil)
(put 'BZR 'vc-functions nil)

(unless (fboundp 'vc-bzr-find-ignore-file)
(defun vc-bzr-find-ignore-file (file)
  "Return the root directory of the repository of FILE."
  (expand-file-name ".bzrignore"
                    (vc-bzr-root file)))
)

(defvar vc-bzr-ignore-param-regexp
  '(:escape: vc-py-regexp-quote :anchor: "RE:^" :trailer: "$" :dir-trailer: "/.*")
  "Ignore parameters for Bzr anchored regular expressions.")

(defun vc-bzr-ignore-param (&optional _ignore-file)
  "Appropriate Bzr ignore parameters for IGNORE-FILE."
        vc-bzr-ignore-param-regexp)

Git only requires the backend function vc-git-ignore-param():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
;; --------------------------------------------------
;; |||:sec:||| Git specialized parameters
;; --------------------------------------------------

(put 'Git 'vc-functions nil)
(put 'GIT 'vc-functions nil)

(unless (fboundp 'vc-git-find-ignore-file)
(defun vc-git-find-ignore-file (file)
  "Return the git ignore file that controls FILE."
  (expand-file-name ".gitignore"
                    (vc-git-root file)))
)

(defun vc-git-ignore-param (&optional _ignore-file)
  "Appropriate Git ignore parameters for IGNORE-FILE."
  vc-ignore-param-glob-anchored)

Hg is a little more complex, due to Python regexp quoting and multiple syntax options:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
;; --------------------------------------------------
;; |||:sec:||| Hg specialized parameters
;; --------------------------------------------------

;; (require 'vc-hg)
(fmakunbound 'vc-hg-ignore)
(put 'Hg 'vc-functions nil)
(put 'HG 'vc-functions nil)

(unless (fboundp 'vc-hg-find-ignore-file)
(defun vc-hg-find-ignore-file (file)
  "Return the root directory of the repository of FILE."
  (expand-file-name ".hgignore"
                    (vc-hg-root file)))
)

(defvar vc-hg-ignore-param-regexp
  '(:escape: vc-py-regexp-quote :anchor: "^" :trailer: "$" :dir-trailer: "/")
  "Ignore parameters for Hg anchored regular expressions.")

(defvar vc-hg-ignore-param-glob
  '(:escape: vc-glob-escape :anchor: "" :trailer: "" :dir-trailer: "/*")
  "Ignore parameters for Hg anchored regular expressions.")

(defun vc-hg-ignore-param (&optional ignore-file)
  "Appropriate Hg ignore parameters for IGNORE-FILE."
  (let ((syntax "regexp"))
    (if (not ignore-file)
        (setq ignore-file (vc-hg-find-ignore-file default-directory)))
    (if (file-exists-p ignore-file)
        (with-current-buffer (find-file-noselect ignore-file)
          (save-match-data
            (goto-char (point-max))
            (if (re-search-backward "^ *syntax: *\\(regexp\\|glob\\)$" nil t)
                (setq syntax (match-string 1))))))
    (if (string= syntax "regexp")
        vc-hg-ignore-param-regexp
      vc-hg-ignore-param-glob)))

The parameter extension can easily be integrated:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
;; --------------------------------------------------
;; |||:sec:||| Integration
;; --------------------------------------------------

;; (require 'vc-hooks)
(define-key vc-prefix-map  "F" 'vc-ignore-file)
(define-key vc-prefix-map  "G" 'vc-ignore-pattern)

(bindings--define-key vc-menu-map [vc-ignore]
  '(menu-item "Ignore File..." vc-ignore-file
              :help "Ignore a file under current version control system"))
(bindings--define-key vc-menu-map [vc-ignore-pattern]
  '(menu-item "Ignore Pattern..." vc-ignore-pattern
              :help "Ignore a pattern under current version control system"))

;; (require 'vc-dir)

(defun vc-dir-ignore (&optional arg)
    "Ignore pattern.
If a prefix argument is given, remove pattern from ignore file.
See ‘vc-ignore-pattern’ for details.

For ignoring marked files in ‘vc-dir-mode’ see ‘vc-ignore-file’."
    (interactive "P")
    (unwind-protect
        (call-interactively 'vc-ignore-pattern)
      (with-temp-message "Use ‘F’ to ignore marked files." (sit-for 2))))

(define-key vc-dir-mode-map  "F" 'vc-ignore-file)
(define-key vc-dir-mode-map  "G" 'vc-dir-ignore)

5.4. Repair bugs

Bugs before Emacs 27 are repaired, so x-vc-repair.el can be used to upgrade older versions of vc:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
;; --------------------------------------------------
;; |||:sec:||| Repair bugs
;; --------------------------------------------------

(condition-case err
    (progn
      (require 'vc)
      (require 'vc-hooks)
      (require 'vc-dir)
      (require 'vc-cvs)
      (require 'vc-svn)
      ;; (require 'vc-bzr)
      (require 'vc-git)
      (require 'vc-hg)
      (require 'vc-mtn)
      )
  (error (message "error: %s (ignored)" (error-message-string err))))

;;;! Emacs 27
(unless (> emacs-major-version 26)

;; GNU bug report logs - #37182 24.5; 24.5.1: C-u vc-dir-mark-all-files should not mark directories
(defun vc-dir-mark-all-files (arg)
  "Mark all files with the same state as the current one.
With non-nil ARG (prefix argument, if interactive) mark all files.
If the current entry is a directory, mark all child files.

The commands operate on files that are on the same state.
This command is intended to make it easy to select all files that
share the same state."
  (interactive "P")
  (if arg
      ;; Mark all files.
      (progn
        ;; First check that no directory is marked, we can't mark
        ;; files in that case.
        (ewoc-map
         (lambda (filearg)
           (when (and (vc-dir-fileinfo->directory filearg)
                      (vc-dir-fileinfo->marked filearg))
             (error "Cannot mark all files, directory `%s' marked"
                    (vc-dir-fileinfo->name filearg))))
         vc-ewoc)
        (ewoc-map
         (lambda (filearg)
           (unless (or (vc-dir-fileinfo->directory filearg)
                       (vc-dir-fileinfo->marked filearg))
             (setf (vc-dir-fileinfo->marked filearg) t)
             t))
         vc-ewoc))
    (let* ((crt  (ewoc-locate vc-ewoc))
           (data (ewoc-data crt)))
      (if (vc-dir-fileinfo->directory data)
          ;; It's a directory, mark child files.
          (let (crt-data)
            (while (and (setq crt (ewoc-next vc-ewoc crt))
                        (setq crt-data (ewoc-data crt))
                        (not (vc-dir-fileinfo->directory crt-data)))
              (setf (vc-dir-fileinfo->marked crt-data) t)
              (ewoc-invalidate vc-ewoc crt)))
        ;; It's a file
        (let ((state (vc-dir-fileinfo->state data)))
          (setq crt (ewoc-nth vc-ewoc 0))
          (while crt
            (let ((crt-data (ewoc-data crt)))
              (when (and (not (vc-dir-fileinfo->marked crt-data))
                         (eq (vc-dir-fileinfo->state crt-data) state)
                         (not (vc-dir-fileinfo->directory crt-data)))
                (vc-dir-mark-file crt)))
            (setq crt (ewoc-next vc-ewoc crt))))))))

;; GNU bug report logs - #37185 24.5.1: vc--add-line, vc--remove-regexp are sub-optimal
;; Subroutine for `vc-default-ignore'
(defun vc--add-line (string file)
  "Add STRING as a line to FILE."
  (with-current-buffer (find-file-noselect file)
    (goto-char (point-min))
    (unless (re-search-forward (concat "^" (regexp-quote string) "$") nil t)
      (goto-char (point-max))
      (unless (bolp) (insert "\n"))
      (insert string "\n")
      (save-buffer))))

(defun vc--remove-regexp (regexp file)
  "Remove all matching for REGEXP in FILE."
  (if (file-exists-p file)
      (with-current-buffer (find-file-noselect file)
        (goto-char (point-min))
        (while (re-search-forward regexp nil t)
          (replace-match ""))
        (save-buffer))))

;; GNU bug report logs - #37214 [PATCH] vc-svn error messages are used as ignore list
;; obsoleted by new ‘vc-gnore’ API
;; (defun vc-svn-ignore (file &optional directory remove _is-file)
;;   "Ignore FILE under Subversion.
;; FILE is a wildcard specification, either relative to
;; DIRECTORY or absolute."
;;   (let* ((path (directory-file-name (expand-file-name file directory)))
;;          (directory (file-name-directory path))
;;          (file (file-name-nondirectory path))
;;          (ignores (vc-svn-ignore-completion-table directory))
;;          (ignores (if remove
;;                       (delete file ignores)
;;                     (push file ignores))))
;;     (vc-svn-command nil 0 nil nil "propset" "svn:ignore"
;;                     (mapconcat #'identity ignores "\n")
;;                     directory)))

;; GNU bug report logs - #37216 [PATCH] vc-svn-ignore sets incorrect properties for relative filenames
(defun vc-svn-ignore-completion-table (directory)
  "Return the list of ignored files in DIRECTORY."
  (with-temp-buffer
    (if (= (vc-svn-command t t nil "propget" "svn:ignore" (expand-file-name directory)) 0)
        (split-string (buffer-string) "\n"))))

(when (and (fboundp 'vc-git-root)
           )

;; GNU bug report logs - #39452 [PATCH] vc-git-state fails for filenames with wildcards

;; See `Git - pathspec`_
;; .. _`Git - pathspec`: https://git-scm.com/docs/gitglossary.html#Documentation/gitglossary.txt-aiddefpathspecapathspec

(defun vc-git-command (buffer okstatus file-or-list &rest flags)
  "A wrapper around `vc-do-command' for use in vc-git.el.
The difference to vc-do-command is that this function always invokes
`vc-git-program'."
  (let ((coding-system-for-read
         (or coding-system-for-read vc-git-log-output-coding-system))
        (coding-system-for-write
         (or coding-system-for-write vc-git-commits-coding-system))
        (process-environment
         (append
          `("GIT_DIR"
            "GIT_LITERAL_PATHSPECS=1"
            ;; Avoid repository locking during background operations
            ;; (bug#21559).
            ,@(when revert-buffer-in-progress-p
                '("GIT_OPTIONAL_LOCKS=0")))
          process-environment)))
    (apply 'vc-do-command (or buffer "*vc*") okstatus vc-git-program
           ;; https://debbugs.gnu.org/16897
           (unless (and (not (cdr-safe file-or-list))
                        (let ((file (or (car-safe file-or-list)
                                        file-or-list)))
                          (and file
                               (eq ?/ (aref file (1- (length file))))
                               (equal file (vc-git-root file)))))
             file-or-list)
           (cons "--no-pager" flags))))

(defun vc-git--call (buffer command &rest args)
  ;; We don't need to care the arguments.  If there is a file name, it
  ;; is always a relative one.  This works also for remote
  ;; directories.  We enable `inhibit-nul-byte-detection', otherwise
  ;; Tramp's eol conversion might be confused.
  (let ((inhibit-nul-byte-detection t)
        (coding-system-for-read
         (or coding-system-for-read vc-git-log-output-coding-system))
        (coding-system-for-write
         (or coding-system-for-write vc-git-commits-coding-system))
        (process-environment
         (append
          `("GIT_DIR"
            "GIT_LITERAL_PATHSPECS=1"
            ;; Avoid repository locking during background operations
            ;; (bug#21559).
            ,@(when revert-buffer-in-progress-p
                '("GIT_OPTIONAL_LOCKS=0")))
          process-environment)))
    (apply 'process-file vc-git-program nil buffer nil "--no-pager" command args)))

)

;;;! Emacs 24
(when (> emacs-major-version 24)

;; GNU bug report logs - #39380 26.3: Opening files in vc-dir-mode with differing root and working dir fails
(defun vc-hg-dir-status-files (dir files update-function)
  ;; XXX: We can't pass DIR directly to 'hg status' because that
  ;; returns all ignored files if FILES is non-nil (bug#22481).
  (let ((default-directory dir))
    (vc-hg-command (current-buffer) 'async files
                   "status" "re:" "-I" "."
                   (concat "-mardu" (if files "i"))
                   "-C"))
  (vc-run-delayed
    (vc-hg-after-dir-status update-function)))

)
;;;! Emacs 24

)
;;;! Emacs 27

;; |:here:| open

(condition-case err
    (progn
      (require 'vc-src)
(unless (fboundp 'vc-src--parse-state)

;; GNU bug report logs - #39502 [PATCH] Use one src status -a call for vc-src-dir-status-files
(defun vc-src--parse-state (out)
  (when (null (string-match "does not exist or is unreadable" out))
    (let ((state (aref out 0)))
      (cond
       ;; FIXME: What to do about L code?
       ((eq state ?.) 'up-to-date)
       ((eq state ?A) 'added)
       ((eq state ?M) 'edited)
       ((eq state ?I) 'ignored)
       ((eq state ?R) 'removed)
       ((eq state ?!) 'missing)
       ((eq state ??) 'unregistered)
       (t 'up-to-date)))))

(defun vc-src-state (file)
  "SRC-specific version of `vc-state'."
  (let*
      ((status nil)
       (default-directory (vc-file-name-directory file nil t))
       (file (vc-file-relative-name file))
       (out
        (with-output-to-string
          (with-current-buffer
              standard-output
            (setq status
                  ;; Ignore all errors.
                  (condition-case nil
                      (process-file
                       vc-src-program nil t nil
                       "status" "-a" file)
                    (error nil)))))))
    (when (eq 0 status)
      (vc-src--parse-state out))))

(defun vc-src-dir-status-files (dir files update-function)
  (let*
      ((result nil)
       (status nil)
       (default-directory (or dir default-directory))
       (out
        (with-output-to-string
          (with-current-buffer
              standard-output
            (setq status
                  ;; Ignore all errors.
                  (condition-case nil
                      (apply
                       #'process-file vc-src-program nil t nil
                       "status" "-a"
                       (mapcar (lambda (f) (vc-file-relative-name f)) files))
                    (error nil))))))
       dlist)
    (when (eq 0 status)
      (dolist (line (split-string out "[\n\r]" t))
        (let* ((pair (split-string line "[\t]" t))
               (state (vc-src--parse-state (car pair)))
               (frel (cadr pair)))
          (if (file-directory-p frel)
              (push frel dlist)
            (when (not (eq state 'up-to-date))
              (push (list frel state) result)))
          ))
      (dolist (drel dlist)
        (let* ((dresult (vc-src-dir-status-files (expand-file-name drel) nil #'identity)))
          (dolist (dres dresult)
            (push (list (concat (file-name-as-directory drel) (car dres)) (cadr dres)) result)))
            )
      (funcall update-function result))))

)

(unless (fboundp 'vc-src-command-raw)

;; |:here:||:todo:| unreported: SRC commands do not work in sub-directories
(defcustom vc-src-command-safe t
  "Run SRC commands separately and normalized for each file.
This is necessary when SRC has trouble working on files in
sub-directories."
  :type 'boolean
  :group 'vc-src)

(defun vc-src-command-raw (buffer file-or-list flags)
  "A wrapper around ‘vc-do-command’ for use in vc-src.el.
This function differs from ‘vc-do-command’ in that it invokes `vc-src-program'."
  (let (file-list)
    (cond ((stringp file-or-list)
           (setq file-list (list "--" file-or-list)))
          (file-or-list
           (setq file-list (cons "--" file-or-list))))
    (apply 'vc-do-command (or buffer "*vc*") 0 vc-src-program file-list flags)))

(defun vc-src-command-iterator (buffer file-or-list flags &optional dir-is-empty)
  "A wrapper around ‘vc-do-command-raw’ for use in vc-src.el.

If ‘vc-src-command-safe’ is non-nil, ‘vc-do-command-raw’ is
applied in BUFFER to each file from FILE-OR-LIST.  File is
normalized, such that it becomes a simple basename relative to
‘default-directory’.

If a file is a directory and DIR-IS-EMPTY is nil,
‘default-directory’ is set to file and the command is run without
a file argument."
  (if (and vc-src-command-safe file-or-list)
      (dolist (file (if (stringp file-or-list) (list file-or-list) file-or-list))
        (let* ((default-directory (vc-file-name-directory file nil (not dir-is-empty)))
               (file (vc-file-relative-name file nil dir-is-empty)))
          (vc-src-command-raw buffer file flags)))
    (vc-src-command-raw buffer file-or-list flags)))

(defun vc-src-command (buffer file-or-list &rest flags)
  "A wrapper around ‘vc-src-command-iterator’ with DIR-IS-EMPTY nil."
  (vc-src-command-iterator buffer file-or-list flags))

(defun vc-src-command-dir-empty (buffer file-or-list &rest flags)
  "A wrapper around ‘vc-src-command-iterator’ with DIR-IS-EMPTY t."
  (vc-src-command-iterator buffer file-or-list flags t))

(defun vc-src-print-log (files buffer &optional shortlog _start-revision limit)
  "Print commit log associated with FILES into specified BUFFER.
If SHORTLOG is non-nil, use the list method.
If START-REVISION is non-nil, it is the newest revision to show.
If LIMIT is non-nil, show no more than this many entries."
  ;; FIXME: Implement the range restrictions.
  ;; `vc-do-command' creates the buffer, but we need it before running
  ;; the command.
  (vc-setup-buffer buffer)
  ;; If the buffer exists from a previous invocation it might be
  ;; read-only.
  (let ((inhibit-read-only t))
    (with-current-buffer
        buffer
      (apply 'vc-src-command-dir-empty buffer files (if shortlog "list" "log")
             (nconc
              ;;(when start-revision (list (format "%s-1" start-revision)))
              (when limit (list "-l" (format "%s" limit)))
              vc-src-log-switches)))))

)
      )
  (error (message "error: %s (ignored)" (error-message-string err))))

;; |:here:||:todo:| unreported

;; |:here:| missing find-ignore-file functions

(if (t) nil                             ; disabled

;; |:here:| unreported: vc-revert does not work for added files under Git
;; |:todo:| it happens sporadically, that ‘vc-backend’ reports nil, but
;; ‘vc-responsible-backend’ reports the correct backend, the reason is
;; unclear

(defmacro vc-call-responsible (fun file &rest args)
  "A convenience macro for calling VC backend functions.
Functions called by this macro must accept FILE as the first argument.
ARGS specifies any additional arguments.  FUN should be unquoted.
BEWARE!! FILE is evaluated twice!!"
  `(vc-call-backend (vc-responsible-backend ,file) ',fun ,file ,@args))

(defun vc-version-backup-file (file &optional rev)
  "Return name of backup file for revision REV of FILE.
If version backups should be used for FILE, and there exists
such a backup for REV or the working revision of file, return
its name; otherwise return nil."
  (when (vc-call-responsible make-version-backups-p file)
    (let ((backup-file (vc-version-backup-file-name file rev)))
      (if (file-exists-p backup-file)
          backup-file
        ;; there is no automatic backup, but maybe the user made one manually
        (setq backup-file (vc-version-backup-file-name file rev 'manual))
        (when (file-exists-p backup-file)
          backup-file)))))

(defun vc-revert-file (file)
  "Revert FILE back to the repository working revision it was based on."
  (with-vc-properties
   (list file)
   (let ((backup-file (vc-version-backup-file file)))
     (when backup-file
       (copy-file backup-file file 'ok-if-already-exists)
       (vc-delete-automatic-version-backups file))
     (vc-call-responsible revert file backup-file))
   `((vc-state . up-to-date)
     (vc-checkout-time . ,(file-attribute-modification-time
                           (file-attributes file)))))
  (vc-resynch-buffer file t t))

)

5.5. Appendix - Version control systems

SRC resides at https://gitlab.com/esr/src. Clone with:

git clone https://gitlab.com/esr/src.git

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

5.7. Appendix - Ignore file syntax

Mostly glob wildcards are used.

Mercurial supports regular expressions by default. Glob syntax can be activated with:

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

5.7.1. 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 ‘'.

5.7.2. 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 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
? [?]
* [*]
[ [[]

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

5.8. 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.el0110-(defcustom vc-handled-backends ‘(RCS CVS SVN SCCS SRC Bzr Git Hg Mtn)
vc-hooks.el0111: ;; RCS, CVS, SVN, SCCS, and SRC come first because they are per-dir
vc-hooks.el0112: ;; 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.

5.8.1. 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:.

5.9. 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:

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

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

5.11. Appendix - Current version of ‘vc-ignore’

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

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

5.12.1. Initial revision of vc-ignore

When attemptinig to reverse engineer the design of the 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
vc-ignore() strong hint yes
vc-dir-ignore() mandatory no
vc-cvs-ignore() no mandatory
vc-svn-ignore() mandatory no
vc-src-ignore()
vc-bzr-ignore() no mandatory
vc-git-ignore() no mandatory
vc-hg-ignore() no mandatory
vc-mtn-ignore()

The initial commit of the vc ignore feature happended on 2013-07-30.

  commit 7aa7fff0c8860b72a2c7cdc7d4d0845245754d43
  Author: Xue Fuqiao <xfq.free@gmail.com>
  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 vc-ignore() hints at the intended input format to be a plain file. So does the interactive prompt specification "fIgnore file: ".

(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 vc-dir-current-file(), which is an absolute file path, enhances the interpretation, that vc-ignore() is meant to handle real file pathes instead of patterns.

(defun vc-dir-ignore ()
  "Ignore the current file."
  (interactive)
  (vc-ignore (vc-dir-current-file)))

vc-cvs-ignore() is a thin wrapper around cvs-append-to-ignore(), which was imported from pcvs. Here the expected parameter STR was never a file path but a valid ignore pattern.

(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 vc-svn-ignore() is a placeholder which does not work at all.

(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:

commit c799337f12e84b4ca88f509ecea3a7e55ff4c768
Author: Dmitry Gutov <dgutov@yandex.ru>
Date:   Fri Oct 3 17:15:05 2014 +0400

manipulates argument FILE with file-relative-name(), which requires FILE to be an actual file path.

(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 bzr ignore leads to an error:

bzr: ERROR: NAME_PATTERN should not be an absolute path

So vc-bzr-ignore() really expects a valid NAME_PATTERN.

(defun vc-bzr-ignore (file)
  "Ignore FILE under Bazaar."
  (interactive)
  (vc-bzr-command "ignore" (get-buffer-create "*vc-ignore*") 0
                 file))

Although vc-git-ignore() states that a FILE will be ignored, only a valid pattern works.

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

vc-hg-ignore() is a cut and paste of vc-git-ignore() (later consolidated into vc-default-ignore()) and therefore needs a pattern to work.

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

5.13. 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 vc-ignore(), vc-hg-ignore:

  1. vc-dir-ignore() without a prefix argument shall call vc-ignore() with the result of (vc-dir-current-file), which is an absolute path name.
  2. vc-dir-ignore() with a prefix argument shall call 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 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.
    1. If FILE matches the regular expression vc-hg-ignore-detect-wildcard "[*^$]", it is considered a pattern and is written unmodified into the ignore file,
    2. 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$”

5.14. 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 vc-ignore() problems. First for Mercurial, then CVS and SVN.