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
\
.
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.
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.
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 |
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).
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/**
.
vc-ignore()
API change request¶The status quo before Emacs 27 is:
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.
vc-dir-ignore()
calls vc-ignore()
with either an
absolute or relative filename.
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. [...]"
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() |
– | – |
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.
vc-register()
). Contrary to other vc
commands, the
current file should not be automatically ignored outside
vc-dir-mode and dired-mode.vc-ignore()
interactive
prompt expects that the exact file path is excluded
unambiguously.vc-ignore()
interactive
prompt expects that no modifications are made to the pattern.Example use cases for CVS and Hg:
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-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.
Function vc-ignore()
is modified to handle both the “pattern”
and “file path” use cases.
vc-ignore()
is renamed to
PATTERN-OR-FILE to clarify its dual purpose.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.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).vc-ignore-pattern()
is an alias for
vc-ignore()
to allow for a clear and unambiguous
documentation string that does not mix use cases.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.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.vc-ignore-file()
vc-ignore-pattern()
; functionality unchangedin vc-dir-mode()
:
vc-ignore-file()
vc-dir-ignore()
; functionality was brokenThe 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.
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
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.
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)
|
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))
)
|
SRC resides at https://gitlab.com/esr/src. Clone with:
git clone https://gitlab.com/esr/src.git
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.
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).
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 ‘'.
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 ofsrc 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 |
---|---|
? |
[?] |
* |
[*] |
[ |
[[] |
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.
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:
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.
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:.
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.
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)
(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)))
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.
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.
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:
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.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).vc-ignore()
is either a pattern, an
absolute file path or a relative file path."[*^$]"
, it is considered a
pattern and is written unmodified into the ignore file,Examples (Mercurial repository):
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.