changelog shortlog graph tags branches changeset files revisions annotate raw help

Mercurial > core / lisp/lib/vc/hg.lisp

changeset 280: d398c7d4433d
parent: 1169b432cf8e
child: 1c6e8353a855
author: Richard Westhaver <ellis@rwest.io>
date: Tue, 16 Apr 2024 15:31:40 -0400
permissions: -rw-r--r--
description: cleanup
1 ;;; Commentary:
2 
3 ;; Mercurial is our primary VCS - but we blur the lines by mirroring
4 ;; our code to Git. In a few years mirroring will probably be
5 ;; unnecessary but it's a really useful hack FTTB.
6 
7 ;; Our forge is based on an instance of Heptapod https://heptapod.net/
8 ;; which is a GitLab fork. Most of the public repos are Mercurial, but
9 ;; there are a few 'reverse-mirrors' which I maintain exclusively as
10 ;; Git repos. Same goes for any fork I maintain - for example, Lust is
11 ;; my fork of rustlang/rust and is just a Git repository.
12 
13 ;; HACK 2023-09-15: hgcmd interface, parsers, metadata object protocols
14 
15 ;; https://wiki.mercurial-scm.org/Design
16 
17 ;; https://wiki.mercurial-scm.org/CommandServer
18 
19 ;; the cmdserver is the obvious solution for Skel to interact with
20 ;; Mercurial so we'll be leaning into it right away without bothering
21 ;; with the standard CLI. I'm unfamiliar with how this is done with
22 ;; Git, or if it's done at all. In Mercurial's case it seems they
23 ;; built it out of licensing issues and to avoid Python cold-start
24 ;; penalty which aren't issues for me ATM anyway. Git is written in C
25 ;; so doesn't suffer a cold-start hit, but it would be nice to
26 ;; interact with repos via a similar lightweight, local, wire
27 ;; protocol.
28 
29 ;; BTW It was hard to find the command to start the command server -
30 ;; it's 'hg serve'. Here's the base shell command invoked by chg:
31 
32 ;; hg serve --no-profile --cmdserver chgunix --address @INITSOCKNAME --daemon-postexec chdir:/ @DIR
33 
34 ;;; Code:
35 (in-package :vc)
36 
37 (defvar *default-hg-client-buffer-size* 4096)
38 (defvar *hg-program* (or (find-exe "rhg") (find-exe "hg")))
39 
40 (defun run-hg-command (cmd &rest args)
41  (sb-ext:run-program *hg-program* (push cmd args) :output :stream))
42 
43 (defun hg-url-p (url)
44  "Return nil if URL does not look like a URL to a hg valid remote."
45  (let ((url-str (if (typep url 'pathname)
46  (namestring url)
47  url)))
48  (scan '(:alternation
49  (:regex "\\.hg$")
50  (:regex "^hg://")
51  (:regex "^https://hg\\.")
52  (:regex "^hg@"))
53  url-str)))
54 
55 ;; (describe (make-instance 'hg-repo))
56 ;; https://repo.mercurial-scm.org/hg/file/tip/mercurial/interfaces/repository.py
57 (defclass hg-repo (vc-repo)
58  ((dirstate) ;; working-directory
59  (bookmarks)
60  (requires)))
61 
62 (defmethod vc-run ((self hg-repo) (cmd string) &rest args)
63  (with-slots (path) self
64  (with-current-directory (path)
65  (with-open-stream (s (sb-ext:process-output (apply #'run-hg-command cmd args)))
66  (with-output-to-string (str)
67  (loop for l = (read-line s nil nil)
68  while l
69  do (write-line l)))))))
70 
71 (defmethod vc-init ((self (eql :hg)))
72  (make-instance 'hg-repo))
73 
74 (defmethod vc-init ((self hg-repo))
75  (with-slots (path) self
76  ;; could throw error here but w/e
77  (sb-ext:process-exit-code (run-hg-command "init" path))))
78 
79 (defmethod vc-clone ((self hg-repo) remote &key &allow-other-keys)
80  (with-slots (path) self
81  (sb-ext:process-exit-code (run-hg-command "clone" remote path))))
82 
83 (defmethod vc-pull ((self hg-repo) remote &key &allow-other-keys)
84  (with-slots (path) self
85  (with-current-directory (path)
86  (sb-ext:process-exit-code (run-hg-command "pull" remote)))))
87 
88 (defmethod vc-push ((self hg-repo) remote &key &allow-other-keys)
89  (with-slots (path) self
90  (with-current-directory (path)
91  (sb-ext:process-exit-code (run-hg-command "push" remote)))))
92 
93 (defmethod vc-commit ((self hg-repo) msg &key &allow-other-keys)
94  (with-slots (path) self
95  (with-current-directory (path)
96  (sb-ext:process-exit-code (run-hg-command "commit" "-m" msg)))))
97 
98 (defmethod vc-add ((self hg-repo) &rest files)
99  (with-slots (path) self
100  (with-current-directory (path)
101  (sb-ext:process-exit-code (apply #'run-hg-command "add" files)))))
102 
103 (defmethod vc-remove ((self hg-repo) &rest files)
104  (with-slots (path) self
105  (with-current-directory (path)
106  (sb-ext:process-exit-code (apply #'run-hg-command "remove" files)))))
107 
108 (defmethod vc-addremove ((self hg-repo) &rest files)
109  (with-slots (path) self
110  (with-current-directory (path)
111  (sb-ext:process-exit-code (apply #'run-hg-command "addremove" files)))))
112 
113 (defmethod vc-status ((self hg-repo) &key &allow-other-keys) (vc-run self "status"))
114 
115 (defmethod vc-branch ((self hg-repo) &key cmd branch &allow-other-keys) (vc-run self "branch" cmd branch))
116 
117 (defmethod vc-diff ((a hg-repo) (b hg-repo) &key &allow-other-keys)
118  (vc-run a "diff" (vc-repo-head a) (vc-repo-head b)))
119 
120 (defmethod vc-id ((self hg-repo))
121  (with-slots (path) self
122  (with-current-directory (path)
123  (with-open-stream (s (sb-ext:process-output (run-hg-command "id")))
124  (with-output-to-string (str)
125  (loop for c = (read-char s nil nil)
126  while c
127  do (write-char c str))
128  str)))))
129 
130 ;;; Client
131 ;; ref: https://wiki.mercurial-scm.org/CommandServer
132 (declaim (inline %make-hg-client))
133 (defstruct (hg-client (:constructor %make-hg-client))
134  "hg-client structures contain the client connection state
135  machinery and a handle to the unix socket running Mercurial command
136  server."
137  (pid 0 :type fixnum :read-only t)
138  (pgid 0 :type fixnum)
139  (cwd (sb-posix:getcwd) :type string)
140  (buffer (make-array *default-hg-client-buffer-size* :element-type 'unsigned-byte :adjustable nil))
141  (socket nil :type (or local-socket null))
142  (caps 0 :type fixnum))
143 
144 (defun make-hg-client (&optional bufsize)
145  (%make-hg-client
146  :buffer (make-array (or bufsize *default-hg-client-buffer-size*)
147  :element-type 'unsigned-byte
148  :adjustable nil)))
149 
150 ;;;; Client Protocol
151 ;; all communication with the mercurial cmdserver is done over a
152 ;; socket. byte order is big-endian.
153 
154 ;; data from server is channel-based - (channel length pair sent
155 ;; before data) - 5 byte header total
156 
157 ;; on init, the server will send hello message on channel #\o. the
158 ;; message is a signel chunk consisting of a #\Newline-separated list
159 ;; of lines of the form:
160 #|
161 <field name>: <field data>
162 |#
163 
164 ;; fields include: capabilities, encoding, pid
165 
166 #|
167 o
168 1234
169 <data: 1234 bytes>
170 |#
171 
172 (defmethod vc-init ((self hg-client))
173  "Initialize the hg commandserver client. This method initializes the
174 appropriate process IDs and a socket for communicating with the
175 commandserver."
176  (with-slots (pid pgid socket caps) self
177  (format nil "pid: ~A, pgid: ~A, socket: ~A, caps: ~A" pid pgid socket caps)))
178 
179 ;; TODO 2023-12-29:
180 (defmethod vc-run ((self hg-client) cmd &rest args)
181  (declare (ignorable args)))
182 
183 ;;; Low-level
184 (defstruct hg-nodeid id)
185 
186 (defstruct hg-revlog)
187 
188 (defstruct hg-manifest)
189 
190 (defstruct hg-changeset id)
191 
192 ;;;; Dirstate
193 
194 ;; see also: https://wiki.mercurial-scm.org/DirstateFormatImprovementsPlan
195 
196 #|
197 .hg/dirstate:
198 <p1 binhash><p2 binhash>
199 <list of dirstate entries>
200 |#
201 
202 #| entry
203 8bit: status
204 32bit: mode
205 32bit: size
206 32bit: mtime
207 32bit: length
208 variable length entry (length given by the previous length field) with:
209 "<filename>" followed if it's a copy by: "\0<source if copy>"
210 |#
211 
212 (defstruct dirstate-entry status mode size mtime length filename)
213 
214 ;; (defmethod read-dirstate-file ((self hg-repo)))
215 
216 (defstruct dirstate
217  (entries (make-array 0 :element-type 'dirstate-entry :fill-pointer 0 :adjustable t) :type (vector dirstate-entry)))