Blob


1 ;;; vc-got.el --- VC backend for Game of Trees VCS -*- lexical-binding: t; -*-
3 ;; Copyright (C) 2020, 2021 Omar Polo
5 ;; Author: Omar Polo <op@omarpolo.com>
6 ;; URL: https://git.omarpolo.com/vc-got/
7 ;; Keywords: vc tools
8 ;; Version: 0
9 ;; Package-Requires: ((emacs "27.1"))
11 ;; This program is free software; you can redistribute it and/or modify
12 ;; it under the terms of the GNU General Public License as published by
13 ;; the Free Software Foundation, either version 3 of the License, or
14 ;; (at your option) any later version.
16 ;; This program is distributed in the hope that it will be useful,
17 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 ;; GNU General Public License for more details.
21 ;; You should have received a copy of the GNU General Public License
22 ;; along with this program. If not, see <https://www.gnu.org/licenses/>.
24 ;;; Commentary:
26 ;; This file contains a VC backend for the Game of Trees (got) version
27 ;; control system.
29 ;; Backend implementation status
30 ;;
31 ;; Function marked with `*' are required, those with `-' are optional.
32 ;;
33 ;; FUNCTION NAME STATUS
34 ;;
35 ;; BACKEND PROPERTIES:
36 ;; * revision-granularity DONE
37 ;; - update-on-retrieve-tag XXX: what should this do?
38 ;;
39 ;; STATE-QUERYING FUNCTIONS:
40 ;; * registered DONE
41 ;; * state DONE
42 ;; - dir-status-files DONE
43 ;; - dir-extra-headers DONE
44 ;; - dir-printer DONE
45 ;; - status-fileinfo-extra NOT IMPLEMENTED
46 ;; * working-revision DONE
47 ;; * checkout-model DONE
48 ;; - mode-line-string DONE
49 ;;
50 ;; STATE-CHANGING FUNCTIONS:
51 ;; * create-repo NOT IMPLEMENTED
52 ;; I don't think got init does what this function is supposed to
53 ;; do.
54 ;; * register DONE
55 ;; - responsible-p DONE
56 ;; - receive-file NOT NEEDED, default `register' works fine
57 ;; - unregister DONE
58 ;; * checkin DONE
59 ;; * find-revision DONE
60 ;; * checkout NOT IMPLEMENTED
61 ;; I'm not sure how to properly implement this. Does filling
62 ;; FILE with the find-revision do the trick? Or use got update?
63 ;; * revert DONE
64 ;; - merge-file NOT IMPLEMENTED
65 ;; - merge-branch DONE
66 ;; - merge-news NOT IMPLEMENTED
67 ;; - pull DONE
68 ;; - push DONE
69 ;; uses git
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 NEEDED, `delete' + `register' is enough
115 ;; - find-file-hook DONE
116 ;; - extra-menu NOT IMPLEMENTED, add `import', `integrate', `stage'?
117 ;; - extra-dir-menu NOT IMPLEMENTED, same as above
118 ;; - conflicted-files DONE
119 ;; - repository-url DONE
121 ;; TODO: vc-git has most function that starts with:
122 ;;
123 ;; (let* ((root (vc-git-root default-directory))
124 ;; (buffer (format "*vc-git : %s*" (expand-file-name root)))
125 ;; ...)
126 ;; ...)
127 ;;
128 ;; we should 1) investigate if also other backends do something like
129 ;; this (or if there is a better way) and 2) try to do the same.
131 ;;; Code:
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 (defgroup vc-got nil
150 "VC GoT backend."
151 :group 'vc)
153 (defcustom vc-got-program "got"
154 "Name of the Got executable (excluding any arguments)."
155 :type 'string)
157 (defcustom vc-got-diff-switches t
158 "String or list of strings specifying switches for Got diff under VC.
159 If nil, use the value of `vc-diff-switches'. If t, use no switches."
160 :type '(choice (const :tag "Unspecified" nil)
161 (const :tag "None" t)
162 (string :tag "Argument String")
163 (repeat :tag "Argument List" :value ("") string)))
165 ;; helpers
166 (defun vc-got--program-version ()
167 "Return string representing the got version."
168 (let (process-file-side-effects)
169 (with-temp-buffer
170 (vc-got--call "-V")
171 (substring (buffer-string) 4 -1))))
173 (defun vc-got-root (file)
174 "Return the work tree root for FILE, or nil."
175 (vc-find-root file ".got"))
177 (defmacro vc-got-with-worktree (file &rest body)
178 "Evaluate BODY in the work tree directory of FILE."
179 (declare (indent defun))
180 `(when-let (default-directory (vc-got-root ,file))
181 ,@body))
183 (defun vc-got--repo-root ()
184 "Return the path to the repository root.
185 Assume `default-directory' is inside a got worktree."
186 (vc-got-with-worktree default-directory
187 (with-temp-buffer
188 (insert-file-contents ".got/repository")
189 (string-trim (buffer-string) "" "\n"))))
191 (defun vc-got--call (&rest args)
192 "Call `vc-got-program' with ARGS.
193 The output will be placed in the current buffer."
194 (apply #'process-file vc-got-program nil (current-buffer) nil
195 (cl-remove-if #'null (flatten-list args))))
197 (defun vc-got--add (files)
198 "Add FILES to got, passing `vc-register-switches' to the command invocation."
199 (with-temp-buffer
200 (vc-got--call "add" vc-register-switches "--" files)))
202 (defun vc-got--log (&optional path limit start-commit stop-commit
203 search-pattern reverse)
204 "Execute the log command in the worktree of PATH in the current buffer.
205 LIMIT limits the maximum number of commit returned.
207 START-COMMIT: start traversing history at the specified commit.
208 STOP-COMMIT: stop traversing history at the specified commit.
209 SEARCH-PATTERN: limit to log messages matched by the regexp given.
210 REVERSE: display the log messages in reverse order.
212 Return nil if the command failed or if PATH isn't included in any
213 worktree."
214 (let (process-file-side-effects)
215 (vc-got-with-worktree (or path default-directory)
216 (when (zerop
217 (save-excursion
218 (vc-got--call "log"
219 (when limit (list "-l" (format "%s" limit)))
220 (when start-commit (list "-c" start-commit))
221 (when stop-commit (list "-x" stop-commit))
222 (when search-pattern (list "-s" search-pattern))
223 (when reverse '("-R"))
224 "--"
225 path)))
226 (save-excursion
227 (delete-matching-lines "^-----------------------------------------------$")
228 t)))))
230 (defun vc-got--status (status-codes dir-or-file &optional files)
231 "Return a list of lists '(FILE STATUS STAGE-STATUS).
232 DIR-OR-FILE can be either a directory or a file. If FILES is
233 given, return the status of those files, otherwise the status of
234 DIR-OR-FILE. STATUS-CODES is either nil, or a string that's
235 passed as the -s flag to got status to limit the types of status
236 to report (e.g. \"CD\" to report only conflicts and deleted
237 files)."
238 (with-temp-buffer
239 (let* ((default-directory (expand-file-name
240 (if (file-directory-p dir-or-file)
241 dir-or-file
242 (file-name-directory dir-or-file))))
243 (root (vc-got-root default-directory))
244 (process-file-side-effects))
245 (when (zerop (vc-got--call "status"
246 (when status-codes (list "-s" status-codes))
247 "--"
248 (or files dir-or-file)))
249 (goto-char (point-min))
250 (cl-loop until (eobp)
251 ;; the format of each line is
252 ;; <status-char> <stage-char> <spc> <filename> \n
253 collect (let* ((file-status (prog1 (vc-got--parse-status-char
254 (char-after))
255 (forward-char)))
256 (stage-status (let* ((c (char-after)))
257 (prog1
258 (when (member c '(?M ?A ?D))
259 c)
260 (forward-char))))
261 (filename (progn
262 (forward-char)
263 (buffer-substring (point)
264 (line-end-position)))))
265 (list (file-relative-name (expand-file-name filename root)
266 default-directory)
267 (or file-status (and stage-status 'up-to-date))
268 stage-status))
269 do (forward-line))))))
271 (defun vc-got--parse-status-char (c)
272 "Parse status char C into a symbol accepted by `vc-state'."
273 (cl-case c
274 (?M 'edited)
275 (?A 'added)
276 (?D 'removed)
277 (?C 'conflict)
278 (?! 'missing)
279 (?~ 'edited) ; XXX: what does it means for a file to be ``obstructed''?
280 (?? 'unregistered)
281 (?m 'edited) ; modified file modes
282 (?N nil)))
284 (defun vc-got--tree-parse ()
285 "Parse into an alist the output of got tree -i in the current buffer."
286 (goto-char (point-min))
287 (cl-loop
288 until (= (point) (point-max))
289 collect (let* ((obj-start (point))
290 (_ (forward-word))
291 (obj (buffer-substring obj-start (point)))
292 (_ (forward-char)) ; skip the space
293 (filename-start (point))
294 (_ (move-end-of-line nil))
295 (filename (buffer-substring filename-start (point))))
296 ;; goto the start of the next line
297 (forward-line)
298 (move-beginning-of-line nil)
299 `(,filename . ,obj))))
301 (defun vc-got--tree (commit path)
302 "Return an alist representing the got tree command output.
303 The outputted tree will be localised in the given PATH at the
304 given COMMIT."
305 (vc-got-with-worktree path
306 (let (process-file-side-effects)
307 (with-temp-buffer
308 (when (zerop (vc-got--call "tree" "-c" commit "-i" "--" path))
309 (vc-got--tree-parse))))))
311 (defun vc-got--cat (commit obj-id)
312 "Execute got cat -c COMMIT OBJ-ID in the current buffer."
313 (let (process-file-side-effects)
314 (zerop (vc-got--call "cat" "-c" commit obj-id))))
316 (defun vc-got--revert (&rest files)
317 "Execute got revert FILES."
318 (vc-got-with-worktree (car files)
319 (with-temp-buffer
320 (zerop (vc-got--call "revert" "--" files)))))
322 (defun vc-got--list-branches ()
323 "Return an alist of (branch . commit)."
324 (let (process-file-side-effects)
325 (with-temp-buffer
326 (when (zerop (vc-got--call "branch" "-l"))
327 (goto-char (point-min))
328 (cl-loop
329 until (= (point) (point-max))
330 ;; parse the `* $branchname: $commit', from the end
331 ;; XXX: use a regex?
332 collect (let* ((_ (move-end-of-line nil))
333 (end-commit (point))
334 (_ (backward-word))
335 (start-commit (point))
336 (_ (backward-char 2))
337 (end-branchname (point))
338 (_ (move-beginning-of-line nil))
339 (_ (forward-char 2))
340 (start-branchname (point))
341 (branchname (buffer-substring start-branchname end-branchname))
342 (commit (buffer-substring start-commit end-commit)))
343 (forward-line)
344 (move-beginning-of-line nil)
345 `(,branchname . ,commit)))))))
347 (defun vc-got--current-branch ()
348 "Return the current branch."
349 (let (process-file-side-effects)
350 (with-temp-buffer
351 (when (zerop (vc-got--call "branch"))
352 (string-trim (buffer-string) "" "\n")))))
354 (defun vc-got--integrate (branch)
355 "Integrate BRANCH into the current one."
356 (with-temp-buffer
357 (zerop (vc-got--call "integrate" branch))))
359 (defun vc-got--update (branch &optional paths)
360 "Update to a different commit or BRANCH.
361 Optionally restrict the update operation to files at or within
362 the specified PATHS."
363 (with-temp-buffer
364 (unless (zerop (vc-got--call "update" "-b" branch "--" paths))
365 (error "[vc-got] can't update to branch %s: %s"
366 branch
367 (buffer-string)))))
369 (defun vc-got--diff (&rest files)
370 "Call got diff against FILES.
371 The result will be stored in the current buffer."
372 (let (process-file-side-effects)
373 (zerop (vc-got--call "diff"
374 (vc-switches 'got 'diff)
375 "--"
376 (mapcar #'file-relative-name files)))))
378 (defun vc-got--unstage (file-or-directory)
379 "Unstage all the staged hunks at or within FILE-OR-DIRECTORY.
380 If it's nil, unstage every staged changes across the entire work
381 tree."
382 (zerop (vc-got--call "unstage" "--" file-or-directory)))
384 (defun vc-got--remove (file &optional force keep-local)
385 "Use got to remove FILE.
386 If FORCE is non-nil perform the operation even if a file contains
387 local modification. If KEEP-LOCAL is non-nil keep the affected
388 files on disk."
389 (vc-got-with-worktree (or file default-directory)
390 (with-temp-buffer
391 (zerop (vc-got--call "remove"
392 (when force "-f")
393 (when keep-local "-k")
394 "--"
395 file)))))
397 (defun vc-got--ref ()
398 "Return a list of all references."
399 (let (process-file-side-effects
400 (re "^refs/\\(heads\\|remotes\\|tags\\)/\\(.*\\):")
401 ;; hardcoding HEAD because it's always present and the regexp
402 ;; won't match it.
403 (table (list "HEAD")))
404 (vc-got-with-worktree default-directory
405 (with-temp-buffer
406 (when (zerop (vc-got--call "ref" "-l"))
407 (goto-char (point-min))
408 (while (re-search-forward re nil t)
409 (push (match-string 2) table))
410 table)))))
412 (defun vc-got--branch (name)
413 "Try to create and switch to the branch called NAME."
414 (let (process-file-side-effects)
415 (vc-got-with-worktree default-directory
416 (with-temp-buffer
417 (if (zerop (vc-got--call "branch" "--" name))
419 (error "[vc-got] can't create branch %s: %s" name
420 (buffer-string)))))))
423 ;; Backend properties
425 (defun vc-got-revision-granularity ()
426 "Got has REPOSITORY granularity."
427 'repository)
429 ;; XXX: what this should do? The description is not entirely clear
430 (defun vc-got-update-on-retrieve-tag ()
431 nil)
434 ;; State-querying functions
436 ;;;###autoload (defun vc-got-registered (file)
437 ;;;###autoload "Return non-nil if FILE is registered with got."
438 ;;;###autoload (when (vc-find-root file ".got")
439 ;;;###autoload (load "vc-got" nil t)
440 ;;;###autoload (vc-got-registered file)))
442 (defun vc-got-registered (file)
443 "Return non-nil if FILE is registered with got."
444 (if (file-directory-p file)
445 nil ; got doesn't track directories
446 (when (vc-find-root file ".got")
447 (let ((s (vc-got-state file)))
448 (not (or (eq s 'unregistered)
449 (null s)))))))
451 (defun vc-got-state (file)
452 "Return the current version control state of FILE. See `vc-state'."
453 (unless (file-directory-p file)
454 (let (process-file-side-effects)
455 ;; Manually calling got status and checking the result inline to
456 ;; avoid building the data structure in vc-got--status.
457 (with-temp-buffer
458 (when (zerop (vc-got--call "status" "--" file))
459 (goto-char (point-min))
460 (if (eobp)
461 'up-to-date
462 (vc-got--parse-status-char (char-after))))))))
464 (defun vc-got--dir-filter-files (files)
465 "Remove ., .. and .got from FILES."
466 (cl-loop for file in files
467 unless (or (string= file "..")
468 (string= file ".")
469 (string= file ".got"))
470 collect file))
472 (defun vc-got-dir-status-files (dir files update-function)
473 "Build the status for FILES in DIR.
474 The builded result is given to the callback UPDATE-FUNCTION. If
475 FILES is nil, consider all the files in DIR."
476 (let* ((fs (vc-got--dir-filter-files (or files (directory-files dir))))
477 ;; XXX: we call with files, wich will probably be nil on the
478 ;; first run, so we catch deleted, missing and edited files
479 ;; in subdirectories.
480 (res (vc-got--status nil dir files))
481 double-check)
482 (cl-loop for file in fs
483 do (when (and (not (cdr (assoc file res #'string=)))
484 (not (file-directory-p file))
485 ;; if file doesn't exists, it's a
486 ;; untracked file that was removed.
487 (file-exists-p file))
488 ;; if we don't know the status of a file here, it's
489 ;; either up-to-date or ignored. Save it for a
490 ;; double check
491 (push file double-check)))
492 (cl-loop with statuses = (vc-got--status nil dir double-check)
493 for file in double-check
494 unless (eq 'unregistered (cadr (assoc file statuses #'string=)))
495 do (push (list file 'up-to-date nil) res))
496 (funcall update-function res nil)))
498 (defun vc-got-dir-extra-headers (dir)
499 "Return a string for the `vc-dir' buffer heading for directory DIR."
500 (let ((remote (vc-got-repository-url dir)))
501 (concat (propertize "Repository : " 'face 'font-lock-type-face)
502 (vc-got--repo-root) "\n"
503 (when remote
504 (concat
505 (propertize "Remote URL : " 'face 'font-lock-type-face)
506 (vc-got-repository-url dir) "\n"))
507 (propertize "Branch : " 'face 'font-lock-type-face)
508 (vc-got--current-branch))))
510 (defun vc-got-dir-printer (info)
511 "Pretty-printer for the vc-dir-fileinfo structure INFO."
512 (let* ((isdir (vc-dir-fileinfo->directory info))
513 (state (if isdir "" (vc-dir-fileinfo->state info)))
514 (stage-state (vc-dir-fileinfo->extra info))
515 (filename (vc-dir-fileinfo->name info)))
516 (insert
517 " "
518 (propertize
519 (format "%c" (if (vc-dir-fileinfo->marked info) ?* ? ))
520 'face 'font-lock-type-face)
521 " "
522 (propertize
523 (format "%-12s" state)
524 'face (cond ((eq state 'up-to-date) 'font-lock-builtin-face)
525 ((memq state '(missing conflict)) 'font-lock-warning-face)
526 ((eq state 'edited) 'font-lock-constant-face)
527 (t 'font-lock-variable-name-face))
528 'mouse-face 'highlight
529 'keymap vc-dir-status-mouse-map)
531 " " (propertize
532 (if stage-state
533 (format "%c" stage-state)
534 " ")
535 'face (cond ((memq stage-state '(?A ?E)) 'font-lock-constant-face)
536 ((eq stage-state ?R) 'font-lock-warning-face)
537 (t 'font-lock-variable-name-face)))
538 " "
539 (propertize filename
540 'face (if isdir 'font-lock-comment-delimiter-face
541 'font-lock-function-name-face)
542 'help-echo
543 (if isdir
544 "Directory\nVC operations can be applied to it\nmouse-3: Pop-up menu"
545 "File\nmouse-3: Pop-up menu")
546 'mouse-face 'highlight
547 'keymap vc-dir-filename-mouse-map))))
549 (defun vc-got-working-revision (file)
550 "Return the id of the last commit that touched the FILE or \"0\" for a new (but added) file."
551 (or
552 (with-temp-buffer
553 (when (vc-got--log file 1)
554 (let (start)
555 (goto-char (point-min))
556 (forward-word) ; skip "commit"
557 (forward-char) ; skip the space
558 (setq start (point)) ; store start of the SHA
559 (forward-word) ; goto SHA end
560 (buffer-substring start (point)))))
561 ;; special case: if this file is added but has no previous commits
562 ;; touching it, got log will fail (as expected), but we have to
563 ;; return "0".
564 (when (eq (vc-got-state file) 'added)
565 "0")))
567 (defun vc-got-checkout-model (_files)
568 "Got uses an implicit checkout model for every file."
569 'implicit)
571 (defun vc-got-mode-line-string (file)
572 "Return the VC mode line string for FILE."
573 (vc-got-with-worktree file
574 (let ((def (vc-default-mode-line-string 'Got file)))
575 (concat (substring def 0 4) (vc-got--current-branch)))))
578 ;; state-changing functions
580 (defun vc-got-create-repo (_backend)
581 "Create an empty repository in the current directory."
582 (error "[vc-got] create-repo not implemented"))
584 (defun vc-got-register (files &optional _comment)
585 "Register FILES, passing `vc-register-switches' to the backend command."
586 (vc-got--add files))
588 (defalias 'vc-got-responsible-p #'vc-got-root)
590 (defun vc-got-unregister (file)
591 "Unregister FILE."
592 (vc-got--remove file t t))
594 (defun vc-got-checkin (files comment &optional _rev)
595 "Commit FILES with COMMENT as commit message."
596 (with-temp-buffer
597 (unless (zerop (vc-got--call "commit" "-m"
598 (log-edit-extract-headers nil comment)
599 "--"
600 files))
601 (error "[vc-got] can't commit: %s" (buffer-string)))))
603 (defun vc-got-find-revision (file rev buffer)
604 "Fill BUFFER with the content of FILE in the given revision REV."
605 (when-let (obj-id (assoc file (vc-got--tree rev file) #'string=))
606 (with-current-buffer buffer
607 (vc-got-with-worktree file
608 (vc-got--cat rev obj-id)))))
610 (defun vc-got-checkout (_file &optional _rev)
611 "Checkout revision REV of FILE.
612 If REV is t, checkout from the head."
613 (error "[vc-got] checkout not implemented"))
615 (defun vc-got-revert (file &optional _content-done)
616 "Revert FILE back to working revision."
617 (vc-got--revert file))
619 (defun vc-got-merge-branch ()
620 "Prompt for a branch and integrate it into the current one."
621 ;; XXX: be smart and try to "got rebase" if "got integrate" fails?
622 (let* ((branches (cl-loop for (branch . commit) in (vc-got--list-branches)
623 collect branch))
624 (branch (completing-read "Merge from branch: " branches)))
625 (when branch
626 (vc-got--integrate branch))))
628 (defun vc-got--push-pull (cmd op prompt)
629 "Execute CMD OP, or prompt the user if PROMPT is non-nil."
630 (let ((buffer (format "*vc-got : %s*" (expand-file-name default-directory))))
631 (when-let (cmd (if prompt
632 (split-string
633 (read-shell-command (format "%s %s command: " cmd op)
634 (format "%s %s " cmd op))
635 " " t)
636 (list cmd op)))
637 (apply #'vc-do-async-command buffer default-directory cmd)
638 ;; this comes from vc-git.el. We're using git to push, so in
639 ;; part it makes sense, but we should revisit for full Got
640 ;; support.
641 (with-current-buffer buffer
642 (vc-compilation-mode 'git)
643 (let ((comp-cmd (mapconcat #'identity cmd " ")))
644 (setq-local compile-command comp-cmd
645 compilation-directory default-directory
646 compilation-arguments (list comp-cmd
647 nil
648 (lambda (_ign) buffer)
649 nil))))
650 (vc-set-async-update buffer))))
652 ;; TODO: this can be expanded. See whan omyksh does:
653 ;; function got-sync {
654 ;; local _remote _info _branch
655 ;; _remote=$1
656 ;; _info="$(got info)"
657 ;; _branch="$(echo "$_info" | awk '/branch reference:/ {l = split($NF, a, "/"); print a[l]}')"
658 ;; [ -z $_remote ] && _remote="origin"
659 ;; [ -z $_branch ] && _branch="main"
660 ;; got fetch "$_remote" && got update -b "$_remote/$_branch" && \
661 ;; got rebase $_branch
662 ;; }
663 (defun vc-got-pull (prompt)
664 "Execute got pull, prompting the user for the full command if PROMPT is not nil."
665 (let ((default-directory (vc-got-root default-directory)))
666 (vc-got--push-pull vc-got-program "fetch" prompt)))
668 (defun vc-got-push (prompt)
669 "Run git push (not got!) in the repository dir.
670 If PROMPT is non-nil, prompt for the git command to run."
671 (let ((default-directory (vc-got--repo-root)))
672 (vc-got--push-pull "git" "push" prompt)))
675 ;; History functions
677 (defun vc-got-print-log (files buffer &optional _shortlog start-revision limit)
678 "Insert the revision log for FILES into BUFFER.
679 LIMIT limits the number of commits, optionally starting at
680 START-REVISION."
681 (with-current-buffer buffer
682 ;; the *vc-diff* may be read only
683 (let ((inhibit-read-only t))
684 (cl-loop for file in files
685 do (vc-got--log (file-relative-name file) limit start-revision)))))
687 ;; XXX: this includes also the latest commit in REMOTE-LOCATION.
688 (defun vc-got-log-outgoing (buffer remote-location)
689 "Fill BUFFER with the diff between the local worktree branch and REMOTE-LOCATION."
690 (vc-setup-buffer buffer)
691 (let ((rl (if (or (not remote-location) (string-empty-p remote-location))
692 (concat "origin/" (vc-got--current-branch))
693 remote-location))
694 (inhibit-read-only t))
695 (with-current-buffer buffer
696 (vc-got--log nil nil nil rl))))
698 (defun vc-got-incoming (buffer remote-location)
699 "Fill BUFFER with the diff between the REMOTE-LOCATION and the local worktree branch."
700 (let ((rl (if (or (not remote-location) (string-empty-p remote-location))
701 (concat "origin/" (vc-got--current-branch))
702 remote-location))
703 (inhibit-read-only t))
704 (with-current-buffer buffer
705 (vc-got--log nil nil (vc-got--current-branch) rl))))
707 (defun vc-got-log-search (buffer pattern)
708 "Search commits for PATTERN and write the results found in BUFFER."
709 (with-current-buffer buffer
710 (let ((inhibit-read-only t))
711 (vc-got--log nil nil nil nil pattern))))
713 (define-derived-mode vc-got-log-view-mode log-view-mode "Got-Log-View"
714 "Got-specific log-view mode.
715 Heavily inspired by `vc-git-log-view-mode'."
716 (require 'add-log)
717 (setq-local
718 log-view-file-re regexp-unmatchable
719 log-view-per-file-logs nil
720 log-view-message-re "^commit +\\([0-9a-z]+\\)"
722 log-view-font-lock-keywords
723 (append
724 `((,log-view-message-re (1 'change-log-acknowledgment)))
725 ;; Handle the case:
726 ;; user: foo@bar
727 '(("^from: \\([A-Za-z0-9_.+-]+@[A-Za-z0-9_.-]+\\)"
728 (1 'change-log-email))
729 ;; Handle the case:
730 ;; user: FirstName LastName <foo@bar>
731 ("^from: \\([^<(]+?\\)[ \t]*[(<]\\([A-Za-z0-9_.+-]+@[A-Za-z0-9_.-]+\\)[>)]"
732 (1 'change-log-name)
733 (2 'change-log-email))
734 ("^date: \\(.+\\)" (1 'change-log-date))))))
736 ;; TODO: async
737 ;; TODO: return 0 or 1
738 (defun vc-got-diff (files &optional rev1 rev2 buffer _async)
739 "Insert into BUFFER (or *vc-diff*) the diff for FILES from REV1 to REV2."
740 (let* ((buffer (get-buffer-create (or buffer "*vc-diff*")))
741 (inhibit-read-only t))
742 (with-current-buffer buffer
743 (vc-got-with-worktree (or (car files)
744 default-directory)
745 (if (and (null rev1)
746 (null rev2))
747 (dolist (file files)
748 (vc-got--diff file))
749 ;; TODO: if rev1 is nil, diff from the current version until
750 ;; rev2.
751 ;;
752 ;; TODO: if rev2 is nil as well, diff against an empty tree
753 ;; (i.e. get the patch from `got log -p rev1')
754 ;;
755 ;; TODO: it would be nice to optionally include FILES here,
756 ;; it would make the `=' key on the *Annotate* buffer do the
757 ;; right thing, but AFAICS got doesn't provide something
758 ;; like this. Probably only hacking something with ``log
759 ;; -p'' and filtering?
760 (vc-got--diff rev1 rev2))))))
762 (defun vc-got-revision-completion-table (_files)
763 "Return a completion table for existing revisions.
764 Ignores FILES because GoT doesn't have the concept of ``file
765 revisions''; instead, like with git, you have tags and branches."
766 (letrec ((table (lazy-completion-table
767 table (lambda () (vc-got--ref)))))
768 table))
770 (defun vc-got-annotate-command (file buf &optional rev)
771 "Show annotated contents of FILE in buffer BUF. If given, use revision REV."
772 (let (process-file-side-effects)
773 (with-current-buffer buf
774 ;; FIXME: vc-ensure-vc-buffer won't recognise this buffer as managed
775 ;; by got unless vc-parent-buffer points to a buffer managed by got.
776 ;; investigate why this is needed.
777 (set (make-local-variable 'vc-parent-buffer) (find-file-noselect file))
778 (vc-got--call "blame"
779 (when rev (list "-c" rev))
780 "--"
781 file))))
783 (defconst vc-got--annotate-re
784 (concat "^[0-9]\\{1,\\}) " ; line number followed by )
785 "\\([a-z0-9]+\\) " ; SHA-1 of commit
786 "\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\) " ; year-mm-dd
787 "\\([^ ]\\)+ ") ; author
788 "Regexp to match annotation output lines.
790 Provides capture groups for:
791 1. revision id
792 2. date of commit
793 3. author of commit")
795 (defconst vc-got--commit-re "^commit \\([a-z0-9]+\\)"
796 "Regexp to match commit lines.
798 Provides capture group for the commit revision id.")
800 (defun vc-got-annotate-time ()
801 "Return the time of the next line of annotation at or after point.
802 Value is returned as floating point fractional number of days."
803 (save-excursion
804 (beginning-of-line)
805 (when (looking-at vc-got--annotate-re)
806 (let ((str (match-string-no-properties 2)))
807 (vc-annotate-convert-time
808 (encode-time 0 0 0
809 (string-to-number (substring str 8 10))
810 (string-to-number (substring str 5 7))
811 (string-to-number (substring str 0 4))))))))
813 (defun vc-got-annotate-extract-revision-at-line ()
814 "Return revision corresponding to the current line or nil."
815 (save-excursion
816 (beginning-of-line)
817 (when (looking-at vc-got--annotate-re)
818 (match-string-no-properties 1))))
821 ;; Tag system
823 (defun vc-got--tag-callback (tag)
824 "`log-edit' callback for `vc-got-create-tag'.
825 Creates the TAG using the content of the current buffer."
826 (interactive)
827 (let ((msg (buffer-substring-no-properties (point-min)
828 (point-max))))
829 (with-temp-buffer
830 (unless (zerop (vc-got--call "tag" "-m" msg "--" tag))
831 (error "[vc-got] can't create tag %s: %s" tag (buffer-string))))))
833 (defun vc-got-create-tag (_dir name branchp)
834 "Attach the tag NAME to the state of the worktree.
835 DIR is ignored (tags are global, not per-file). If BRANCHP is
836 true, NAME should create a new branch otherwise it will pop-up a
837 `log-edit' buffer to provide the tag message."
838 ;; TODO: vc reccomends to ensure that all the file are in a clean
839 ;; state, but is it useful?
840 (if branchp
841 (vc-got--branch name)
842 (let ((buf (get-buffer-create "*vc-got tag*")))
843 (with-current-buffer buf
844 (erase-buffer)
845 (switch-to-buffer buf)
846 (log-edit (lambda ()
847 (interactive)
848 (unwind-protect
849 (vc-got--tag-callback name)
850 (kill-buffer buf))))))))
852 (defun vc-got-retrieve-tag (dir name _update)
853 "Switch to the tag NAME for files at or below DIR."
854 (let ((default-directory dir))
855 (vc-got--update name dir)))
858 ;; Miscellaneous
860 (defun vc-got-find-ignore-file (file)
861 "Return the gitignore file that controls FILE."
862 (expand-file-name ".gitignore"
863 (vc-got-root file)))
865 (defun vc-got-previous-revision (file rev)
866 "Return the revision number that precedes REV for FILE, or nil if no such revision exists."
867 (with-temp-buffer
868 (vc-got--log file 2 rev nil nil t)
869 (goto-char (point-min))
870 (keep-lines "^commit")
871 (when (looking-at vc-got--commit-re)
872 (match-string-no-properties 1))))
874 (defun vc-got-next-revision (file rev)
875 "Return the revision number that follows REV for FILE, or nil if no such revision exists."
876 (with-temp-buffer
877 (vc-got--log file nil nil rev)
878 (keep-lines "^commit" (point-min) (point-max))
879 (goto-char (point-max))
880 (forward-line -1) ; return from empty line to last actual commit
881 (unless (= (point) (point-min))
882 (forward-line -1)
883 (when (looking-at vc-got--commit-re)
884 (match-string-no-properties 1)))))
886 (defun vc-got-delete-file (file)
887 "Delete FILE locally and mark it deleted in work tree."
888 (vc-got--remove file t))
890 (defun vc-got-find-file-hook ()
891 "Activate `smerge-mode' if there is a conflict."
892 ;; just like vc-git-find-file-hook
893 (when (and buffer-file-name
894 (eq (vc-state buffer-file-name 'Got) 'conflict)
895 (save-excursion
896 (goto-char (point-min))
897 (re-search-forward "^<<<<<<< " nil 'noerror)))
898 (smerge-start-session)
899 (vc-message-unresolved-conflicts buffer-file-name)))
901 (defun vc-got-conflicted-files (dir)
902 "Return the list of files with conflicts in directory DIR."
903 (let* ((root (vc-got-root dir))
904 (default-directory root)
905 (process-file-side-effects))
906 (cl-loop with conflicts = nil
907 for (file status _) in (vc-got--status "C" ".")
908 do (when (and (eq status 'conflict)
909 (file-in-directory-p file dir))
910 (push file conflicts))
911 finally return conflicts)))
913 (defun vc-got-repository-url (_file &optional remote-name)
914 "Return URL for REMOTE-NAME, or for \"origin\" if nil."
915 (let* ((default-directory (vc-got--repo-root))
916 (remote-name (or remote-name "origin"))
917 (heading (concat "[remote \"" remote-name "\"]"))
918 (conf (cond ((file-exists-p ".git/config") ".git/config")
919 ((file-exists-p ".git") nil)
920 ((file-exists-p "config") "config")))
921 found)
922 (when conf
923 (with-temp-buffer
924 (insert-file-contents conf)
925 (goto-char (point-min))
926 (when (search-forward heading nil t)
927 (forward-line)
928 (while (and (not found)
929 (looking-at ".*=") ; too broad?
930 (not (= (point) (point-max))))
931 (when (looking-at ".*url = \\(.*\\)")
932 (setq found (match-string-no-properties 1)))
933 (forward-line))
934 found)))))
936 (provide 'vc-got)
937 ;;; vc-got.el ends here