Blob


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/
7 ;; Keywords: vc
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 ;; Backend implementation status
27 ;;
28 ;; Function marked with `*' are required, those with `-' are optional.
29 ;;
30 ;; FUNCTION NAME STATUS
31 ;;
32 ;; BACKEND PROPERTIES:
33 ;; * revision-granularity DONE
34 ;; - update-on-retrieve-tag XXX: what should this do?
35 ;;
36 ;; STATE-QUERYING FUNCTIONS:
37 ;; * registered DONE
38 ;; * state DONE
39 ;; - dir-status-files DONE
40 ;; - dir-extra-headers DONE
41 ;; - dir-printer DONE
42 ;; - status-fileinfo-extra NOT IMPLEMENTED
43 ;; * working-revision DONE
44 ;; * checkout-model DONE
45 ;; - mode-line-string DONE
46 ;;
47 ;; STATE-CHANGING FUNCTIONS:
48 ;; * create-repo NOT IMPLEMENTED
49 ;; I don't think got init does what this function is supposed to
50 ;; do.
51 ;; * register DONE
52 ;; - responsible-p DONE
53 ;; - receive-file NOT NEEDED, default `register' works fine
54 ;; - unregister DONE
55 ;; * checkin DONE
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?
60 ;; * revert DONE
61 ;; - merge-file NOT IMPLEMENTED
62 ;; - merge-branch DONE
63 ;; - merge-news NOT IMPLEMENTED
64 ;; - pull DONE
65 ;; - push DONE
66 ;; uses git
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
73 ;;
74 ;; HISTORY FUNCTIONS
75 ;; * print-log DONE
76 ;; * log-outgoing DONE
77 ;; * log-incoming DONE
78 ;; - log-search DONE
79 ;; - log-view-mode DONE
80 ;; - show-log-entry NOT IMPLEMENTED
81 ;; - comment-history NOT IMPLEMENTED
82 ;; - update-changelog NOT IMPLEMENTED
83 ;; * diff DONE
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
93 ;;
94 ;; TAG SYSTEM
95 ;; - create-tag DONE
96 ;; - retrieve-tag DONE
97 ;;
98 ;; MISCELLANEOUS NOT IMPLEMENTED
99 ;; - make-version-backups-p NOT NEEDED, `got' works fine locally
100 ;; - root DONE
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:
117 ;;
118 ;; (let* ((root (vc-git-root default-directory))
119 ;; (buffer (format "*vc-git : %s*" (expand-file-name root)))
120 ;; ...)
121 ;; ...)
122 ;;
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.
126 ;;; Code:
128 (eval-when-compile
129 (require 'subr-x))
131 (require 'cl-lib)
132 (require 'seq)
133 (require 'vc)
134 (require 'vc-annotate)
136 ;; FIXME: avoid loading this? We only need it for
137 ;; vc-dir-filename-mouse-map in our custom printer.
138 (require 'vc-dir)
140 (defgroup vc-got nil
141 "VC GoT backend."
142 :group 'vc)
144 (defcustom vc-got-program "got"
145 "Name of the Got executable (excluding any arguments)."
146 :type 'string)
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)))
156 ;; helpers
157 (defun vc-got--program-version ()
158 "Return string representing the got version."
159 (let (process-file-side-effects)
160 (with-temp-buffer
161 (vc-got--call "-V")
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))
172 ,@body))
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
178 (with-temp-buffer
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."
190 (with-temp-buffer
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
204 worktree."
205 (let (process-file-side-effects)
206 (vc-got-with-worktree (or path default-directory)
207 (when (zerop
208 (save-excursion
209 (vc-got--call "log"
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"))
215 "--"
216 path)))
217 (save-excursion
218 (delete-matching-lines "^-----------------------------------------------$")
219 t)))))
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
228 files)."
229 (with-temp-buffer
230 (let* ((default-directory (expand-file-name
231 (if (file-directory-p dir-or-file)
232 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))
238 "--"
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
245 (char-after))
246 (forward-char)))
247 (stage-status (prog1 (vc-got--parse-stage-char
248 (char-after))
249 (forward-char)))
250 (filename (progn
251 (forward-char)
252 (buffer-substring (point)
253 (line-end-position)))))
254 (list (file-relative-name (expand-file-name filename root)
255 default-directory)
256 (or file-status (and stage-status 'staged))
257 stage-status))
258 do (forward-line))))))
260 (defun vc-got--parse-status-char (c)
261 "Parse status char C into a symbol accepted by `vc-state'."
262 (cl-case c
263 (?M 'edited)
264 (?A 'added)
265 (?D 'removed)
266 (?C 'conflict)
267 (?! 'missing)
268 (?~ 'edited) ; XXX: what does it means for a file to be ``obstructed''?
269 (?? 'unregistered)
270 (?m 'edited) ; modified file modes
271 (?N nil)))
273 (defun vc-got--parse-stage-char (c)
274 "Parse the stage status char C into a symbol."
275 (cl-case c
276 (?M 'edit)
277 (?A 'add)
278 (?D 'remove)))
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))
283 (cl-loop
284 until (= (point) (point-max))
285 collect (let* ((obj-start (point))
286 (_ (forward-word))
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
293 (forward-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
300 given COMMIT."
301 (vc-got-with-worktree path
302 (let (process-file-side-effects)
303 (with-temp-buffer
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)
315 (with-temp-buffer
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)
321 (with-temp-buffer
322 (when (zerop (vc-got--call "branch" "-l"))
323 (goto-char (point-min))
324 (cl-loop
325 until (= (point) (point-max))
326 ;; parse the `* $branchname: $commit', from the end
327 ;; XXX: use a regex?
328 collect (let* ((_ (move-end-of-line nil))
329 (end-commit (point))
330 (_ (backward-word))
331 (start-commit (point))
332 (_ (backward-char 2))
333 (end-branchname (point))
334 (_ (move-beginning-of-line nil))
335 (_ (forward-char 2))
336 (start-branchname (point))
337 (branchname (buffer-substring start-branchname end-branchname))
338 (commit (buffer-substring start-commit end-commit)))
339 (forward-line)
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)
346 (with-temp-buffer
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."
352 (with-temp-buffer
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."
359 (with-temp-buffer
360 (unless (zerop (vc-got--call "update" "-b" branch "--" paths))
361 (error "[vc-got] can't update to branch %s: %s"
362 branch
363 (buffer-string)))))
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)
370 "--"
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
376 tree."
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
383 files on disk."
384 (vc-got-with-worktree (or file default-directory)
385 (with-temp-buffer
386 (zerop (vc-got--call "remove"
387 (when force "-f")
388 (when keep-local "-k")
389 "--"
390 file)))))
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
397 ;; won't match it.
398 (table (list "HEAD")))
399 (vc-got-with-worktree default-directory
400 (with-temp-buffer
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))
405 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
411 (with-temp-buffer
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."
422 'repository)
424 ;; XXX: what this should do? The description is not entirely clear
425 (defun vc-got-update-on-retrieve-tag ()
426 nil)
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)
444 (null s)))))))
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.
452 (with-temp-buffer
453 (when (zerop (vc-got--call "status" "--" file))
454 (goto-char (point-min))
455 (if (eobp)
456 'up-to-date
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 "..")
463 (string= file ".")
464 (string= file ".got"))
465 collect file))
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))
476 double-check)
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
485 ;; double check
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"
498 (when remote
499 (concat
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)))
511 (insert
512 (propertize
513 (format "%c" (if (vc-dir-fileinfo->marked info) ?* ? ))
514 'face 'font-lock-type-face)
515 " "
516 (propertize
517 (if stage-state
518 (format "staged:%-6s" stage-state)
519 (format "%-13s" ""))
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)))
523 " "
524 (propertize
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)
531 " "
532 (propertize
533 (format "%s" filename)
534 'face
535 (if isdir 'font-lock-comment-delimiter-face 'font-lock-function-name-face)
536 'help-echo
537 (if isdir
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."
545 (or
546 (with-temp-buffer
547 (when (vc-got--log file 1)
548 (let (start)
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
557 ;; return "0".
558 (when (eq (vc-got-state file) 'added)
559 "0")))
561 (defun vc-got-checkout-model (_files)
562 "Got uses an implicit checkout model for every file."
563 'implicit)
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."
580 (vc-got--add files))
582 (defalias 'vc-got-responsible-p #'vc-got-root)
584 (defun vc-got-unregister (file)
585 "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."
590 (with-temp-buffer
591 (unless (zerop (vc-got--call "commit" "-m"
592 (log-edit-extract-headers nil comment)
593 "--"
594 files))
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)
617 collect branch))
618 (branch (completing-read "Merge from branch: " branches)))
619 (when branch
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
626 (split-string
627 (read-shell-command (format "%s %s command: " cmd op)
628 (format "%s %s " cmd op))
629 " " t)
630 (list 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
634 ;; support.
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
641 nil
642 (lambda (_ign) buffer)
643 nil))))
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
649 ;; _remote=$1
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
656 ;; }
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)))
669 ;; History functions
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
674 START-REVISION."
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))
687 remote-location))
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))
696 remote-location))
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'."
710 (require 'add-log)
711 (setq-local
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
717 (append
718 `((,log-view-message-re (1 'change-log-acknowledgment)))
719 ;; Handle the case:
720 ;; user: foo@bar
721 '(("^from: \\([A-Za-z0-9_.+-]+@[A-Za-z0-9_.-]+\\)"
722 (1 'change-log-email))
723 ;; Handle the case:
724 ;; user: FirstName LastName <foo@bar>
725 ("^from: \\([^<(]+?\\)[ \t]*[(<]\\([A-Za-z0-9_.+-]+@[A-Za-z0-9_.-]+\\)[>)]"
726 (1 'change-log-name)
727 (2 'change-log-email))
728 ("^date: \\(.+\\)" (1 'change-log-date))))))
730 ;; TODO: async
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)
738 default-directory)
739 (if (and (null rev1)
740 (null rev2))
741 (dolist (file files)
742 (vc-got--diff file))
743 ;; TODO: if rev1 is nil, diff from the current version until
744 ;; rev2.
745 ;;
746 ;; TODO: if rev2 is nil as well, diff against an empty tree
747 ;; (i.e. get the patch from `got log -p rev1')
748 ;;
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)))))
762 table))
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))
774 "--"
775 file))))
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:
785 1. revision id
786 2. date of commit
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."
797 (save-excursion
798 (beginning-of-line)
799 (when (looking-at vc-got--annotate-re)
800 (let ((str (match-string-no-properties 2)))
801 (vc-annotate-convert-time
802 (encode-time 0 0 0
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."
809 (save-excursion
810 (beginning-of-line)
811 (when (looking-at vc-got--annotate-re)
812 (match-string-no-properties 1))))
815 ;; Tag system
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."
820 (interactive)
821 (let ((msg (buffer-substring-no-properties (point-min)
822 (point-max))))
823 (with-temp-buffer
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?
834 (if branchp
835 (vc-got--branch name)
836 (let ((buf (get-buffer-create "*vc-got tag*")))
837 (with-current-buffer buf
838 (erase-buffer)
839 (switch-to-buffer buf)
840 (log-edit (lambda ()
841 (interactive)
842 (unwind-protect
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)))
852 ;; Miscellaneous
854 (defun vc-got-find-ignore-file (file)
855 "Return the gitignore file that controls FILE."
856 (expand-file-name ".gitignore"
857 (vc-got-root file)))
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."
861 (with-temp-buffer
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."
870 (with-temp-buffer
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))
876 (forward-line -1)
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)
889 (save-excursion
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")))
915 found)
916 (when conf
917 (with-temp-buffer
918 (insert-file-contents conf)
919 (goto-char (point-min))
920 (when (search-forward heading nil t)
921 (forward-line)
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)))
927 (forward-line))
928 found)))))
931 ;; hacks
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"))
936 (funcall fn)
937 (beginning-of-line)
938 (unless (eolp)
939 (forward-char 31))))
940 (advice-add 'vc-dir-move-to-goal-column :around #'vc-got-fix-dir-move-to-goal-column)
942 (provide 'vc-got)
943 ;;; vc-got.el ends here