Blob


1 ;;; vc-got.el --- VC backend for Game of Trees VCS -*- lexical-binding: t; -*-
3 ;; Copyright (C) 2021-2022 Free Software Foundation, Inc.
5 ;; Author: Omar Polo <op@omarpolo.com>
6 ;; Timo Myyrä <timo.myyra@bittivirhe.fi>
7 ;; URL: https://projects.omarpolo.com/vc-got.html
8 ;; Keywords: vc tools
9 ;; Version: 1.1.2
10 ;; Package-Requires: ((emacs "25.1"))
12 ;; This program is free software; you can redistribute it and/or modify
13 ;; it under the terms of the GNU General Public License as published by
14 ;; the Free Software Foundation, either version 3 of the License, or
15 ;; (at your option) any later version.
17 ;; This program is distributed in the hope that it will be useful,
18 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ;; GNU General Public License for more details.
22 ;; You should have received a copy of the GNU General Public License
23 ;; along with this program. If not, see <https://www.gnu.org/licenses/>.
25 ;;; Commentary:
27 ;; vc-got is a VC backend for the Game of Trees (got) version control
28 ;; system.
29 ;;
30 ;; Backend implementation status
31 ;;
32 ;; Function marked with `*' are required, those with `-' are optional.
33 ;;
34 ;; FUNCTION NAME STATUS
35 ;;
36 ;; BACKEND PROPERTIES:
37 ;; * revision-granularity DONE
38 ;; - update-on-retrieve-tag XXX: what should this do?
39 ;;
40 ;; STATE-QUERYING FUNCTIONS:
41 ;; * registered DONE
42 ;; * state DONE
43 ;; - dir-status-files DONE
44 ;; - dir-extra-headers DONE
45 ;; - dir-printer DONE
46 ;; - status-fileinfo-extra NOT IMPLEMENTED
47 ;; * working-revision DONE
48 ;; * checkout-model DONE
49 ;; - mode-line-string DONE
50 ;;
51 ;; STATE-CHANGING FUNCTIONS:
52 ;; * create-repo NOT IMPLEMENTED
53 ;; I don't think got init does what this function is supposed to
54 ;; do.
55 ;; * register DONE
56 ;; - responsible-p DONE
57 ;; - receive-file NOT NEEDED, default `register' is fine
58 ;; - unregister DONE
59 ;; * checkin DONE
60 ;; * find-revision DONE
61 ;; * checkout NOT IMPLEMENTED
62 ;; I'm not sure how to properly implement this. Does filling
63 ;; FILE with the find-revision do the trick? Or use got update?
64 ;; * revert DONE
65 ;; - merge-file NOT IMPLEMENTED
66 ;; - merge-branch DONE
67 ;; - merge-news NOT IMPLEMENTED
68 ;; - pull DONE
69 ;; - push DONE
70 ;; - steal-lock NOT NEEDED, `got' is not using locks
71 ;; - modify-change-comment NOT IMPLEMENTED
72 ;; can be implemented via histedit, if I understood correctly
73 ;; what it is supposed to do.
74 ;; - mark-resolved NOT NEEDED
75 ;; got notice by itself when a file doesn't have any pending
76 ;; conflicts to be resolved.
77 ;; - find-admin-dir NOT NEEDED
78 ;;
79 ;; HISTORY FUNCTIONS
80 ;; * print-log DONE
81 ;; * log-outgoing DONE
82 ;; * log-incoming DONE
83 ;; - log-search DONE
84 ;; - log-view-mode DONE
85 ;; - show-log-entry NOT IMPLEMENTED
86 ;; - comment-history NOT IMPLEMENTED
87 ;; - update-changelog NOT IMPLEMENTED
88 ;; * diff DONE
89 ;; - revision-completion-table DONE
90 ;; - annotate-command DONE
91 ;; - annotate-time DONE
92 ;; - annotate-current-time NOT NEEDED
93 ;; the default time handling is enough.
94 ;; - annotate-extract-revision-at-line DONE
95 ;; - region-history NOT IMPLEMENTED
96 ;; - region-history-mode NOT IMPLEMENTED
97 ;; - mergebase NOT IMPLEMENTED
98 ;;
99 ;; TAG SYSTEM
100 ;; - create-tag DONE
101 ;; - retrieve-tag DONE
102 ;;
103 ;; MISCELLANEOUS NOT IMPLEMENTED
104 ;; - make-version-backups-p NOT NEEDED, `got' works fine locally
105 ;; - root DONE
106 ;; - ignore NOT NEEDED, the default action is good
107 ;; - ignore-completion-table NOT NEEDED, the default action is good
108 ;; - find-ignore-file DONE
109 ;; - previous-revision DONE
110 ;; - next-revision DONE
111 ;; - log-edit-mode NOT IMPLEMENTED
112 ;; - check-headers NOT NEEDED, `got' does not use headers
113 ;; - delete-file DONE
114 ;; - rename-file NOT IMPLEMENTED
115 ;; - find-file-hook DONE
116 ;; - extra-menu NOT IMPLEMENTED
117 ;; - extra-dir-menu NOT IMPLEMENTED, same as above
118 ;; - conflicted-files DONE
119 ;; - repository-url DONE
121 ;;; Code:
123 ;; TODO: vc-git has most function that starts with:
124 ;;
125 ;; (let* ((root (vc-git-root default-directory))
126 ;; (buffer (format "*vc-git : %s*" (expand-file-name root)))
127 ;; ...)
128 ;; ...)
129 ;;
130 ;; we should 1) investigate if also other backends do something like
131 ;; this (or if there is a better way) and 2) try to do the same.
133 (eval-when-compile
134 (require 'subr-x))
136 (require 'cl-lib)
137 (require 'seq)
138 (require 'vc)
139 (require 'vc-annotate)
141 ;; FIXME: avoid loading this? We only need it for
142 ;; log-edit-extract-headers in vc-got-checkin.
143 (require 'log-edit)
145 ;; FIXME: avoid loading this? We only need it for
146 ;; vc-dir-filename-mouse-map in our custom printer.
147 (require 'vc-dir)
149 ;; FIXME: avoid loading this? We only need it for
150 ;; compilation-{directory,arguments}.
151 (require 'compile)
153 ;; FIXME: avoid loading this? We only need it for
154 ;; log-view-{file-re,per-file-logs,message-re}.
155 (require 'log-view)
157 (defgroup vc-got nil
158 "VC GoT backend."
159 :group 'vc)
161 (defcustom vc-got-program "got"
162 "Name of the Got executable (excluding any arguments)."
163 :type 'string)
165 (defcustom vc-got-diff-switches t
166 "String or list of strings specifying switches for Got diff under VC.
167 If nil, use the value of `vc-diff-switches'. If t, use no switches."
168 :type '(choice (const :tag "Unspecified" nil)
169 (const :tag "None" t)
170 (string :tag "Argument String")
171 (repeat :tag "Argument List" :value ("") string)))
173 ;; helpers
174 (defmacro vc-got--with-emacs-version<= (version &rest body)
175 "Eval BODY only when the Emacs version in greater or equal VERSION."
176 (declare (debug body)
177 (indent defun))
178 (when (version<= version emacs-version)
179 `(progn ,@body)))
181 (defun vc-got--version ()
182 "Return string representing the got version."
183 (let (process-file-side-effects)
184 (with-temp-buffer
185 (vc-got--call "-V")
186 (substring (buffer-string) 4 -1))))
188 (defun vc-got--version<= (target)
189 "Compare the current version against TARGET.
190 Takes care of handling the -current suffix."
191 (let* ((version-string (vc-got--version))
192 (current-version (string-replace "-current" "" version-string)))
193 (when (version<= current-version target)
194 ;; let X.Y-current sort *after* X.Y
195 (string= version-string current-version))))
197 (defun vc-got-root (file)
198 "Return the work tree root for FILE, or nil."
199 (vc-find-root file ".got"))
201 (defmacro vc-got-with-worktree (file &rest body)
202 "Evaluate BODY in the work tree directory of FILE."
203 (declare (debug t) (indent defun))
204 `(when-let (default-directory (vc-got-root ,file))
205 ,@body))
207 (defun vc-got--repo-root ()
208 "Return the path to the repository root.
209 Assume `default-directory' is inside a got worktree."
210 (vc-got-with-worktree default-directory
211 (with-temp-buffer
212 (insert-file-contents ".got/repository")
213 (string-trim (buffer-string) "" "\n"))))
215 (defun vc-got--call (&rest args)
216 "Call `vc-got-program' with ARGS.
217 The output will be placed in the current buffer."
218 (apply #'process-file vc-got-program nil (current-buffer) nil
219 (apply #'nconc (mapcar (lambda (s) (if (listp s) s (list s))) args))))
221 (defun vc-got--add (files)
222 "Add FILES to got, passing `vc-register-switches' to the command invocation."
223 (with-temp-buffer
224 (vc-got--call "add" vc-register-switches "--" files)))
226 (defun vc-got--log (&optional path limit start-commit stop-commit
227 search-pattern reverse include-diff)
228 "Execute the log command in the worktree of PATH in the current buffer.
229 LIMIT limits the maximum number of commit returned.
231 START-COMMIT: start traversing history at the specified commit.
232 STOP-COMMIT: stop traversing history at the specified commit.
233 SEARCH-PATTERN: limit to log messages matched by the regexp given.
234 REVERSE: display the log messages in reverse order.
235 INCLUDE-DIFF: display the patch of modifications made in each commit.
237 Return nil if the command failed or if PATH isn't included in any
238 worktree."
239 (let* ((process-file-side-effects nil)
240 ;; got 0.71-current at some point switched to -S for search
241 ;; pattern and -s for the one-line format.
242 ;; XXX: remove in a few releases.
243 (search-flag (if (vc-got--version<= "0.71")
244 "-s"
245 "-S")))
246 (vc-got-with-worktree (or path default-directory)
247 (when (zerop
248 (save-excursion
249 (vc-got--call "log"
250 (and limit (list "-l" (format "%s" limit)))
251 (and start-commit (list "-c" start-commit))
252 (and stop-commit (list "-x" stop-commit))
253 (and search-pattern (list search-flag
254 search-pattern))
255 (and reverse '("-R"))
256 (and include-diff '("-p"))
257 ;; "--"
258 path)))
259 (save-excursion
260 (delete-matching-lines
261 "^-----------------------------------------------$")
262 t)))))
264 (defun vc-got--status (status-codes dir-or-file &optional files)
265 "Return a list of lists (FILE STATUS STAGE-STATUS).
266 DIR-OR-FILE can be either a directory or a file. If FILES is
267 given, return the status of those files, otherwise the status of
268 DIR-OR-FILE. STATUS-CODES is either nil, or a string that's
269 passed as the -s flag to got status to limit the types of status
270 to report (e.g. \"CD\" to report only conflicts and deleted
271 files)."
272 (with-temp-buffer
273 (let* ((default-directory (expand-file-name
274 (if (file-directory-p dir-or-file)
275 dir-or-file
276 (file-name-directory dir-or-file))))
277 (root (vc-got-root default-directory))
278 (process-file-side-effects))
279 (when (zerop (vc-got--call "status"
280 (and status-codes (list "-s" status-codes))
281 "--"
282 (or files dir-or-file)))
283 (goto-char (point-min))
284 (cl-loop until (eobp)
285 collect (vc-got--parse-status-line root)
286 do (forward-line))))))
288 (defun vc-got--parse-status-line (root)
289 "Parse a line of the the output of status.
290 ROOT is the root of the repo."
291 ;; the format of each line is
292 ;; <status-char> <stage-char> <spc> <filename> \n
293 (let* ((file-status (prog1 (vc-got--parse-status-char
294 (char-after))
295 (forward-char)))
296 (stage-status (let* ((c (char-after)))
297 (prog1
298 (when (member c '(?M ?A ?D))
299 c)
300 (forward-char))))
301 (filename (progn
302 (forward-char)
303 (buffer-substring (point)
304 (line-end-position)))))
305 (list (file-relative-name (expand-file-name filename root)
306 default-directory)
307 (or file-status (and stage-status 'up-to-date))
308 stage-status)))
310 (defun vc-got--parse-status-char (c)
311 "Parse status char C into a symbol accepted by `vc-state'."
312 (cl-case c
313 (?M 'edited)
314 (?A 'added)
315 (?D 'removed)
316 (?C 'conflict)
317 (?! 'missing)
318 (?~ 'edited) ; XXX: what does it means for a file to be ``obstructed''?
319 (?? 'unregistered)
320 (?m 'edited) ; modified file modes
321 (?N nil)))
323 (defun vc-got--cat (commit obj-id)
324 "Execute got cat -c COMMIT OBJ-ID in the current buffer."
325 (let (process-file-side-effects)
326 (zerop (vc-got--call "cat" "-c" commit obj-id))))
328 (defun vc-got--revert (&rest files)
329 "Execute got revert FILES."
330 (vc-got-with-worktree (car files)
331 (with-temp-buffer
332 (zerop (vc-got--call "revert" "--" files)))))
334 (defun vc-got--list-branches ()
335 "Return an alist of (branch . commit)."
336 (let (process-file-side-effects)
337 (with-temp-buffer
338 (when (zerop (vc-got--call "branch" "-l"))
339 (let (alist)
340 (goto-char (point-min))
341 (while (re-search-forward "^\\*?[[:space:]]+\\(.+\\): \\([[:word:]]+\\)$"
342 nil t)
343 (push (cons (match-string 1) (match-string 2)) alist))
344 alist)))))
346 (defun vc-got--current-branch ()
347 "Return the current branch."
348 (let (process-file-side-effects)
349 (with-temp-buffer
350 (when (zerop (vc-got--call "branch"))
351 (string-trim (buffer-string) "" "\n")))))
353 (defun vc-got--integrate (branch)
354 "Integrate BRANCH into the current one."
355 (with-temp-buffer
356 (zerop (vc-got--call "integrate" branch))))
358 (defun vc-got--update (branch &optional paths)
359 "Update to a different commit or BRANCH.
360 Optionally restrict the update operation to files at or within
361 the specified PATHS."
362 (with-temp-buffer
363 (unless (zerop (vc-got--call "update" "-b" branch "--" paths))
364 (error "[vc-got] can't update to branch %s: %s"
365 branch
366 (buffer-string)))))
368 (defun vc-got--diff-files (files)
369 "Compute the local modifications to FILES."
370 (let (process-file-side-effects)
371 (zerop (vc-got--call "diff" (vc-switches 'got 'diff) "-P" "--"
372 files))))
374 (defun vc-got--diff-objects (obj1 obj2)
375 "Diff the two objects OBJ1 and OBJ2.
376 OBJ1 and OBJ2 are interpreted as a reference, tag name, or an
377 object ID SHA1 hash."
378 (let (process-file-side-effects)
379 (zerop (vc-got--call "diff" (vc-switches 'got 'diff) "--" obj1 obj2))))
381 (defun vc-got--unstage (file-or-directory)
382 "Unstage all the staged hunks at or within FILE-OR-DIRECTORY.
383 If it's nil, unstage every staged changes across the entire work
384 tree."
385 (zerop (vc-got--call "unstage" "--" file-or-directory)))
387 (defun vc-got--remove (file &optional force keep-local)
388 "Use got to remove FILE.
389 If FORCE is non-nil perform the operation even if a file contains
390 local modification. If KEEP-LOCAL is non-nil keep the affected
391 files on disk."
392 (vc-got-with-worktree (or file default-directory)
393 (with-temp-buffer
394 (zerop (vc-got--call "remove"
395 (and force "-f")
396 (and keep-local "-k")
397 "--"
398 file)))))
400 (defun vc-got--ref ()
401 "Return a list of all references."
402 (let ((process-file-side-effects nil)
403 (re "^refs/\\(heads\\|remotes\\|tags\\)/\\(.*\\):")
404 ;; hardcoding HEAD because it's always present and the regexp
405 ;; won't match it.
406 (table (list "HEAD")))
407 (vc-got-with-worktree default-directory
408 (with-temp-buffer
409 (when (zerop (vc-got--call "ref" "-l"))
410 (goto-char (point-min))
411 (while (re-search-forward re nil t)
412 (push (match-string 2) table))
413 table)))))
415 (defun vc-got--branch (name)
416 "Try to create and switch to the branch called NAME."
417 (let (process-file-side-effects)
418 (vc-got-with-worktree default-directory
419 (with-temp-buffer
420 (or (zerop (vc-got--call "branch" "--" name))
421 (error "[vc-got] can't create branch %s: %s" name
422 (buffer-string)))))))
425 ;; Backend properties
427 (defun vc-got-revision-granularity ()
428 "Got has REPOSITORY granularity."
429 'repository)
431 (defun vc-got-update-on-retrieve-tag ()
432 "Like vc-git, vc-got don't need to buffers on `retrieve-tag'."
433 nil)
436 ;; State-querying functions
438 ;;;###autoload (defun vc-got-registered (file)
439 ;;;###autoload "Return non-nil if FILE is registered with got."
440 ;;;###autoload (when (vc-find-root file ".got")
441 ;;;###autoload (load "vc-got" nil t)
442 ;;;###autoload (vc-got-registered file)))
444 (defun vc-got-registered (file)
445 "Return non-nil if FILE is registered with got."
446 (if (file-directory-p file)
447 nil ; got doesn't track directories
448 (when (vc-find-root file ".got")
449 (let ((s (vc-got-state file)))
450 (not (memq s '(nil unregistered)))))))
452 (defun vc-got-state (file)
453 "Return the current version control state of FILE. See `vc-state'."
454 (unless (file-directory-p file)
455 (let (process-file-side-effects)
456 ;; Manually calling got status and checking the result inline to
457 ;; avoid building the data structure in vc-got--status.
458 (with-temp-buffer
459 (when (zerop (vc-got--call "status" "--" file))
460 (goto-char (point-min))
461 (if (eobp)
462 'up-to-date
463 (vc-got--parse-status-char (char-after))))))))
465 (defun vc-got--dir-filter-files (files)
466 "Remove ., .. and .got from FILES."
467 (cl-loop for file in files
468 unless (or (string= file "..")
469 (string= file ".")
470 (string= file ".got"))
471 collect file))
473 (defun vc-got-dir-status-files (dir files update-function)
474 "Build the status for FILES in DIR.
475 The builded result is given to the callback UPDATE-FUNCTION. If
476 FILES is nil, consider all the files in DIR."
477 (let* ((fs (vc-got--dir-filter-files (or files (directory-files dir))))
478 ;; XXX: we call with files, wich will probably be nil on the
479 ;; first run, so we catch deleted, missing and edited files
480 ;; in subdirectories.
481 (res (vc-got--status nil dir files))
482 double-check)
483 (cl-loop for file in fs
484 do (when (and (not (cdr (assoc file res #'string=)))
485 (not (file-directory-p file))
486 ;; if file doesn't exists, it's a
487 ;; untracked file that was removed.
488 (file-exists-p file))
489 ;; if we don't know the status of a file here, it's
490 ;; either up-to-date or ignored. Save it for a
491 ;; double check
492 (push file double-check)))
493 (cl-loop with statuses = (vc-got--status nil dir double-check)
494 for file in double-check
495 unless (eq 'unregistered (cadr (assoc file statuses #'string=)))
496 do (push (list file 'up-to-date nil) res))
497 (funcall update-function res nil)))
499 (defun vc-got-dir-extra-headers (dir)
500 "Return a string for the `vc-dir' buffer heading for directory DIR."
501 (let ((remote (vc-got-repository-url dir)))
502 (concat (propertize "Repository : " 'face 'font-lock-type-face)
503 (vc-got--repo-root) "\n"
504 (when remote
505 (concat
506 (propertize "Remote URL : " 'face 'font-lock-type-face)
507 (vc-got-repository-url dir) "\n"))
508 (propertize "Branch : " 'face 'font-lock-type-face)
509 (vc-got--current-branch))))
511 (defun vc-got-dir-printer (info)
512 "Pretty-printer for the vc-dir-fileinfo structure INFO."
513 (let* ((isdir (vc-dir-fileinfo->directory info))
514 (state (if isdir "" (vc-dir-fileinfo->state info)))
515 (stage-state (vc-dir-fileinfo->extra info))
516 (filename (vc-dir-fileinfo->name info)))
517 (insert
518 " "
519 (propertize
520 (format "%c" (if (vc-dir-fileinfo->marked info) ?* ? ))
521 'face 'font-lock-type-face)
522 " "
523 (propertize
524 (format "%-12s" state)
525 'face (cond ((eq state 'up-to-date) 'font-lock-builtin-face)
526 ((memq state '(missing conflict)) 'font-lock-warning-face)
527 ((eq state 'edited) 'font-lock-constant-face)
528 (t 'font-lock-variable-name-face))
529 'mouse-face 'highlight
530 'keymap (vc-got--with-emacs-version<= "28.0.50"
531 vc-dir-status-mouse-map))
532 " " (propertize
533 (if stage-state
534 (format "%c" stage-state)
535 " ")
536 'face (cond ((memq stage-state '(?A ?E)) 'font-lock-constant-face)
537 ((eq stage-state ?R) 'font-lock-warning-face)
538 (t 'font-lock-variable-name-face)))
539 " "
540 (propertize filename
541 'face (if isdir 'font-lock-comment-delimiter-face
542 'font-lock-function-name-face)
543 'help-echo
544 (if isdir
545 (concat
546 "Directory\n"
547 "VC operations can be applied to it\n"
548 "mouse-3: Pop-up menu")
549 "File\nmouse-3: Pop-up menu")
550 'mouse-face 'highlight
551 'keymap vc-dir-filename-mouse-map))))
553 (defun vc-got-working-revision (file)
554 "Return the last commit that touched FILE or \"0\" if it's newly added."
555 (or
556 (with-temp-buffer
557 (when (vc-got--log file 1)
558 (let (start)
559 (goto-char (point-min))
560 (forward-word) ; skip "commit"
561 (forward-char) ; skip the space
562 (setq start (point)) ; store start of the SHA
563 (forward-word) ; goto SHA end
564 (buffer-substring start (point)))))
565 ;; special case: if this file is added but has no previous commits
566 ;; touching it, got log will fail (as expected), but we have to
567 ;; return "0".
568 (when (eq (vc-got-state file) 'added)
569 "0")))
571 (defun vc-got-checkout-model (_files)
572 "Return the checkout model.
573 Got uses an implicit checkout model for every file."
574 'implicit)
576 (defun vc-got-mode-line-string (file)
577 "Return the VC mode line string for FILE."
578 (vc-got-with-worktree file
579 (let ((def (vc-default-mode-line-string 'Got file)))
580 (concat (substring def 0 4) (vc-got--current-branch)))))
583 ;; state-changing functions
585 (defun vc-got-create-repo (_backend)
586 "Create an empty repository in the current directory."
587 (error "[vc-got] create-repo not implemented"))
589 (defun vc-got-register (files &optional _comment)
590 "Register FILES, passing `vc-register-switches' to the backend command."
591 (vc-got--add files))
593 (defalias 'vc-got-responsible-p #'vc-got-root)
595 (defun vc-got-unregister (file)
596 "Unregister FILE."
597 (vc-got--remove file t t))
599 (defun vc-got-checkin (files comment &optional _rev)
600 "Commit FILES with COMMENT as commit message."
601 (with-temp-buffer
602 (unless (zerop (vc-got--call "commit" "-m"
603 (log-edit-extract-headers
604 '(("Author" . "-A"))
605 comment)
606 "--"
607 files))
608 (error "[vc-got] can't commit: %s" (buffer-string)))))
610 (defun vc-got-find-revision (file rev buffer)
611 "Fill BUFFER with the content of FILE in the given revision REV."
612 (with-current-buffer buffer
613 (vc-got-with-worktree file
614 (vc-got--cat rev (file-relative-name file)))))
616 (defun vc-got-checkout (_file &optional _rev)
617 "Checkout revision REV of FILE.
618 If REV is t, checkout from the head."
619 (error "[vc-got] checkout not implemented"))
621 (defun vc-got-revert (file &optional _content-done)
622 "Revert FILE back to working revision."
623 (vc-got--revert file))
625 (defun vc-got-merge-branch ()
626 "Prompt for a branch and integrate it into the current one."
627 ;; XXX: be smart and try to "got rebase" if "got integrate" fails?
628 (let* ((branches (cl-loop for (branch . commit) in (vc-got--list-branches)
629 collect branch))
630 (branch (completing-read "Merge from branch: " branches)))
631 (when branch
632 (vc-got--integrate branch))))
634 (defun vc-got--proc-filter (proc s)
635 "Custom output filter for async process PROC.
636 It's like `vc-process-filter' but supports \r inside S."
637 (let ((buffer (process-buffer proc)))
638 (when (buffer-live-p buffer)
639 (with-current-buffer buffer
640 (save-excursion
641 (let ((buffer-undo-list t)
642 (inhibit-read-only t))
643 (goto-char (process-mark proc))
644 (if (not (string-match ".*\r\\(.*\\)" s))
645 (insert s)
646 ;; handle \r
647 (end-of-line)
648 (let ((end (point)))
649 (beginning-of-line)
650 (delete-region (point) end))
651 (insert (match-string 1 s)))
652 (set-marker (process-mark proc) (point))))))))
654 (defun vc-got--push-pull (cmd op prompt)
655 "Execute CMD OP, or prompt the user if PROMPT is non-nil."
656 (let ((buffer (format "*vc-got : %s*" (expand-file-name default-directory))))
657 (when-let (cmd (if prompt
658 (split-string
659 (read-shell-command (format "%s %s command: " cmd op)
660 (format "%s %s " cmd op))
661 " " t)
662 (list cmd op)))
663 (apply #'vc-do-async-command buffer default-directory cmd)
664 (with-current-buffer buffer
665 (vc-compilation-mode 'got)
666 (let ((comp-cmd (mapconcat #'identity cmd " "))
667 (proc (get-buffer-process buffer)))
668 (setq-local compile-command comp-cmd)
669 (setq-local compilation-directory default-directory)
670 (setq-local compilation-arguments (list comp-cmd
671 nil
672 (lambda (_ign) buffer)
673 nil))
674 ;; Setup a custom process filter that handles \r.
675 (set-process-filter proc #'vc-got--proc-filter)))
676 (vc-set-async-update buffer))))
678 ;; TODO: this could be expanded. After a pull the worktree needs to
679 ;; be updated, either with a ``got update -b branch-name'' or ``got
680 ;; update -b remote/branchname'' plus a rebase.
681 (defun vc-got-pull (prompt)
682 "Execute a fetch prompting for the full command if PROMPT is not nil."
683 (vc-got--push-pull vc-got-program "fetch" prompt))
685 (defun vc-got-push (prompt)
686 "Execute a send prompting for the full command if PROMPT is not nil."
687 (vc-got--push-pull vc-got-program "send" prompt))
690 ;; History functions
692 (defun vc-got-print-log (files buffer &optional _shortlog start-revision limit)
693 "Insert the revision log for FILES into BUFFER.
694 LIMIT limits the number of commits, optionally starting at
695 START-REVISION."
696 (vc-setup-buffer buffer)
697 (with-current-buffer buffer
698 (let ((worktree-path (vc-got-root default-directory))
699 (inhibit-read-only t))
700 (dolist (file files)
701 (vc-got--log (file-relative-name file worktree-path)
702 limit
703 start-revision)))))
705 (defun vc-got-log-outgoing (buffer remote-location)
706 "Fill BUFFER with the diff between the local worktree branch and REMOTE-LOCATION."
707 (vc-setup-buffer buffer)
708 (let ((rl (vc-got-next-revision
709 nil
710 (if (or (not remote-location) (string-empty-p remote-location))
711 (concat "origin/" (vc-got--current-branch))
712 remote-location)))
713 (inhibit-read-only t))
714 (with-current-buffer buffer
715 (vc-got--log nil nil nil rl))))
717 (defun vc-got-log-incoming (buffer remote-location)
718 "Fill BUFFER with the incoming diff from REMOTE-LOCATION.
719 That is, the diff between REMOTE-LOCATION and the local repository."
720 (vc-setup-buffer buffer)
721 (let ((rl (if (or (not remote-location) (string-empty-p remote-location))
722 (concat "origin/" (vc-got--current-branch))
723 remote-location))
724 (inhibit-read-only t))
725 (with-current-buffer buffer
726 (vc-got--log nil nil (vc-got--current-branch) rl))))
728 (defun vc-got-log-search (buffer pattern)
729 "Search commits for PATTERN and write the results found in BUFFER."
730 (with-current-buffer buffer
731 (let ((inhibit-read-only t))
732 (vc-got--log nil nil nil nil pattern))))
734 (define-derived-mode vc-got-log-view-mode log-view-mode "Got-Log-View"
735 "Got-specific log-view mode.
736 Heavily inspired by `vc-git-log-view-mode'."
737 (require 'add-log)
738 (setq-local log-view-file-re regexp-unmatchable)
739 (setq-local log-view-per-file-logs nil)
740 (setq-local log-view-message-re "^commit +\\([0-9a-z]+\\)")
741 (setq-local log-view-font-lock-keywords
742 (append
743 `((,log-view-message-re (1 'change-log-acknowledgment)))
744 ;; Handle the case:
745 ;; user: foo@bar
746 '(("^from: \\([A-Za-z0-9_.+-]+@[A-Za-z0-9_.-]+\\)"
747 (1 'change-log-email))
748 ;; Handle the case:
749 ;; user: FirstName LastName <foo@bar>
750 ("^from: \\([^<(]+?\\)[ \t]*[(<]\\([A-Za-z0-9_.+-]+@[A-Za-z0-9_.-]+\\)[>)]"
751 (1 'change-log-name)
752 (2 'change-log-email))
753 ("^date: \\(.+\\)" (1 'change-log-date))))))
755 ;; TODO: async
756 ;; TODO: return 0 or 1
757 (defun vc-got-diff (files &optional rev1 rev2 buffer _async)
758 "Insert into BUFFER (or *vc-diff*) the diff for FILES from REV1 to REV2."
759 (let* ((buffer (get-buffer-create (or buffer "*vc-diff*")))
760 (inhibit-read-only t))
761 (with-current-buffer buffer
762 (vc-got-with-worktree (or (car files)
763 default-directory)
764 (cond ((and (null rev1)
765 (null rev2))
766 (vc-got--diff-files files))
767 ((and (null rev1)
768 rev2)
769 ;; TODO: this includes the whole diff while to respect
770 ;; the vc semantics we should filter only the diff for
771 ;; files in FILES.
772 ;;
773 ;; XXX: this includes also the commit message, I
774 ;; consider it a feature over the usual vc behaviour of
775 ;; showing only the diff.
776 (vc-got--log nil 1 rev2 nil nil nil t))
777 ;;
778 ;; TODO: if rev1 is nil, diff from the current version until
779 ;; rev2.
780 ;;
781 ;; TODO 2: if rev2 is nil as well, diff against an empty
782 ;; tree (i.e. get the patch from `got log -p rev1')
783 (t (vc-got--diff-objects rev1 rev2)))))))
785 (defun vc-got-revision-completion-table (_files)
786 "Return a completion table for existing revisions.
787 Ignores FILES because GoT doesn't have the concept of ``file
788 revisions''; instead, like with git, you have tags and branches."
789 (letrec ((table (lazy-completion-table
790 table (lambda () (vc-got--ref)))))
791 table))
793 (defun vc-got-annotate-command (file buf &optional rev)
794 "Show annotated contents of FILE in buffer BUF. If given, use revision REV."
795 (let (process-file-side-effects)
796 (with-current-buffer buf
797 ;; FIXME: vc-ensure-vc-buffer won't recognise this buffer as managed
798 ;; by got unless vc-parent-buffer points to a buffer managed by got.
799 ;; investigate why this is needed.
800 (setq-local vc-parent-buffer (find-file-noselect file))
801 (vc-got--call "blame"
802 (when rev (list "-c" rev))
803 "--"
804 file))))
806 (defconst vc-got--annotate-re
807 (concat "^[0-9]\\{1,\\}) " ; line number followed by )
808 "\\([a-z0-9]+\\) " ; SHA-1 of commit
809 "\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\) " ; year-mm-dd
810 "\\([^ ]\\)+ ") ; author
811 "Regexp to match annotation output lines.
812 Provides capture groups for:
813 1. revision id
814 2. date of commit
815 3. author of commit")
817 (defconst vc-got--commit-re "^commit \\([a-z0-9]+\\)"
818 "Regexp to match commit lines.
819 Provides capture group for the commit revision id.")
821 (defun vc-got-annotate-time ()
822 "Return the time of the next line of annotation at or after point.
823 Value is returned as floating point fractional number of days."
824 ;; XXX: to behave like vc-git here we should call re-search-forward
825 ;; instead of looking-at, as it makes the fontification of the line
826 ;; start AFTER the info. The problem is, due to the format of the
827 ;; blame, it produces an ugly result, with colors starting at
828 ;; different offsets depending on how long the committer name is.
829 (when (looking-at vc-got--annotate-re)
830 (let ((str (match-string-no-properties 2)))
831 (vc-annotate-convert-time
832 (encode-time 0 0 0
833 (string-to-number (substring str 8 10))
834 (string-to-number (substring str 5 7))
835 (string-to-number (substring str 0 4)))))))
837 (defun vc-got-annotate-extract-revision-at-line ()
838 "Return revision corresponding to the current line or nil."
839 (save-excursion
840 (beginning-of-line)
841 (when (looking-at vc-got--annotate-re)
842 (match-string-no-properties 1))))
845 ;; Tag system
847 (defun vc-got--tag-callback (tag)
848 "`log-edit' callback for `vc-got-create-tag'.
849 Creates the TAG using the content of the current buffer."
850 (interactive)
851 (let ((msg (buffer-substring-no-properties (point-min)
852 (point-max))))
853 (with-temp-buffer
854 (unless (zerop (vc-got--call "tag"
855 "-m"
856 (log-edit-extract-headers nil msg)
857 "--"
858 tag))
859 (error "[vc-got] can't create tag %s: %s" tag (buffer-string))))))
861 (defun vc-got-create-tag (_dir name branchp)
862 "Attach the tag NAME to the state of the worktree.
863 DIR is ignored (tags are global, not per-file). If BRANCHP is
864 true, NAME should create a new branch otherwise it will pop-up a
865 `log-edit' buffer to provide the tag message."
866 ;; TODO: vc reccomends to ensure that all the file are in a clean
867 ;; state, but is it useful?
868 (if branchp
869 (vc-got--branch name)
870 (let ((buf (get-buffer-create "*vc-got tag*")))
871 (with-current-buffer buf
872 (erase-buffer)
873 (save-excursion
874 (insert "Summary: tag " name "\n\n"))
875 (move-end-of-line 1)
876 (switch-to-buffer buf)
877 (log-edit (lambda ()
878 (interactive)
879 (unwind-protect
880 (vc-got--tag-callback name)
881 (kill-buffer buf))))))))
883 (defun vc-got-retrieve-tag (dir name _update)
884 "Switch to the tag NAME for files at or below DIR."
885 (let ((default-directory dir))
886 (vc-got--update name dir)))
889 ;; Miscellaneous
891 (defun vc-got-find-ignore-file (file)
892 "Return the gitignore file that controls FILE."
893 (expand-file-name ".gitignore"
894 (vc-got-root file)))
896 (defun vc-got-previous-revision (file rev)
897 "Return the revision number that precedes REV for FILE or nil."
898 (with-temp-buffer
899 (vc-got--log file 2 rev nil nil t)
900 (goto-char (point-min))
901 (keep-lines "^commit")
902 (when (looking-at vc-got--commit-re)
903 (match-string-no-properties 1))))
905 (defun vc-got-next-revision (file rev)
906 "Return the revision number that follows REV for FILE or nil."
907 (with-temp-buffer
908 (vc-got--log file nil nil rev)
909 (keep-lines "^commit" (point-min) (point-max))
910 (goto-char (point-max))
911 (forward-line -1) ; return from empty line to last actual commit
912 (unless (= (point) (point-min))
913 (forward-line -1)
914 (when (looking-at vc-got--commit-re)
915 (match-string-no-properties 1)))))
917 (defun vc-got-delete-file (file)
918 "Delete FILE locally and mark it deleted in work tree."
919 (vc-got--remove file t))
921 (defun vc-got-find-file-hook ()
922 "Activate `smerge-mode' if there is a conflict."
923 ;; just like vc-git-find-file-hook
924 (when (and buffer-file-name
925 (eq (vc-state buffer-file-name 'Got) 'conflict)
926 (save-excursion
927 (goto-char (point-min))
928 (re-search-forward "^<<<<<<< " nil 'noerror)))
929 (smerge-start-session)
930 (vc-message-unresolved-conflicts buffer-file-name)))
932 (defun vc-got-conflicted-files (dir)
933 "Return the list of files with conflicts in directory DIR."
934 (let* ((root (vc-got-root dir))
935 (default-directory root)
936 (process-file-side-effects nil))
937 (cl-loop for (file status _) in (vc-got--status "C" ".")
938 when (and (eq status 'conflict)
939 (file-in-directory-p file dir))
940 collect file)))
942 (defun vc-got-repository-url (_file &optional remote-name)
943 "Return URL for REMOTE-NAME, or for \"origin\" if nil."
944 (let* ((default-directory (vc-got--repo-root))
945 (remote-name (or remote-name "origin"))
946 (heading (concat "[remote \"" remote-name "\"]"))
947 (conf (cond ((file-exists-p ".git/config") ".git/config")
948 ((file-exists-p ".git") nil)
949 ((file-exists-p "config") "config")))
950 found)
951 (when conf
952 (with-temp-buffer
953 (insert-file-contents conf)
954 (goto-char (point-min))
955 (when (search-forward heading nil t)
956 (forward-line)
957 (while (and (not found)
958 (looking-at ".*=") ; too broad?
959 (not (= (point) (point-max))))
960 (when (looking-at ".*url = \\(.*\\)")
961 (setq found (match-string-no-properties 1)))
962 (forward-line))
963 found)))))
967 ;; Automatically register the backend and add ".got" to the exclusion
968 ;; list.
970 ;;;###autoload
971 (add-to-list 'vc-handled-backends 'Got)
973 ;;;###autoload
974 (add-to-list 'vc-directory-exclusion-list ".got")
976 (provide 'vc-got)
977 ;;; vc-got.el ends here