Subject: Re: bug#37189: 25.4.1: vc-hg-ignore implementation is missing

From: Wolfgang Scherer <[email-masked]>
Date: Sat, 1 Feb 2020 02:20:08 +0100

Hi Dmitry,

I don't think, that the problem with ignore specs can be solved in
this (adhoc) manner. It should (finally) be properly designed.

There is a fundamental difference between per-directory ignore specs
and per-tree ignore specs. Let me try to outline, where I see the
problem with the current implementation:

1. Per-tree VC support is (unecessarily) mapped onto per-directory
   semantics.
2. `vc-ignore` is a quick and dirty afterthought, which does not
   properly model per-tree use cases.
3. The rationale for my implementation of `vc-hg-ignore` covers
   several use cases, which should be considered for a proper design.

Until the `vc-ignore` system is fully streamlined, the patches I provided
can be used to deliver something other than nonsense for the
`vc-dir-mode` cases.

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.

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.

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.

   a. If FILE matches the regular expression
      `vc-hg-ignore-detect-wildcard` ("[*^$]"), it is considered a
      pattern an is written unmodified into the ignore file,

   b. Otherwise, FILE is expanded to an absolute file name, which is
      then made relative to the ignore file directory. The relative
      file path is then escaped according to the active ignore syntax
      and written into the ignore file.

Examples (Mercurial repository):

* `M-x vc-ignore` in sub directory "below1/below2":
  "/home/repo/below1/below2/file.name" => "^below1/below2/file\.name$"

* `M-x vc-ignore` in sub directory "below1/below2":
  "file.name" => "^below1/below2/file\.name$"

* `M-x vc-ignore` in sub directory "some/where/in":
  "\.ext$" => "\.ext$"

* `G` in `vc-dir-mode` on "below1/below2/file.name":
  "/home/repo/below1/below2/file.name" => "^below1/below2/file\.name$"

* `G` in `vc-dir-mode` marked "below1/below2/file.name":
  "below1/below2/file.name" => "^below1/below2/file\.name$"

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

B. Initial revision of vc-ignore
================================

Note: `cvs-append-to-ignore` imported from pcvs.el

commit 7aa7fff0c8860b72a2c7cdc7d4d0845245754d43
Author: Xue Fuqiao <[email-masked]>
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.

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

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

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

+(defun vc-git-ignore (file)
+  "Ignore FILE under Git."
+  (interactive)
+  (with-temp-buffer
+    (insert-file-contents
+     (let (gitignore (concat (file-name-as-directory (vc-git-root
+                                                     default-directory)) ".gitignore"))
+       (unless (search-forward file nil t)
+        (goto-char (point-max))
+        (insert (concat "\n" file "\n"))
+        (write-region 1 (point-max) gitignore))))))

+(defun vc-hg-ignore (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))))))

+(defun vc-svn-ignore (file)
+  "Ignore FILE under Subversion."
+  (interactive)
+  (vc-svn-command (get-buffer-create "*vc-ignore*") 0
+                 file "propedit" "svn:ignore"))
+

+(defun vc-ignore (file)
+  "Ignore FILE under the current VCS."
+  (interactive "fIgnore file: ")
+  (let ((backend (vc-backend file)))
+    (vc-call-backend backend 'ignore file)))

C. 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 and does not map the per-tree semantics
into an inapplicable per-directory UI.

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 Mercurial, then CVS and SVN.