1 ;;; vc-got.el --- Game of Tree backend for VC -*- lexical-binding: t; -*-
3 ;; Copyright (C) 2020 Omar Polo
5 ;; Author: Omar Polo <op@omarpolo.com>
6 ;; URL: https://git.omarpolo.com/vc-got/
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/>.
26 ;; Backend implementation status
28 ;; Function marked with `*' are required, those with `-' are optional.
30 ;; FUNCTION NAME STATUS
32 ;; BACKEND PROPERTIES:
33 ;; * revision-granularity DONE
34 ;; - update-on-retrieve-tag XXX: what should this do?
36 ;; STATE-QUERYING FUNCTIONS:
39 ;; - dir-status-files DONE
40 ;; - dir-extra-headers DONE
42 ;; - status-fileinfo-extra NOT IMPLEMENTED
43 ;; * working-revision DONE
44 ;; * checkout-model DONE
45 ;; - mode-line-string DONE
47 ;; STATE-CHANGING FUNCTIONS:
48 ;; * create-repo NOT IMPLEMENTED
49 ;; I don't think got init does what this function is supposed to
52 ;; - responsible-p DONE
53 ;; - receive-file NOT NEEDED, default `register' works fine
56 ;; * find-revision DONE
57 ;; * checkout NOT IMPLEMENTED
58 ;; I'm not sure how to properly implement this. Does filling
59 ;; FILE with the find-revision do the trick? Or use got update?
61 ;; - merge-file NOT IMPLEMENTED
62 ;; - merge-branch DONE
63 ;; - merge-news NOT IMPLEMENTED
67 ;; - steal-lock NOT NEEDED, `got' is not using locks
68 ;; - modify-change-comment NOT IMPLEMENTED
69 ;; can be implemented via histedit, if I understood correctly
70 ;; what it is supposed to do.
71 ;; - mark-resolved NOT IMPLEMENTED
72 ;; - find-admin-dir NOT IMPLEMENTED
76 ;; * log-outgoing DONE
77 ;; * log-incoming DONE
79 ;; - log-view-mode DONE
80 ;; - show-log-entry NOT IMPLEMENTED
81 ;; - comment-history NOT IMPLEMENTED
82 ;; - update-changelog NOT IMPLEMENTED
84 ;; - revision-completion-table DONE
85 ;; - annotate-command DONE
86 ;; - annotate-time DONE
87 ;; - annotate-current-time NOT NEEDED
88 ;; the default time handling is enough.
89 ;; - annotate-extract-revision-at-line DONE
90 ;; - region-history NOT IMPLEMENTED
91 ;; - region-history-mode NOT IMPLEMENTED
92 ;; - mergebase NOT IMPLEMENTED
96 ;; - retrieve-tag DONE
98 ;; MISCELLANEOUS NOT IMPLEMENTED
99 ;; - make-version-backups-p NOT NEEDED, `got' works fine locally
101 ;; - ignore NOT NEEDED, the default action is good
102 ;; - ignore-completion-table NOT NEEDED, the default action is good
103 ;; - find-ignore-file DONE
104 ;; - previous-revision DONE
105 ;; - next-revision DONE
106 ;; - log-edit-mode NOT IMPLEMENTED
107 ;; - check-headers NOT NEEDED, `got' does not use headers
108 ;; - delete-file DONE
109 ;; - rename-file NOT NEEDED, `delete' + `register' is enough
110 ;; - find-file-hook DONE
111 ;; - extra-menu NOT IMPLEMENTED, add `import', `integrate', `stage'?
112 ;; - extra-dir-menu NOT IMPLEMENTED, same as above
113 ;; - conflicted-files DONE
114 ;; - repository-url DONE
116 ;; TODO: vc-git has most function that starts with:
118 ;; (let* ((root (vc-git-root default-directory))
119 ;; (buffer (format "*vc-git : %s*" (expand-file-name root)))
123 ;; we should 1) investigate if also other backends do something like
124 ;; this (or if there is a better way) and 2) try to do the same.
134 (require 'vc-annotate)
136 ;; FIXME: avoid loading this? We only need it for
137 ;; vc-dir-filename-mouse-map in our custom printer.
144 (defcustom vc-got-program "got"
145 "Name of the Got executable (excluding any arguments)."
148 (defcustom vc-got-diff-switches t
149 "String or list of strings specifying switches for Got diff under VC.
150 If nil, use the value of `vc-diff-switches'. If t, use no switches."
151 :type '(choice (const :tag "Unspecified" nil)
152 (const :tag "None" t)
153 (string :tag "Argument String")
154 (repeat :tag "Argument List" :value ("") string)))
157 (defun vc-got--program-version ()
158 "Return string representing the got version."
159 (let (process-file-side-effects)
162 (substring (buffer-string) 4 -1))))
164 (defun vc-got-root (file)
165 "Return the work tree root for FILE, or nil."
166 (vc-find-root file ".got"))
168 (defmacro vc-got-with-worktree (file &rest body)
169 "Evaluate BODY in the work tree directory of FILE."
170 (declare (indent defun))
171 `(when-let (default-directory (vc-got-root ,file))
174 (defun vc-got--repo-root ()
175 "Return the path to the repository root.
176 Assume `default-directory' is inside a got worktree."
177 (vc-got-with-worktree default-directory
179 (insert-file-contents ".got/repository")
180 (string-trim (buffer-string) "" "\n"))))
182 (defun vc-got--call (&rest args)
183 "Call `vc-got-program' with ARGS.
184 The output will be placed in the current buffer."
185 (apply #'process-file vc-got-program nil (current-buffer) nil
186 (cl-remove-if #'null (flatten-list args))))
188 (defun vc-got--add (files)
189 "Add FILES to got, passing `vc-register-switches' to the command invocation."
191 (vc-got--call "add" vc-register-switches "--" files)))
193 (defun vc-got--log (&optional path limit start-commit stop-commit
194 search-pattern reverse)
195 "Execute the log command in the worktree of PATH in the current buffer.
196 LIMIT limits the maximum number of commit returned.
198 START-COMMIT: start traversing history at the specified commit.
199 STOP-COMMIT: stop traversing history at the specified commit.
200 SEARCH-PATTERN: limit to log messages matched by the regexp given.
201 REVERSE: display the log messages in reverse order.
203 Return nil if the command failed or if PATH isn't included in any
205 (let (process-file-side-effects)
206 (vc-got-with-worktree (or path default-directory)
210 (when limit (list "-l" (format "%s" limit)))
211 (when start-commit (list "-c" start-commit))
212 (when stop-commit (list "-x" stop-commit))
213 (when search-pattern (list "-s" search-pattern))
214 (when reverse '("-R"))
218 (delete-matching-lines "^-----------------------------------------------$")
221 (defun vc-got--status (status-codes dir-or-file &optional files)
222 "Return a list of lists '(FILE STATUS STAGE-STATUS).
223 DIR-OR-FILE can be either a directory or a file. If FILES is
224 given, return the status of those files, otherwise the status of
225 DIR-OR-FILE. STATUS-CODES is either nil, or a string that's
226 passed as the -s flag to got status to limit the types of status
227 to report (e.g. \"CD\" to report only conflicts and deleted
230 (let* ((default-directory (expand-file-name
231 (if (file-directory-p dir-or-file)
233 (file-name-directory dir-or-file))))
234 (root (vc-got-root default-directory))
235 (process-file-side-effects))
236 (when (zerop (vc-got--call "status"
237 (when status-codes (list "-s" status-codes))
239 (or files dir-or-file)))
240 (goto-char (point-min))
241 (cl-loop until (eobp)
242 ;; the format of each line is
243 ;; <status-char> <stage-char> <spc> <filename> \n
244 collect (let* ((file-status (prog1 (vc-got--parse-status-char
247 (stage-status (prog1 (vc-got--parse-stage-char
252 (buffer-substring (point)
253 (line-end-position)))))
254 (list (file-relative-name (expand-file-name filename root)
256 (or file-status (and stage-status 'staged))
258 do (forward-line))))))
260 (defun vc-got--parse-status-char (c)
261 "Parse status char C into a symbol accepted by `vc-state'."
268 (?~ 'edited) ; XXX: what does it means for a file to be ``obstructed''?
270 (?m 'edited) ; modified file modes
273 (defun vc-got--parse-stage-char (c)
274 "Parse the stage status char C into a symbol."
280 (defun vc-got--tree-parse ()
281 "Parse into an alist the output of got tree -i in the current buffer."
282 (goto-char (point-min))
284 until (= (point) (point-max))
285 collect (let* ((obj-start (point))
287 (obj (buffer-substring obj-start (point)))
288 (_ (forward-char)) ; skip the space
289 (filename-start (point))
290 (_ (move-end-of-line nil))
291 (filename (buffer-substring filename-start (point))))
292 ;; goto the start of the next line
294 (move-beginning-of-line nil)
295 `(,filename . ,obj))))
297 (defun vc-got--tree (commit path)
298 "Return an alist representing the got tree command output.
299 The outputted tree will be localised in the given PATH at the
301 (vc-got-with-worktree path
302 (let (process-file-side-effects)
304 (when (zerop (vc-got--call "tree" "-c" commit "-i" "--" path))
305 (vc-got--tree-parse))))))
307 (defun vc-got--cat (commit obj-id)
308 "Execute got cat -c COMMIT OBJ-ID in the current buffer."
309 (let (process-file-side-effects)
310 (zerop (vc-got--call "cat" "-c" commit obj-id))))
312 (defun vc-got--revert (&rest files)
313 "Execute got revert FILES."
314 (vc-got-with-worktree (car files)
316 (zerop (vc-got--call "revert" "--" files)))))
318 (defun vc-got--list-branches ()
319 "Return an alist of (branch . commit)."
320 (let (process-file-side-effects)
322 (when (zerop (vc-got--call "branch" "-l"))
323 (goto-char (point-min))
325 until (= (point) (point-max))
326 ;; parse the `* $branchname: $commit', from the end
328 collect (let* ((_ (move-end-of-line nil))
331 (start-commit (point))
332 (_ (backward-char 2))
333 (end-branchname (point))
334 (_ (move-beginning-of-line nil))
336 (start-branchname (point))
337 (branchname (buffer-substring start-branchname end-branchname))
338 (commit (buffer-substring start-commit end-commit)))
340 (move-beginning-of-line nil)
341 `(,branchname . ,commit)))))))
343 (defun vc-got--current-branch ()
344 "Return the current branch."
345 (let (process-file-side-effects)
347 (when (zerop (vc-got--call "branch"))
348 (string-trim (buffer-string) "" "\n")))))
350 (defun vc-got--integrate (branch)
351 "Integrate BRANCH into the current one."
353 (zerop (vc-got--call "integrate" branch))))
355 (defun vc-got--update (branch &optional paths)
356 "Update to a different commit or BRANCH.
357 Optionally restrict the update operation to files at or within
358 the specified PATHS."
360 (unless (zerop (vc-got--call "update" "-b" branch "--" paths))
361 (error "[vc-got] can't update to branch %s: %s"
365 (defun vc-got--diff (&rest args)
366 "Call got diff with ARGS. The result will be stored in the current buffer."
367 (let (process-file-side-effects)
368 (zerop (vc-got--call "diff"
369 (vc-switches 'got 'diff)
371 (mapcar #'file-relative-name args)))))
373 (defun vc-got--unstage (file-or-directory)
374 "Unstage all the staged hunks at or within FILE-OR-DIRECTORY.
375 If it's nil, unstage every staged changes across the entire work
377 (zerop (vc-got--call "unstage" "--" file-or-directory)))
379 (defun vc-got--remove (file &optional force keep-local)
380 "Use got to remove FILE.
381 If FORCE is non-nil perform the operation even if a file contains
382 local modification. If KEEP-LOCAL is non-nil keep the affected
384 (vc-got-with-worktree (or file default-directory)
386 (zerop (vc-got--call "remove"
388 (when keep-local "-k")
392 (defun vc-got--ref ()
393 "Return a list of all references."
394 (let (process-file-side-effects
395 (re "^refs/\\(heads\\|remotes\\|tags\\)/\\(.*\\):")
396 ;; hardcoding HEAD because it's always present and the regexp
398 (table (list "HEAD")))
399 (vc-got-with-worktree default-directory
401 (when (zerop (vc-got--call "ref" "-l"))
402 (goto-char (point-min))
403 (while (re-search-forward re nil t)
404 (push (match-string 2) table))
407 (defun vc-got--branch (name)
408 "Try to create and switch to the branch called NAME."
409 (let (process-file-side-effects)
410 (vc-got-with-worktree default-directory
412 (if (zerop (vc-got--call "branch" "--" name))
414 (error "[vc-got] can't create branch %s: %s" name
415 (buffer-string)))))))
418 ;; Backend properties
420 (defun vc-got-revision-granularity ()
421 "Got has REPOSITORY granularity."
424 ;; XXX: what this should do? The description is not entirely clear
425 (defun vc-got-update-on-retrieve-tag ()
429 ;; State-querying functions
431 ;;;###autoload (defun vc-got-registered (file)
432 ;;;###autoload "Return non-nil if FILE is registered with got."
433 ;;;###autoload (when (vc-find-root file ".got")
434 ;;;###autoload (load "vc-got" nil t)
435 ;;;###autoload (vc-got-registered file)))
437 (defun vc-got-registered (file)
438 "Return non-nil if FILE is registered with got."
439 (if (file-directory-p file)
440 nil ; got doesn't track directories
441 (when (vc-find-root file ".got")
442 (let ((s (vc-got-state file)))
443 (not (or (eq s 'unregistered)
446 (defun vc-got-state (file)
447 "Return the current version control state of FILE. See `vc-state'."
448 (unless (file-directory-p file)
449 (let (process-file-side-effects)
450 ;; Manually calling got status and checking the result inline to
451 ;; avoid building the data structure in vc-got--status.
453 (when (zerop (vc-got--call "status" "--" file))
454 (goto-char (point-min))
457 (vc-got--parse-status-char (char-after))))))))
459 (defun vc-got--dir-filter-files (files)
460 "Remove ., .. and .got from FILES."
461 (cl-loop for file in files
462 unless (or (string= file "..")
464 (string= file ".got"))
467 (defun vc-got-dir-status-files (dir files update-function)
468 "Build the status for FILES in DIR.
469 The builded result is given to the callback UPDATE-FUNCTION. If
470 FILES is nil, consider all the files in DIR."
471 (let* ((fs (vc-got--dir-filter-files (or files (directory-files dir))))
472 ;; XXX: we call with files, wich will probably be nil on the
473 ;; first run, so we catch deleted, missing and edited files
474 ;; in subdirectories.
475 (res (vc-got--status nil dir files))
477 (cl-loop for file in fs
478 do (when (and (not (cdr (assoc file res #'string=)))
479 (not (file-directory-p file))
480 ;; if file doesn't exists, it's a
481 ;; untracked file that was removed.
482 (file-exists-p file))
483 ;; if we don't know the status of a file here, it's
484 ;; either up-to-date or ignored. Save it for a
486 (push file double-check)))
487 (cl-loop with statuses = (vc-got--status nil dir double-check)
488 for file in double-check
489 unless (eq 'unregistered (cadr (assoc file statuses #'string=)))
490 do (push (list file 'up-to-date nil) res))
491 (funcall update-function res nil)))
493 (defun vc-got-dir-extra-headers (dir)
494 "Return a string for the `vc-dir' buffer heading for directory DIR."
495 (let ((remote (vc-got-repository-url dir)))
496 (concat (propertize "Repository : " 'face 'font-lock-type-face)
497 (vc-got--repo-root) "\n"
500 (propertize "Remote URL : " 'face 'font-lock-type-face)
501 (vc-got-repository-url dir) "\n"))
502 (propertize "Branch : " 'face 'font-lock-type-face)
503 (vc-got--current-branch))))
505 (defun vc-got-dir-printer (info)
506 "Pretty-printer for the vc-dir-fileinfo structure INFO."
507 (let* ((isdir (vc-dir-fileinfo->directory info))
508 (state (if isdir "" (vc-dir-fileinfo->state info)))
509 (stage-state (vc-dir-fileinfo->extra info))
510 (filename (vc-dir-fileinfo->name info)))
513 (format "%c" (if (vc-dir-fileinfo->marked info) ?* ? ))
514 'face 'font-lock-type-face)
518 (format "staged:%-6s" stage-state)
520 'face (cond ((memq stage-state '(add edit)) 'font-lock-constant-face)
521 ((eq stage-state 'remove) 'font-lock-warning-face)
522 (t 'font-lock-variable-name-face)))
525 (format "%-14s" state)
526 'face (cond ((eq state 'up-to-date) 'font-lock-builtin-face)
527 ((memq state '(missing conflict)) 'font-lock-warning-face)
528 ((eq state 'edited) 'font-lock-constant-face)
529 (t 'font-lock-variable-name-face))
530 'mouse-face 'highlight)
533 (format "%s" filename)
535 (if isdir 'font-lock-comment-delimiter-face 'font-lock-function-name-face)
538 "Directory\nVC operations can be applied to it\nmouse-3: Pop-up menu"
539 "File\nmouse-3: Pop-up menu")
540 'mouse-face 'highlight
541 'keymap vc-dir-filename-mouse-map))))
543 (defun vc-got-working-revision (file)
544 "Return the id of the last commit that touched the FILE or \"0\" for a new (but added) file."
547 (when (vc-got--log file 1)
549 (goto-char (point-min))
550 (forward-word) ; skip "commit"
551 (forward-char) ; skip the space
552 (setq start (point)) ; store start of the SHA
553 (forward-word) ; goto SHA end
554 (buffer-substring start (point)))))
555 ;; special case: if this file is added but has no previous commits
556 ;; touching it, got log will fail (as expected), but we have to
558 (when (eq (vc-got-state file) 'added)
561 (defun vc-got-checkout-model (_files)
562 "Got uses an implicit checkout model for every file."
565 (defun vc-got-mode-line-string (file)
566 "Return the VC mode line string for FILE."
567 (vc-got-with-worktree file
568 (let ((def (vc-default-mode-line-string 'Got file)))
569 (concat (substring def 0 4) (vc-got--current-branch)))))
572 ;; state-changing functions
574 (defun vc-got-create-repo (_backend)
575 "Create an empty repository in the current directory."
576 (error "[vc-got] create-repo not implemented"))
578 (defun vc-got-register (files &optional _comment)
579 "Register FILES, passing `vc-register-switches' to the backend command."
582 (defalias 'vc-got-responsible-p #'vc-got-root)
584 (defun vc-got-unregister (file)
586 (vc-got--remove file t t))
588 (defun vc-got-checkin (files comment &optional _rev)
589 "Commit FILES with COMMENT as commit message."
591 (unless (zerop (vc-got--call "commit" "-m"
592 (log-edit-extract-headers nil comment)
595 (error "[vc-got] can't commit: %s" (buffer-string)))))
597 (defun vc-got-find-revision (file rev buffer)
598 "Fill BUFFER with the content of FILE in the given revision REV."
599 (when-let (obj-id (assoc file (vc-got--tree rev file) #'string=))
600 (with-current-buffer buffer
601 (vc-got-with-worktree file
602 (vc-got--cat rev obj-id)))))
604 (defun vc-got-checkout (_file &optional _rev)
605 "Checkout revision REV of FILE.
606 If REV is t, checkout from the head."
607 (error "[vc-got] checkout not implemented"))
609 (defun vc-got-revert (file &optional _content-done)
610 "Revert FILE back to working revision."
611 (vc-got--revert file))
613 (defun vc-got-merge-branch ()
614 "Prompt for a branch and integrate it into the current one."
615 ;; XXX: be smart and try to "got rebase" if "got integrate" fails?
616 (let* ((branches (cl-loop for (branch . commit) in (vc-got--list-branches)
618 (branch (completing-read "Merge from branch: " branches)))
620 (vc-got--integrate branch))))
622 (defun vc-got--push-pull (cmd op prompt)
623 "Execute CMD OP, or prompt the user if PROMPT is non-nil."
624 (let ((buffer (format "*vc-got : %s*" (expand-file-name default-directory))))
625 (when-let (cmd (if prompt
627 (read-shell-command (format "%s %s command: " cmd op)
628 (format "%s %s " cmd op))
631 (apply #'vc-do-async-command buffer default-directory cmd)
632 ;; this comes from vc-git.el. We're using git to push, so in
633 ;; part it makes sense, but we should revisit for full Got
635 (with-current-buffer buffer
636 (vc-compilation-mode 'git)
637 (let ((comp-cmd (mapconcat #'identity cmd " ")))
638 (setq-local compile-command comp-cmd
639 compilation-directory default-directory
640 compilation-arguments (list comp-cmd
642 (lambda (_ign) buffer)
644 (vc-set-async-update buffer))))
646 ;; TODO: this can be expanded. See whan omyksh does:
647 ;; function got-sync {
648 ;; local _remote _info _branch
650 ;; _info="$(got info)"
651 ;; _branch="$(echo "$_info" | awk '/branch reference:/ {l = split($NF, a, "/"); print a[l]}')"
652 ;; [ -z $_remote ] && _remote="origin"
653 ;; [ -z $_branch ] && _branch="main"
654 ;; got fetch "$_remote" && got update -b "$_remote/$_branch" && \
655 ;; got rebase $_branch
657 (defun vc-got-pull (prompt)
658 "Execute got pull, prompting the user for the full command if PROMPT is not nil."
659 (let ((default-directory (vc-got-root default-directory)))
660 (vc-got--push-pull vc-got-program "fetch" prompt)))
662 (defun vc-got-push (prompt)
663 "Run git push (not got!) in the repository dir.
664 If PROMPT is non-nil, prompt for the git command to run."
665 (let ((default-directory (vc-got--repo-root)))
666 (vc-got--push-pull "git" "push" prompt)))
671 (defun vc-got-print-log (files buffer &optional _shortlog start-revision limit)
672 "Insert the revision log for FILES into BUFFER.
673 LIMIT limits the number of commits, optionally starting at
675 (with-current-buffer buffer
676 ;; the *vc-diff* may be read only
677 (let ((inhibit-read-only t))
678 (cl-loop for file in files
679 do (vc-got--log (file-relative-name file) limit start-revision)))))
681 ;; XXX: this includes also the latest commit in REMOTE-LOCATION.
682 (defun vc-got-log-outgoing (buffer remote-location)
683 "Fill BUFFER with the diff between the local worktree branch and REMOTE-LOCATION."
684 (vc-setup-buffer buffer)
685 (let ((rl (if (or (not remote-location) (string-empty-p remote-location))
686 (concat "origin/" (vc-got--current-branch))
688 (inhibit-read-only t))
689 (with-current-buffer buffer
690 (vc-got--log nil nil nil rl))))
692 (defun vc-got-incoming (buffer remote-location)
693 "Fill BUFFER with the diff between the REMOTE-LOCATION and the local worktree branch."
694 (let ((rl (if (or (not remote-location) (string-empty-p remote-location))
695 (concat "origin/" (vc-got--current-branch))
697 (inhibit-read-only t))
698 (with-current-buffer buffer
699 (vc-got--log nil nil (vc-got--current-branch) rl))))
701 (defun vc-got-log-search (buffer pattern)
702 "Search commits for PATTERN and write the results found in BUFFER."
703 (with-current-buffer buffer
704 (let ((inhibit-read-only t))
705 (vc-got--log nil nil nil nil pattern))))
707 (define-derived-mode vc-got-log-view-mode log-view-mode "Got-Log-View"
708 "Got-specific log-view mode.
709 Heavily inspired by `vc-git-log-view-mode'."
712 log-view-file-re regexp-unmatchable
713 log-view-per-file-logs nil
714 log-view-message-re "^commit +\\([0-9a-z]+\\)"
716 log-view-font-lock-keywords
718 `((,log-view-message-re (1 'change-log-acknowledgment)))
721 '(("^from: \\([A-Za-z0-9_.+-]+@[A-Za-z0-9_.-]+\\)"
722 (1 'change-log-email))
724 ;; user: FirstName LastName <foo@bar>
725 ("^from: \\([^<(]+?\\)[ \t]*[(<]\\([A-Za-z0-9_.+-]+@[A-Za-z0-9_.-]+\\)[>)]"
727 (2 'change-log-email))
728 ("^date: \\(.+\\)" (1 'change-log-date))))))
731 ;; TODO: return 0 or 1
732 (defun vc-got-diff (files &optional rev1 rev2 buffer _async)
733 "Insert into BUFFER (or *vc-diff*) the diff for FILES from REV1 to REV2."
734 (let* ((buffer (get-buffer-create (or buffer "*vc-diff*")))
735 (inhibit-read-only t))
736 (with-current-buffer buffer
737 (vc-got-with-worktree (or (car files)
743 ;; TODO: if rev1 is nil, diff from the current version until
746 ;; TODO: if rev2 is nil as well, diff against an empty tree
747 ;; (i.e. get the patch from `got log -p rev1')
749 ;; TODO: it would be nice to optionally include FILES here,
750 ;; it would make the `=' key on the *Annotate* buffer do the
751 ;; right thing, but AFAICS got doesn't provide something
752 ;; like this. Probably only hacking something with ``log
753 ;; -p'' and filtering?
754 (vc-got--diff rev1 rev2))))))
756 (defun vc-got-revision-completion-table (_files)
757 "Return a completion table for existing revisions.
758 Ignores FILES because GoT doesn't have the concept of ``file
759 revisions''; instead, like with git, you have tags and branches."
760 (letrec ((table (lazy-completion-table
761 table (lambda () (vc-got--ref)))))
764 (defun vc-got-annotate-command (file buf &optional rev)
765 "Show annotated contents of FILE in buffer BUF. If given, use revision REV."
766 (let (process-file-side-effects)
767 (with-current-buffer buf
768 ;; FIXME: vc-ensure-vc-buffer won't recognise this buffer as managed
769 ;; by got unless vc-parent-buffer points to a buffer managed by got.
770 ;; investigate why this is needed.
771 (set (make-local-variable 'vc-parent-buffer) (find-file-noselect file))
772 (vc-got--call "blame"
773 (when rev (list "-c" rev))
777 (defconst vc-got--annotate-re
778 (concat "^[0-9]\\{1,\\}) " ; line number followed by )
779 "\\([a-z0-9]+\\) " ; SHA-1 of commit
780 "\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\) " ; year-mm-dd
781 "\\([^ ]\\)+ ") ; author
782 "Regexp to match annotation output lines.
784 Provides capture groups for:
787 3. author of commit")
789 (defconst vc-got--commit-re "^commit \\([a-z0-9]+\\)"
790 "Regexp to match commit lines.
792 Provides capture group for the commit revision id.")
794 (defun vc-got-annotate-time ()
795 "Return the time of the next line of annotation at or after point.
796 Value is returned as floating point fractional number of days."
799 (when (looking-at vc-got--annotate-re)
800 (let ((str (match-string-no-properties 2)))
801 (vc-annotate-convert-time
803 (string-to-number (substring str 8 10))
804 (string-to-number (substring str 5 7))
805 (string-to-number (substring str 0 4))))))))
807 (defun vc-got-annotate-extract-revision-at-line ()
808 "Return revision corresponding to the current line or nil."
811 (when (looking-at vc-got--annotate-re)
812 (match-string-no-properties 1))))
817 (defun vc-got--tag-callback (tag)
818 "`log-edit' callback for `vc-got-create-tag'.
819 Creates the TAG using the content of the current buffer."
821 (let ((msg (buffer-substring-no-properties (point-min)
824 (unless (zerop (vc-got--call "tag" "-m" msg "--" tag))
825 (error "[vc-got] can't create tag %s: %s" tag (buffer-string))))))
827 (defun vc-got-create-tag (_dir name branchp)
828 "Attach the tag NAME to the state of the worktree.
829 DIR is ignored (tags are global, not per-file). If BRANCHP is
830 true, NAME should create a new branch otherwise it will pop-up a
831 `log-edit' buffer to provide the tag message."
832 ;; TODO: vc reccomends to ensure that all the file are in a clean
833 ;; state, but is it useful?
835 (vc-got--branch name)
836 (let ((buf (get-buffer-create "*vc-got tag*")))
837 (with-current-buffer buf
839 (switch-to-buffer buf)
843 (vc-got--tag-callback name)
844 (kill-buffer buf))))))))
846 (defun vc-got-retrieve-tag (dir name _update)
847 "Switch to the tag NAME for files at or below DIR."
848 (let ((default-directory dir))
849 (vc-got--update name dir)))
854 (defun vc-got-find-ignore-file (file)
855 "Return the gitignore file that controls FILE."
856 (expand-file-name ".gitignore"
859 (defun vc-got-previous-revision (file rev)
860 "Return the revision number that precedes REV for FILE, or nil if no such revision exists."
862 (vc-got--log file 2 rev nil nil t)
863 (goto-char (point-min))
864 (keep-lines "^commit")
865 (when (looking-at vc-got--commit-re)
866 (match-string-no-properties 1))))
868 (defun vc-got-next-revision (file rev)
869 "Return the revision number that follows REV for FILE, or nil if no such revision exists."
871 (vc-got--log file nil nil rev)
872 (keep-lines "^commit" (point-min) (point-max))
873 (goto-char (point-max))
874 (forward-line -1) ; return from empty line to last actual commit
875 (unless (= (point) (point-min))
877 (when (looking-at vc-got--commit-re)
878 (match-string-no-properties 1)))))
880 (defun vc-got-delete-file (file)
881 "Delete FILE locally and mark it deleted in work tree."
882 (vc-got--remove file t))
884 (defun vc-got-find-file-hook ()
885 "Activate `smerge-mode' if there is a conflict."
886 ;; just like vc-git-find-file-hook
887 (when (and buffer-file-name
888 (eq (vc-state buffer-file-name 'Got) 'conflict)
890 (goto-char (point-min))
891 (re-search-forward "^<<<<<<< " nil 'noerror)))
892 (smerge-start-session)
893 (vc-message-unresolved-conflicts buffer-file-name)))
895 (defun vc-got-conflicted-files (dir)
896 "Return the list of files with conflicts in directory DIR."
897 (let* ((root (vc-got-root dir))
898 (default-directory root)
899 (process-file-side-effects))
900 (cl-loop with conflicts = nil
901 for (file status _) in (vc-got--status "C" ".")
902 do (when (and (eq status 'conflict)
903 (file-in-directory-p file dir))
904 (push file conflicts))
905 finally return conflicts)))
907 (defun vc-got-repository-url (_file &optional remote-name)
908 "Return URL for REMOTE-NAME, or for \"origin\" if nil."
909 (let* ((default-directory (vc-got--repo-root))
910 (remote-name (or remote-name "origin"))
911 (heading (concat "[remote \"" remote-name "\"]"))
912 (conf (cond ((file-exists-p ".git/config") ".git/config")
913 ((file-exists-p ".git") nil)
914 ((file-exists-p "config") "config")))
918 (insert-file-contents conf)
919 (goto-char (point-min))
920 (when (search-forward heading nil t)
922 (while (and (not found)
923 (looking-at ".*=") ; too broad?
924 (not (= (point) (point-max))))
925 (when (looking-at ".*url = \\(.*\\)")
926 (setq found (match-string-no-properties 1)))
932 (defun vc-got-fix-dir-move-to-goal-column (fn)
933 "Move the cursor on the file column.
934 Adviced around `vc-dir-move-to-goal-column' (FN) because it hardcodes column 25."
935 (if (not (vc-find-root default-directory ".got"))
940 (advice-add 'vc-dir-move-to-goal-column :around #'vc-got-fix-dir-move-to-goal-column)
943 ;;; vc-got.el ends here