Mercurial > hg > sonic-visualiser
comparison repoint.sml @ 1808:adc8a48f4e4c repoint
Vext -> Repoint. Let's see how this goes.
author | Chris Cannam |
---|---|
date | Wed, 09 May 2018 13:28:55 +0100 |
parents | |
children | 1b6ffed298a2 |
comparison
equal
deleted
inserted
replaced
1807:dc4e6eb475ed | 1808:adc8a48f4e4c |
---|---|
1 (* | |
2 DO NOT EDIT THIS FILE. | |
3 This file is automatically generated from the individual | |
4 source files in the Repoint repository. | |
5 *) | |
6 | |
7 (* | |
8 Repoint | |
9 | |
10 A simple manager for third-party source code dependencies | |
11 | |
12 Copyright 2018 Chris Cannam, Particular Programs Ltd, | |
13 and Queen Mary, University of London | |
14 | |
15 Permission is hereby granted, free of charge, to any person | |
16 obtaining a copy of this software and associated documentation | |
17 files (the "Software"), to deal in the Software without | |
18 restriction, including without limitation the rights to use, copy, | |
19 modify, merge, publish, distribute, sublicense, and/or sell copies | |
20 of the Software, and to permit persons to whom the Software is | |
21 furnished to do so, subject to the following conditions: | |
22 | |
23 The above copyright notice and this permission notice shall be | |
24 included in all copies or substantial portions of the Software. | |
25 | |
26 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
27 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
28 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
29 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR | |
30 ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF | |
31 CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | |
32 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
33 | |
34 Except as contained in this notice, the names of Chris Cannam, | |
35 Particular Programs Ltd, and Queen Mary, University of London | |
36 shall not be used in advertising or otherwise to promote the sale, | |
37 use or other dealings in this Software without prior written | |
38 authorization. | |
39 *) | |
40 | |
41 val repoint_version = "0.9.98" | |
42 | |
43 | |
44 datatype vcs = | |
45 HG | | |
46 GIT | | |
47 SVN | |
48 | |
49 datatype source = | |
50 URL_SOURCE of string | | |
51 SERVICE_SOURCE of { | |
52 service : string, | |
53 owner : string option, | |
54 repo : string option | |
55 } | |
56 | |
57 type id_or_tag = string | |
58 | |
59 datatype pin = | |
60 UNPINNED | | |
61 PINNED of id_or_tag | |
62 | |
63 datatype libstate = | |
64 ABSENT | | |
65 CORRECT | | |
66 SUPERSEDED | | |
67 WRONG | |
68 | |
69 datatype localstate = | |
70 MODIFIED | | |
71 LOCK_MISMATCHED | | |
72 CLEAN | |
73 | |
74 datatype branch = | |
75 BRANCH of string | | |
76 DEFAULT_BRANCH | |
77 | |
78 (* If we can recover from an error, for example by reporting failure | |
79 for this one thing and going on to the next thing, then the error | |
80 should usually be returned through a result type rather than an | |
81 exception. *) | |
82 | |
83 datatype 'a result = | |
84 OK of 'a | | |
85 ERROR of string | |
86 | |
87 type libname = string | |
88 | |
89 type libspec = { | |
90 libname : libname, | |
91 vcs : vcs, | |
92 source : source, | |
93 branch : branch, | |
94 project_pin : pin, | |
95 lock_pin : pin | |
96 } | |
97 | |
98 type lock = { | |
99 libname : libname, | |
100 id_or_tag : id_or_tag | |
101 } | |
102 | |
103 type remote_spec = { | |
104 anon : string option, | |
105 auth : string option | |
106 } | |
107 | |
108 type provider = { | |
109 service : string, | |
110 supports : vcs list, | |
111 remote_spec : remote_spec | |
112 } | |
113 | |
114 type account = { | |
115 service : string, | |
116 login : string | |
117 } | |
118 | |
119 type context = { | |
120 rootpath : string, | |
121 extdir : string, | |
122 providers : provider list, | |
123 accounts : account list | |
124 } | |
125 | |
126 type userconfig = { | |
127 providers : provider list, | |
128 accounts : account list | |
129 } | |
130 | |
131 type project = { | |
132 context : context, | |
133 libs : libspec list | |
134 } | |
135 | |
136 structure RepointFilenames = struct | |
137 val project_file = "repoint-project.json" | |
138 val project_lock_file = "repoint-lock.json" | |
139 val user_config_file = ".repoint.json" | |
140 val archive_dir = ".repoint-archive" | |
141 end | |
142 | |
143 signature VCS_CONTROL = sig | |
144 | |
145 (** Check whether the given VCS is installed and working *) | |
146 val is_working : context -> bool result | |
147 | |
148 (** Test whether the library is present locally at all *) | |
149 val exists : context -> libname -> bool result | |
150 | |
151 (** Return the id (hash) of the current revision for the library *) | |
152 val id_of : context -> libname -> id_or_tag result | |
153 | |
154 (** Test whether the library is at the given id *) | |
155 val is_at : context -> libname * id_or_tag -> bool result | |
156 | |
157 (** Test whether the library is on the given branch, i.e. is at | |
158 the branch tip or an ancestor of it *) | |
159 val is_on_branch : context -> libname * branch -> bool result | |
160 | |
161 (** Test whether the library is at the newest revision for the | |
162 given branch. False may indicate that the branch has advanced | |
163 or that the library is not on the branch at all. This function | |
164 may use the network to check for new revisions *) | |
165 val is_newest : context -> libname * source * branch -> bool result | |
166 | |
167 (** Test whether the library is at the newest revision available | |
168 locally for the given branch. False may indicate that the | |
169 branch has advanced or that the library is not on the branch | |
170 at all. This function must not use the network *) | |
171 val is_newest_locally : context -> libname * branch -> bool result | |
172 | |
173 (** Test whether the library has been modified in the local | |
174 working copy *) | |
175 val is_modified_locally : context -> libname -> bool result | |
176 | |
177 (** Check out, i.e. clone a fresh copy of, the repo for the given | |
178 library on the given branch *) | |
179 val checkout : context -> libname * source * branch -> unit result | |
180 | |
181 (** Update the library to the given branch tip. Assumes that a | |
182 local copy of the library already exists *) | |
183 val update : context -> libname * source * branch -> unit result | |
184 | |
185 (** Update the library to the given specific id or tag *) | |
186 val update_to : context -> libname * source * id_or_tag -> unit result | |
187 | |
188 (** Return a URL from which the library can be cloned, given that | |
189 the local copy already exists. For a DVCS this can be the | |
190 local copy, but for a centralised VCS it will have to be the | |
191 remote repository URL. Used for archiving *) | |
192 val copy_url_for : context -> libname -> string result | |
193 end | |
194 | |
195 signature LIB_CONTROL = sig | |
196 val review : context -> libspec -> (libstate * localstate) result | |
197 val status : context -> libspec -> (libstate * localstate) result | |
198 val update : context -> libspec -> unit result | |
199 val id_of : context -> libspec -> id_or_tag result | |
200 val is_working : context -> vcs -> bool result | |
201 end | |
202 | |
203 structure FileBits :> sig | |
204 val extpath : context -> string | |
205 val libpath : context -> libname -> string | |
206 val subpath : context -> libname -> string -> string | |
207 val command_output : context -> libname -> string list -> string result | |
208 val command : context -> libname -> string list -> unit result | |
209 val file_url : string -> string | |
210 val file_contents : string -> string | |
211 val mydir : unit -> string | |
212 val homedir : unit -> string | |
213 val mkpath : string -> unit result | |
214 val rmpath : string -> unit result | |
215 val nonempty_dir_exists : string -> bool | |
216 val project_spec_path : string -> string | |
217 val project_lock_path : string -> string | |
218 val verbose : unit -> bool | |
219 end = struct | |
220 | |
221 fun verbose () = | |
222 case OS.Process.getEnv "REPOINT_VERBOSE" of | |
223 SOME "0" => false | |
224 | SOME _ => true | |
225 | NONE => false | |
226 | |
227 fun split_relative path desc = | |
228 case OS.Path.fromString path of | |
229 { isAbs = true, ... } => raise Fail (desc ^ " may not be absolute") | |
230 | { arcs, ... } => arcs | |
231 | |
232 fun extpath ({ rootpath, extdir, ... } : context) = | |
233 let val { isAbs, vol, arcs } = OS.Path.fromString rootpath | |
234 in OS.Path.toString { | |
235 isAbs = isAbs, | |
236 vol = vol, | |
237 arcs = arcs @ | |
238 split_relative extdir "extdir" | |
239 } | |
240 end | |
241 | |
242 fun subpath ({ rootpath, extdir, ... } : context) libname remainder = | |
243 (* NB libname is allowed to be a path fragment, e.g. foo/bar *) | |
244 let val { isAbs, vol, arcs } = OS.Path.fromString rootpath | |
245 in OS.Path.toString { | |
246 isAbs = isAbs, | |
247 vol = vol, | |
248 arcs = arcs @ | |
249 split_relative extdir "extdir" @ | |
250 split_relative libname "library path" @ | |
251 split_relative remainder "subpath" | |
252 } | |
253 end | |
254 | |
255 fun libpath context "" = | |
256 extpath context | |
257 | libpath context libname = | |
258 subpath context libname "" | |
259 | |
260 fun project_file_path rootpath filename = | |
261 let val { isAbs, vol, arcs } = OS.Path.fromString rootpath | |
262 in OS.Path.toString { | |
263 isAbs = isAbs, | |
264 vol = vol, | |
265 arcs = arcs @ [ filename ] | |
266 } | |
267 end | |
268 | |
269 fun project_spec_path rootpath = | |
270 project_file_path rootpath (RepointFilenames.project_file) | |
271 | |
272 fun project_lock_path rootpath = | |
273 project_file_path rootpath (RepointFilenames.project_lock_file) | |
274 | |
275 fun trim str = | |
276 hd (String.fields (fn x => x = #"\n" orelse x = #"\r") str) | |
277 | |
278 fun file_url path = | |
279 let val forward_path = | |
280 String.translate (fn #"\\" => "/" | | |
281 c => Char.toString c) | |
282 (OS.Path.mkCanonical path) | |
283 in | |
284 (* Path is expected to be absolute already, but if it | |
285 starts with a drive letter, we'll need an extra slash *) | |
286 case explode forward_path of | |
287 #"/"::rest => "file:///" ^ implode rest | |
288 | _ => "file:///" ^ forward_path | |
289 end | |
290 | |
291 fun file_contents filename = | |
292 let val stream = TextIO.openIn filename | |
293 fun read_all str acc = | |
294 case TextIO.inputLine str of | |
295 SOME line => read_all str (trim line :: acc) | |
296 | NONE => rev acc | |
297 val contents = read_all stream [] | |
298 val _ = TextIO.closeIn stream | |
299 in | |
300 String.concatWith "\n" contents | |
301 end | |
302 | |
303 fun expand_commandline cmdlist = | |
304 (* We are quite strict about what we accept here, except | |
305 for the first element in cmdlist which is assumed to be a | |
306 known command location rather than arbitrary user input. *) | |
307 let open Char | |
308 fun quote arg = | |
309 if List.all | |
310 (fn c => isAlphaNum c orelse c = #"-" orelse c = #"_") | |
311 (explode arg) | |
312 then arg | |
313 else "\"" ^ arg ^ "\"" | |
314 fun check arg = | |
315 let val valid = explode " /#:;?,._-{}@=+" | |
316 in | |
317 app (fn c => | |
318 if isAlphaNum c orelse | |
319 List.exists (fn v => v = c) valid orelse | |
320 c > chr 127 | |
321 then () | |
322 else raise Fail ("Invalid character '" ^ | |
323 (Char.toString c) ^ | |
324 "' in command list")) | |
325 (explode arg); | |
326 arg | |
327 end | |
328 in | |
329 String.concatWith " " | |
330 (map quote | |
331 (hd cmdlist :: map check (tl cmdlist))) | |
332 end | |
333 | |
334 val tick_cycle = ref 0 | |
335 val tick_chars = Vector.fromList (map String.str (explode "|/-\\")) | |
336 | |
337 fun tick libname cmdlist = | |
338 let val n = Vector.length tick_chars | |
339 fun pad_to n str = | |
340 if n <= String.size str then str | |
341 else pad_to n (str ^ " ") | |
342 val name = if libname <> "" then libname | |
343 else if cmdlist = nil then "" | |
344 else hd (rev cmdlist) | |
345 in | |
346 print (" " ^ | |
347 Vector.sub(tick_chars, !tick_cycle) ^ " " ^ | |
348 pad_to 70 name ^ | |
349 "\r"); | |
350 tick_cycle := (if !tick_cycle = n - 1 then 0 else 1 + !tick_cycle) | |
351 end | |
352 | |
353 fun run_command context libname cmdlist redirect = | |
354 let open OS | |
355 val dir = libpath context libname | |
356 val cmd = expand_commandline cmdlist | |
357 val _ = if verbose () | |
358 then print ("\n=== " ^ dir ^ "\n<<< " ^ cmd ^ "\n") | |
359 else tick libname cmdlist | |
360 val _ = FileSys.chDir dir | |
361 val status = case redirect of | |
362 NONE => Process.system cmd | |
363 | SOME file => Process.system (cmd ^ ">" ^ file) | |
364 in | |
365 if Process.isSuccess status | |
366 then OK () | |
367 else ERROR ("Command failed: " ^ cmd ^ " (in dir " ^ dir ^ ")") | |
368 end | |
369 handle ex => ERROR ("Unable to run command: " ^ exnMessage ex) | |
370 | |
371 fun command context libname cmdlist = | |
372 run_command context libname cmdlist NONE | |
373 | |
374 fun command_output context libname cmdlist = | |
375 let open OS | |
376 val tmpFile = FileSys.tmpName () | |
377 val result = run_command context libname cmdlist (SOME tmpFile) | |
378 val contents = file_contents tmpFile | |
379 val _ = if verbose () | |
380 then print (">>> \"" ^ contents ^ "\"\n") | |
381 else () | |
382 in | |
383 FileSys.remove tmpFile handle _ => (); | |
384 case result of | |
385 OK () => OK contents | |
386 | ERROR e => ERROR e | |
387 end | |
388 | |
389 fun mydir () = | |
390 let open OS | |
391 val { dir, file } = Path.splitDirFile (CommandLine.name ()) | |
392 in | |
393 FileSys.realPath | |
394 (if Path.isAbsolute dir | |
395 then dir | |
396 else Path.concat (FileSys.getDir (), dir)) | |
397 end | |
398 | |
399 fun homedir () = | |
400 (* Failure is not routine, so we use an exception here *) | |
401 case (OS.Process.getEnv "HOME", | |
402 OS.Process.getEnv "HOMEPATH") of | |
403 (SOME home, _) => home | |
404 | (NONE, SOME home) => home | |
405 | (NONE, NONE) => | |
406 raise Fail "Failed to look up home directory from environment" | |
407 | |
408 fun mkpath' path = | |
409 if OS.FileSys.isDir path handle _ => false | |
410 then OK () | |
411 else case OS.Path.fromString path of | |
412 { arcs = nil, ... } => OK () | |
413 | { isAbs = false, ... } => ERROR "mkpath requires absolute path" | |
414 | { isAbs, vol, arcs } => | |
415 case mkpath' (OS.Path.toString { (* parent *) | |
416 isAbs = isAbs, | |
417 vol = vol, | |
418 arcs = rev (tl (rev arcs)) }) of | |
419 ERROR e => ERROR e | |
420 | OK () => ((OS.FileSys.mkDir path; OK ()) | |
421 handle OS.SysErr (e, _) => | |
422 ERROR ("Directory creation failed: " ^ e)) | |
423 | |
424 fun mkpath path = | |
425 mkpath' (OS.Path.mkCanonical path) | |
426 | |
427 fun dir_contents dir = | |
428 let open OS | |
429 fun files_from dirstream = | |
430 case FileSys.readDir dirstream of | |
431 NONE => [] | |
432 | SOME file => | |
433 (* readDir is supposed to filter these, | |
434 but let's be extra cautious: *) | |
435 if file = Path.parentArc orelse file = Path.currentArc | |
436 then files_from dirstream | |
437 else file :: files_from dirstream | |
438 val stream = FileSys.openDir dir | |
439 val files = map (fn f => Path.joinDirFile | |
440 { dir = dir, file = f }) | |
441 (files_from stream) | |
442 val _ = FileSys.closeDir stream | |
443 in | |
444 files | |
445 end | |
446 | |
447 fun rmpath' path = | |
448 let open OS | |
449 fun remove path = | |
450 if FileSys.isLink path (* dangling links bother isDir *) | |
451 then FileSys.remove path | |
452 else if FileSys.isDir path | |
453 then (app remove (dir_contents path); FileSys.rmDir path) | |
454 else FileSys.remove path | |
455 in | |
456 (remove path; OK ()) | |
457 handle SysErr (e, _) => ERROR ("Path removal failed: " ^ e) | |
458 end | |
459 | |
460 fun rmpath path = | |
461 rmpath' (OS.Path.mkCanonical path) | |
462 | |
463 fun nonempty_dir_exists path = | |
464 let open OS.FileSys | |
465 in | |
466 (not (isLink path) andalso | |
467 isDir path andalso | |
468 dir_contents path <> []) | |
469 handle _ => false | |
470 end | |
471 | |
472 end | |
473 | |
474 functor LibControlFn (V: VCS_CONTROL) :> LIB_CONTROL = struct | |
475 | |
476 (* Valid states for unpinned libraries: | |
477 | |
478 - CORRECT: We are on the right branch and are up-to-date with | |
479 it as far as we can tell. (If not using the network, this | |
480 should be reported to user as "Present" rather than "Correct" | |
481 as the remote repo may have advanced without us knowing.) | |
482 | |
483 - SUPERSEDED: We are on the right branch but we can see that | |
484 there is a newer revision either locally or on the remote (in | |
485 Git terms, we are at an ancestor of the desired branch tip). | |
486 | |
487 - WRONG: We are on the wrong branch (in Git terms, we are not | |
488 at the desired branch tip or any ancestor of it). | |
489 | |
490 - ABSENT: Repo doesn't exist here at all. | |
491 | |
492 Valid states for pinned libraries: | |
493 | |
494 - CORRECT: We are at the pinned revision. | |
495 | |
496 - WRONG: We are at any revision other than the pinned one. | |
497 | |
498 - ABSENT: Repo doesn't exist here at all. | |
499 *) | |
500 | |
501 fun check with_network context | |
502 ({ libname, source, branch, | |
503 project_pin, lock_pin, ... } : libspec) = | |
504 let fun check_unpinned () = | |
505 let val newest = | |
506 if with_network | |
507 then V.is_newest context (libname, source, branch) | |
508 else V.is_newest_locally context (libname, branch) | |
509 in | |
510 case newest of | |
511 ERROR e => ERROR e | |
512 | OK true => OK CORRECT | |
513 | OK false => | |
514 case V.is_on_branch context (libname, branch) of | |
515 ERROR e => ERROR e | |
516 | OK true => OK SUPERSEDED | |
517 | OK false => OK WRONG | |
518 end | |
519 fun check_pinned target = | |
520 case V.is_at context (libname, target) of | |
521 ERROR e => ERROR e | |
522 | OK true => OK CORRECT | |
523 | OK false => OK WRONG | |
524 fun check_remote () = | |
525 case project_pin of | |
526 UNPINNED => check_unpinned () | |
527 | PINNED target => check_pinned target | |
528 fun check_local () = | |
529 case V.is_modified_locally context libname of | |
530 ERROR e => ERROR e | |
531 | OK true => OK MODIFIED | |
532 | OK false => | |
533 case lock_pin of | |
534 UNPINNED => OK CLEAN | |
535 | PINNED target => | |
536 case V.is_at context (libname, target) of | |
537 ERROR e => ERROR e | |
538 | OK true => OK CLEAN | |
539 | OK false => OK LOCK_MISMATCHED | |
540 in | |
541 case V.exists context libname of | |
542 ERROR e => ERROR e | |
543 | OK false => OK (ABSENT, CLEAN) | |
544 | OK true => | |
545 case (check_remote (), check_local ()) of | |
546 (ERROR e, _) => ERROR e | |
547 | (_, ERROR e) => ERROR e | |
548 | (OK r, OK l) => OK (r, l) | |
549 end | |
550 | |
551 val review = check true | |
552 val status = check false | |
553 | |
554 fun update context | |
555 ({ libname, source, branch, | |
556 project_pin, lock_pin, ... } : libspec) = | |
557 let fun update_unpinned () = | |
558 case V.is_newest context (libname, source, branch) of | |
559 ERROR e => ERROR e | |
560 | OK true => OK () | |
561 | OK false => V.update context (libname, source, branch) | |
562 fun update_pinned target = | |
563 case V.is_at context (libname, target) of | |
564 ERROR e => ERROR e | |
565 | OK true => OK () | |
566 | OK false => V.update_to context (libname, source, target) | |
567 fun update' () = | |
568 case lock_pin of | |
569 PINNED target => update_pinned target | |
570 | UNPINNED => | |
571 case project_pin of | |
572 PINNED target => update_pinned target | |
573 | UNPINNED => update_unpinned () | |
574 in | |
575 case V.exists context libname of | |
576 ERROR e => ERROR e | |
577 | OK true => update' () | |
578 | OK false => | |
579 case V.checkout context (libname, source, branch) of | |
580 ERROR e => ERROR e | |
581 | OK () => update' () | |
582 end | |
583 | |
584 fun id_of context ({ libname, ... } : libspec) = | |
585 V.id_of context libname | |
586 | |
587 fun is_working context vcs = | |
588 V.is_working context | |
589 | |
590 end | |
591 | |
592 (* Simple Standard ML JSON parser | |
593 https://bitbucket.org/cannam/sml-simplejson | |
594 Copyright 2017 Chris Cannam. BSD licence. | |
595 Parts based on the JSON parser in the Ponyo library by Phil Eaton. | |
596 *) | |
597 | |
598 signature JSON = sig | |
599 | |
600 datatype json = OBJECT of (string * json) list | |
601 | ARRAY of json list | |
602 | NUMBER of real | |
603 | STRING of string | |
604 | BOOL of bool | |
605 | NULL | |
606 | |
607 datatype 'a result = OK of 'a | |
608 | ERROR of string | |
609 | |
610 val parse : string -> json result | |
611 val serialise : json -> string | |
612 val serialiseIndented : json -> string | |
613 | |
614 end | |
615 | |
616 structure Json :> JSON = struct | |
617 | |
618 datatype json = OBJECT of (string * json) list | |
619 | ARRAY of json list | |
620 | NUMBER of real | |
621 | STRING of string | |
622 | BOOL of bool | |
623 | NULL | |
624 | |
625 datatype 'a result = OK of 'a | |
626 | ERROR of string | |
627 | |
628 structure T = struct | |
629 datatype token = NUMBER of char list | |
630 | STRING of string | |
631 | BOOL of bool | |
632 | NULL | |
633 | CURLY_L | |
634 | CURLY_R | |
635 | SQUARE_L | |
636 | SQUARE_R | |
637 | COLON | |
638 | COMMA | |
639 | |
640 fun toString t = | |
641 case t of NUMBER digits => implode digits | |
642 | STRING s => s | |
643 | BOOL b => Bool.toString b | |
644 | NULL => "null" | |
645 | CURLY_L => "{" | |
646 | CURLY_R => "}" | |
647 | SQUARE_L => "[" | |
648 | SQUARE_R => "]" | |
649 | COLON => ":" | |
650 | COMMA => "," | |
651 end | |
652 | |
653 fun bmpToUtf8 cp = (* convert a codepoint in Unicode BMP to utf8 bytes *) | |
654 let open Word | |
655 infix 6 orb andb >> | |
656 in | |
657 map (Char.chr o toInt) | |
658 (if cp < 0wx80 then | |
659 [cp] | |
660 else if cp < 0wx800 then | |
661 [0wxc0 orb (cp >> 0w6), 0wx80 orb (cp andb 0wx3f)] | |
662 else if cp < 0wx10000 then | |
663 [0wxe0 orb (cp >> 0w12), | |
664 0wx80 orb ((cp >> 0w6) andb 0wx3f), | |
665 0wx80 orb (cp andb 0wx3f)] | |
666 else raise Fail ("Invalid BMP point " ^ (Word.toString cp))) | |
667 end | |
668 | |
669 fun error pos text = ERROR (text ^ " at character position " ^ | |
670 Int.toString (pos - 1)) | |
671 fun token_error pos = error pos ("Unexpected token") | |
672 | |
673 fun lexNull pos acc (#"u" :: #"l" :: #"l" :: xs) = | |
674 lex (pos + 3) (T.NULL :: acc) xs | |
675 | lexNull pos acc _ = token_error pos | |
676 | |
677 and lexTrue pos acc (#"r" :: #"u" :: #"e" :: xs) = | |
678 lex (pos + 3) (T.BOOL true :: acc) xs | |
679 | lexTrue pos acc _ = token_error pos | |
680 | |
681 and lexFalse pos acc (#"a" :: #"l" :: #"s" :: #"e" :: xs) = | |
682 lex (pos + 4) (T.BOOL false :: acc) xs | |
683 | lexFalse pos acc _ = token_error pos | |
684 | |
685 and lexChar tok pos acc xs = | |
686 lex pos (tok :: acc) xs | |
687 | |
688 and lexString pos acc cc = | |
689 let datatype escaped = ESCAPED | NORMAL | |
690 fun lexString' pos text ESCAPED [] = | |
691 error pos "End of input during escape sequence" | |
692 | lexString' pos text NORMAL [] = | |
693 error pos "End of input during string" | |
694 | lexString' pos text ESCAPED (x :: xs) = | |
695 let fun esc c = lexString' (pos + 1) (c :: text) NORMAL xs | |
696 in case x of | |
697 #"\"" => esc x | |
698 | #"\\" => esc x | |
699 | #"/" => esc x | |
700 | #"b" => esc #"\b" | |
701 | #"f" => esc #"\f" | |
702 | #"n" => esc #"\n" | |
703 | #"r" => esc #"\r" | |
704 | #"t" => esc #"\t" | |
705 | _ => error pos ("Invalid escape \\" ^ | |
706 Char.toString x) | |
707 end | |
708 | lexString' pos text NORMAL (#"\\" :: #"u" ::a::b::c::d:: xs) = | |
709 if List.all Char.isHexDigit [a,b,c,d] | |
710 then case Word.fromString ("0wx" ^ (implode [a,b,c,d])) of | |
711 SOME w => (let val utf = rev (bmpToUtf8 w) in | |
712 lexString' (pos + 6) (utf @ text) | |
713 NORMAL xs | |
714 end | |
715 handle Fail err => error pos err) | |
716 | NONE => error pos "Invalid Unicode BMP escape sequence" | |
717 else error pos "Invalid Unicode BMP escape sequence" | |
718 | lexString' pos text NORMAL (x :: xs) = | |
719 if Char.ord x < 0x20 | |
720 then error pos "Invalid unescaped control character" | |
721 else | |
722 case x of | |
723 #"\"" => OK (rev text, xs, pos + 1) | |
724 | #"\\" => lexString' (pos + 1) text ESCAPED xs | |
725 | _ => lexString' (pos + 1) (x :: text) NORMAL xs | |
726 in | |
727 case lexString' pos [] NORMAL cc of | |
728 OK (text, rest, newpos) => | |
729 lex newpos (T.STRING (implode text) :: acc) rest | |
730 | ERROR e => ERROR e | |
731 end | |
732 | |
733 and lexNumber firstChar pos acc cc = | |
734 let val valid = explode ".+-e" | |
735 fun lexNumber' pos digits [] = (rev digits, [], pos) | |
736 | lexNumber' pos digits (x :: xs) = | |
737 if x = #"E" then lexNumber' (pos + 1) (#"e" :: digits) xs | |
738 else if Char.isDigit x orelse List.exists (fn c => x = c) valid | |
739 then lexNumber' (pos + 1) (x :: digits) xs | |
740 else (rev digits, x :: xs, pos) | |
741 val (digits, rest, newpos) = | |
742 lexNumber' (pos - 1) [] (firstChar :: cc) | |
743 in | |
744 case digits of | |
745 [] => token_error pos | |
746 | _ => lex newpos (T.NUMBER digits :: acc) rest | |
747 end | |
748 | |
749 and lex pos acc [] = OK (rev acc) | |
750 | lex pos acc (x::xs) = | |
751 (case x of | |
752 #" " => lex | |
753 | #"\t" => lex | |
754 | #"\n" => lex | |
755 | #"\r" => lex | |
756 | #"{" => lexChar T.CURLY_L | |
757 | #"}" => lexChar T.CURLY_R | |
758 | #"[" => lexChar T.SQUARE_L | |
759 | #"]" => lexChar T.SQUARE_R | |
760 | #":" => lexChar T.COLON | |
761 | #"," => lexChar T.COMMA | |
762 | #"\"" => lexString | |
763 | #"t" => lexTrue | |
764 | #"f" => lexFalse | |
765 | #"n" => lexNull | |
766 | x => lexNumber x) (pos + 1) acc xs | |
767 | |
768 fun show [] = "end of input" | |
769 | show (tok :: _) = T.toString tok | |
770 | |
771 fun parseNumber digits = | |
772 (* Note lexNumber already case-insensitised the E for us *) | |
773 let open Char | |
774 | |
775 fun okExpDigits [] = false | |
776 | okExpDigits (c :: []) = isDigit c | |
777 | okExpDigits (c :: cs) = isDigit c andalso okExpDigits cs | |
778 | |
779 fun okExponent [] = false | |
780 | okExponent (#"+" :: cs) = okExpDigits cs | |
781 | okExponent (#"-" :: cs) = okExpDigits cs | |
782 | okExponent cc = okExpDigits cc | |
783 | |
784 fun okFracTrailing [] = true | |
785 | okFracTrailing (c :: cs) = | |
786 (isDigit c andalso okFracTrailing cs) orelse | |
787 (c = #"e" andalso okExponent cs) | |
788 | |
789 fun okFraction [] = false | |
790 | okFraction (c :: cs) = | |
791 isDigit c andalso okFracTrailing cs | |
792 | |
793 fun okPosTrailing [] = true | |
794 | okPosTrailing (#"." :: cs) = okFraction cs | |
795 | okPosTrailing (#"e" :: cs) = okExponent cs | |
796 | okPosTrailing (c :: cs) = | |
797 isDigit c andalso okPosTrailing cs | |
798 | |
799 fun okPositive [] = false | |
800 | okPositive (#"0" :: []) = true | |
801 | okPositive (#"0" :: #"." :: cs) = okFraction cs | |
802 | okPositive (#"0" :: #"e" :: cs) = okExponent cs | |
803 | okPositive (#"0" :: cs) = false | |
804 | okPositive (c :: cs) = isDigit c andalso okPosTrailing cs | |
805 | |
806 fun okNumber (#"-" :: cs) = okPositive cs | |
807 | okNumber cc = okPositive cc | |
808 in | |
809 if okNumber digits | |
810 then case Real.fromString (implode digits) of | |
811 NONE => ERROR "Number out of range" | |
812 | SOME r => OK r | |
813 else ERROR ("Invalid number \"" ^ (implode digits) ^ "\"") | |
814 end | |
815 | |
816 fun parseObject (T.CURLY_R :: xs) = OK (OBJECT [], xs) | |
817 | parseObject tokens = | |
818 let fun parsePair (T.STRING key :: T.COLON :: xs) = | |
819 (case parseTokens xs of | |
820 ERROR e => ERROR e | |
821 | OK (j, xs) => OK ((key, j), xs)) | |
822 | parsePair other = | |
823 ERROR ("Object key/value pair expected around \"" ^ | |
824 show other ^ "\"") | |
825 fun parseObject' acc [] = ERROR "End of input during object" | |
826 | parseObject' acc tokens = | |
827 case parsePair tokens of | |
828 ERROR e => ERROR e | |
829 | OK (pair, T.COMMA :: xs) => | |
830 parseObject' (pair :: acc) xs | |
831 | OK (pair, T.CURLY_R :: xs) => | |
832 OK (OBJECT (rev (pair :: acc)), xs) | |
833 | OK (_, _) => ERROR "Expected , or } after object element" | |
834 in | |
835 parseObject' [] tokens | |
836 end | |
837 | |
838 and parseArray (T.SQUARE_R :: xs) = OK (ARRAY [], xs) | |
839 | parseArray tokens = | |
840 let fun parseArray' acc [] = ERROR "End of input during array" | |
841 | parseArray' acc tokens = | |
842 case parseTokens tokens of | |
843 ERROR e => ERROR e | |
844 | OK (j, T.COMMA :: xs) => parseArray' (j :: acc) xs | |
845 | OK (j, T.SQUARE_R :: xs) => OK (ARRAY (rev (j :: acc)), xs) | |
846 | OK (_, _) => ERROR "Expected , or ] after array element" | |
847 in | |
848 parseArray' [] tokens | |
849 end | |
850 | |
851 and parseTokens [] = ERROR "Value expected" | |
852 | parseTokens (tok :: xs) = | |
853 (case tok of | |
854 T.NUMBER d => (case parseNumber d of | |
855 OK r => OK (NUMBER r, xs) | |
856 | ERROR e => ERROR e) | |
857 | T.STRING s => OK (STRING s, xs) | |
858 | T.BOOL b => OK (BOOL b, xs) | |
859 | T.NULL => OK (NULL, xs) | |
860 | T.CURLY_L => parseObject xs | |
861 | T.SQUARE_L => parseArray xs | |
862 | _ => ERROR ("Unexpected token " ^ T.toString tok ^ | |
863 " before " ^ show xs)) | |
864 | |
865 fun parse str = | |
866 case lex 1 [] (explode str) of | |
867 ERROR e => ERROR e | |
868 | OK tokens => case parseTokens tokens of | |
869 OK (value, []) => OK value | |
870 | OK (_, _) => ERROR "Extra data after input" | |
871 | ERROR e => ERROR e | |
872 | |
873 fun stringEscape s = | |
874 let fun esc x = [x, #"\\"] | |
875 fun escape' acc [] = rev acc | |
876 | escape' acc (x :: xs) = | |
877 escape' (case x of | |
878 #"\"" => esc x @ acc | |
879 | #"\\" => esc x @ acc | |
880 | #"\b" => esc #"b" @ acc | |
881 | #"\f" => esc #"f" @ acc | |
882 | #"\n" => esc #"n" @ acc | |
883 | #"\r" => esc #"r" @ acc | |
884 | #"\t" => esc #"t" @ acc | |
885 | _ => | |
886 let val c = Char.ord x | |
887 in | |
888 if c < 0x20 | |
889 then let val hex = Word.toString (Word.fromInt c) | |
890 in (rev o explode) (if c < 0x10 | |
891 then ("\\u000" ^ hex) | |
892 else ("\\u00" ^ hex)) | |
893 end @ acc | |
894 else | |
895 x :: acc | |
896 end) | |
897 xs | |
898 in | |
899 implode (escape' [] (explode s)) | |
900 end | |
901 | |
902 fun serialise json = | |
903 case json of | |
904 OBJECT pp => "{" ^ String.concatWith | |
905 "," (map (fn (key, value) => | |
906 serialise (STRING key) ^ ":" ^ | |
907 serialise value) pp) ^ | |
908 "}" | |
909 | ARRAY arr => "[" ^ String.concatWith "," (map serialise arr) ^ "]" | |
910 | NUMBER n => implode (map (fn #"~" => #"-" | c => c) | |
911 (explode (Real.toString n))) | |
912 | STRING s => "\"" ^ stringEscape s ^ "\"" | |
913 | BOOL b => Bool.toString b | |
914 | NULL => "null" | |
915 | |
916 fun serialiseIndented json = | |
917 let fun indent 0 = "" | |
918 | indent i = " " ^ indent (i - 1) | |
919 fun serialiseIndented' i json = | |
920 let val ser = serialiseIndented' (i + 1) | |
921 in | |
922 case json of | |
923 OBJECT [] => "{}" | |
924 | ARRAY [] => "[]" | |
925 | OBJECT pp => "{\n" ^ indent (i + 1) ^ | |
926 String.concatWith | |
927 (",\n" ^ indent (i + 1)) | |
928 (map (fn (key, value) => | |
929 ser (STRING key) ^ ": " ^ | |
930 ser value) pp) ^ | |
931 "\n" ^ indent i ^ "}" | |
932 | ARRAY arr => "[\n" ^ indent (i + 1) ^ | |
933 String.concatWith | |
934 (",\n" ^ indent (i + 1)) | |
935 (map ser arr) ^ | |
936 "\n" ^ indent i ^ "]" | |
937 | other => serialise other | |
938 end | |
939 in | |
940 serialiseIndented' 0 json ^ "\n" | |
941 end | |
942 | |
943 end | |
944 | |
945 | |
946 structure JsonBits :> sig | |
947 exception Config of string | |
948 val load_json_from : string -> Json.json (* filename -> json *) | |
949 val save_json_to : string -> Json.json -> unit | |
950 val lookup_optional : Json.json -> string list -> Json.json option | |
951 val lookup_optional_string : Json.json -> string list -> string option | |
952 val lookup_mandatory : Json.json -> string list -> Json.json | |
953 val lookup_mandatory_string : Json.json -> string list -> string | |
954 end = struct | |
955 | |
956 exception Config of string | |
957 | |
958 fun load_json_from filename = | |
959 case Json.parse (FileBits.file_contents filename) of | |
960 Json.OK json => json | |
961 | Json.ERROR e => raise Config ("Failed to parse file: " ^ e) | |
962 | |
963 fun save_json_to filename json = | |
964 (* using binary I/O to avoid ever writing CR/LF line endings *) | |
965 let val jstr = Json.serialiseIndented json | |
966 val stream = BinIO.openOut filename | |
967 in | |
968 BinIO.output (stream, Byte.stringToBytes jstr); | |
969 BinIO.closeOut stream | |
970 end | |
971 | |
972 fun lookup_optional json kk = | |
973 let fun lookup key = | |
974 case json of | |
975 Json.OBJECT kvs => | |
976 (case List.filter (fn (k, v) => k = key) kvs of | |
977 [] => NONE | |
978 | [(_,v)] => SOME v | |
979 | _ => raise Config ("Duplicate key: " ^ | |
980 (String.concatWith " -> " kk))) | |
981 | _ => raise Config "Object expected" | |
982 in | |
983 case kk of | |
984 [] => NONE | |
985 | key::[] => lookup key | |
986 | key::kk => case lookup key of | |
987 NONE => NONE | |
988 | SOME j => lookup_optional j kk | |
989 end | |
990 | |
991 fun lookup_optional_string json kk = | |
992 case lookup_optional json kk of | |
993 SOME (Json.STRING s) => SOME s | |
994 | SOME _ => raise Config ("Value (if present) must be string: " ^ | |
995 (String.concatWith " -> " kk)) | |
996 | NONE => NONE | |
997 | |
998 fun lookup_mandatory json kk = | |
999 case lookup_optional json kk of | |
1000 SOME v => v | |
1001 | NONE => raise Config ("Value is mandatory: " ^ | |
1002 (String.concatWith " -> " kk)) | |
1003 | |
1004 fun lookup_mandatory_string json kk = | |
1005 case lookup_optional json kk of | |
1006 SOME (Json.STRING s) => s | |
1007 | _ => raise Config ("Value must be string: " ^ | |
1008 (String.concatWith " -> " kk)) | |
1009 end | |
1010 | |
1011 structure Provider :> sig | |
1012 val load_providers : Json.json -> provider list | |
1013 val load_more_providers : provider list -> Json.json -> provider list | |
1014 val remote_url : context -> vcs -> source -> libname -> string | |
1015 end = struct | |
1016 | |
1017 val known_providers : provider list = | |
1018 [ { | |
1019 service = "bitbucket", | |
1020 supports = [HG, GIT], | |
1021 remote_spec = { | |
1022 anon = SOME "https://bitbucket.org/{owner}/{repository}", | |
1023 auth = SOME "ssh://{vcs}@bitbucket.org/{owner}/{repository}" | |
1024 } | |
1025 }, | |
1026 { | |
1027 service = "github", | |
1028 supports = [GIT], | |
1029 remote_spec = { | |
1030 anon = SOME "https://github.com/{owner}/{repository}", | |
1031 auth = SOME "ssh://{vcs}@github.com/{owner}/{repository}" | |
1032 } | |
1033 } | |
1034 ] | |
1035 | |
1036 fun vcs_name vcs = | |
1037 case vcs of HG => "hg" | |
1038 | GIT => "git" | |
1039 | SVN => "svn" | |
1040 | |
1041 fun vcs_from_name name = | |
1042 case name of "hg" => HG | |
1043 | "git" => GIT | |
1044 | "svn" => SVN | |
1045 | other => raise Fail ("Unknown vcs name \"" ^ name ^ "\"") | |
1046 | |
1047 fun load_more_providers previously_loaded json = | |
1048 let open JsonBits | |
1049 fun load pjson pname : provider = | |
1050 { | |
1051 service = pname, | |
1052 supports = | |
1053 case lookup_mandatory pjson ["vcs"] of | |
1054 Json.ARRAY vv => | |
1055 map (fn (Json.STRING v) => vcs_from_name v | |
1056 | _ => raise Fail "Strings expected in vcs array") | |
1057 vv | |
1058 | _ => raise Fail "Array expected for vcs", | |
1059 remote_spec = { | |
1060 anon = lookup_optional_string pjson ["anonymous"], | |
1061 auth = lookup_optional_string pjson ["authenticated"] | |
1062 } | |
1063 } | |
1064 val loaded = | |
1065 case lookup_optional json ["services"] of | |
1066 NONE => [] | |
1067 | SOME (Json.OBJECT pl) => map (fn (k, v) => load v k) pl | |
1068 | _ => raise Fail "Object expected for services in config" | |
1069 val newly_loaded = | |
1070 List.filter (fn p => not (List.exists (fn pp => #service p = | |
1071 #service pp) | |
1072 previously_loaded)) | |
1073 loaded | |
1074 in | |
1075 previously_loaded @ newly_loaded | |
1076 end | |
1077 | |
1078 fun load_providers json = | |
1079 load_more_providers known_providers json | |
1080 | |
1081 fun expand_spec spec { vcs, service, owner, repo } login = | |
1082 (* ugly *) | |
1083 let fun replace str = | |
1084 case str of | |
1085 "vcs" => vcs_name vcs | |
1086 | "service" => service | |
1087 | "owner" => | |
1088 (case owner of | |
1089 SOME ostr => ostr | |
1090 | NONE => raise Fail ("Owner not specified for service " ^ | |
1091 service)) | |
1092 | "repository" => repo | |
1093 | "account" => | |
1094 (case login of | |
1095 SOME acc => acc | |
1096 | NONE => raise Fail ("Account not given for service " ^ | |
1097 service)) | |
1098 | other => raise Fail ("Unknown variable \"" ^ other ^ | |
1099 "\" in spec for service " ^ service) | |
1100 fun expand' acc sstr = | |
1101 case Substring.splitl (fn c => c <> #"{") sstr of | |
1102 (pfx, sfx) => | |
1103 if Substring.isEmpty sfx | |
1104 then rev (pfx :: acc) | |
1105 else | |
1106 case Substring.splitl (fn c => c <> #"}") sfx of | |
1107 (tok, remainder) => | |
1108 if Substring.isEmpty remainder | |
1109 then rev (tok :: pfx :: acc) | |
1110 else let val replacement = | |
1111 replace | |
1112 (* tok begins with "{": *) | |
1113 (Substring.string | |
1114 (Substring.triml 1 tok)) | |
1115 in | |
1116 expand' (Substring.full replacement :: | |
1117 pfx :: acc) | |
1118 (* remainder begins with "}": *) | |
1119 (Substring.triml 1 remainder) | |
1120 end | |
1121 in | |
1122 Substring.concat (expand' [] (Substring.full spec)) | |
1123 end | |
1124 | |
1125 fun provider_url req login providers = | |
1126 case providers of | |
1127 [] => raise Fail ("Unknown service \"" ^ (#service req) ^ | |
1128 "\" for vcs \"" ^ (vcs_name (#vcs req)) ^ "\"") | |
1129 | ({ service, supports, remote_spec : remote_spec } :: rest) => | |
1130 if service <> (#service req) orelse | |
1131 not (List.exists (fn v => v = (#vcs req)) supports) | |
1132 then provider_url req login rest | |
1133 else | |
1134 case (login, #auth remote_spec, #anon remote_spec) of | |
1135 (SOME _, SOME auth, _) => expand_spec auth req login | |
1136 | (SOME _, _, SOME anon) => expand_spec anon req NONE | |
1137 | (NONE, _, SOME anon) => expand_spec anon req NONE | |
1138 | _ => raise Fail ("No suitable anonymous or authenticated " ^ | |
1139 "URL spec provided for service \"" ^ | |
1140 service ^ "\"") | |
1141 | |
1142 fun login_for ({ accounts, ... } : context) service = | |
1143 case List.find (fn a => service = #service a) accounts of | |
1144 SOME { login, ... } => SOME login | |
1145 | NONE => NONE | |
1146 | |
1147 fun reponame_for path = | |
1148 case String.tokens (fn c => c = #"/") path of | |
1149 [] => raise Fail "Non-empty library path required" | |
1150 | toks => hd (rev toks) | |
1151 | |
1152 fun remote_url (context : context) vcs source libname = | |
1153 case source of | |
1154 URL_SOURCE u => u | |
1155 | SERVICE_SOURCE { service, owner, repo } => | |
1156 provider_url { vcs = vcs, | |
1157 service = service, | |
1158 owner = owner, | |
1159 repo = case repo of | |
1160 SOME r => r | |
1161 | NONE => reponame_for libname } | |
1162 (login_for context service) | |
1163 (#providers context) | |
1164 end | |
1165 | |
1166 structure HgControl :> VCS_CONTROL = struct | |
1167 | |
1168 (* Pulls always use an explicit URL, never just the default | |
1169 remote, in order to ensure we update properly if the location | |
1170 given in the project file changes. *) | |
1171 | |
1172 type vcsstate = { id: string, modified: bool, | |
1173 branch: string, tags: string list } | |
1174 | |
1175 val hg_program = "hg" | |
1176 | |
1177 val hg_args = [ "--config", "ui.interactive=true", | |
1178 "--config", "ui.merge=:merge" ] | |
1179 | |
1180 fun hg_command context libname args = | |
1181 FileBits.command context libname (hg_program :: hg_args @ args) | |
1182 | |
1183 fun hg_command_output context libname args = | |
1184 FileBits.command_output context libname (hg_program :: hg_args @ args) | |
1185 | |
1186 fun is_working context = | |
1187 case hg_command_output context "" ["--version"] of | |
1188 OK "" => OK false | |
1189 | OK _ => OK true | |
1190 | ERROR e => ERROR e | |
1191 | |
1192 fun exists context libname = | |
1193 OK (OS.FileSys.isDir (FileBits.subpath context libname ".hg")) | |
1194 handle _ => OK false | |
1195 | |
1196 fun remote_for context (libname, source) = | |
1197 Provider.remote_url context HG source libname | |
1198 | |
1199 fun current_state context libname : vcsstate result = | |
1200 let fun is_branch text = text <> "" andalso #"(" = hd (explode text) | |
1201 and extract_branch b = | |
1202 if is_branch b (* need to remove enclosing parens *) | |
1203 then (implode o rev o tl o rev o tl o explode) b | |
1204 else "default" | |
1205 and is_modified id = id <> "" andalso #"+" = hd (rev (explode id)) | |
1206 and extract_id id = | |
1207 if is_modified id (* need to remove trailing "+" *) | |
1208 then (implode o rev o tl o rev o explode) id | |
1209 else id | |
1210 and split_tags tags = String.tokens (fn c => c = #"/") tags | |
1211 and state_for (id, branch, tags) = | |
1212 OK { id = extract_id id, | |
1213 modified = is_modified id, | |
1214 branch = extract_branch branch, | |
1215 tags = split_tags tags } | |
1216 in | |
1217 case hg_command_output context libname ["id"] of | |
1218 ERROR e => ERROR e | |
1219 | OK out => | |
1220 case String.tokens (fn x => x = #" ") out of | |
1221 [id, branch, tags] => state_for (id, branch, tags) | |
1222 | [id, other] => if is_branch other | |
1223 then state_for (id, other, "") | |
1224 else state_for (id, "", other) | |
1225 | [id] => state_for (id, "", "") | |
1226 | _ => ERROR ("Unexpected output from hg id: " ^ out) | |
1227 end | |
1228 | |
1229 fun branch_name branch = case branch of | |
1230 DEFAULT_BRANCH => "default" | |
1231 | BRANCH "" => "default" | |
1232 | BRANCH b => b | |
1233 | |
1234 fun id_of context libname = | |
1235 case current_state context libname of | |
1236 ERROR e => ERROR e | |
1237 | OK { id, ... } => OK id | |
1238 | |
1239 fun is_at context (libname, id_or_tag) = | |
1240 case current_state context libname of | |
1241 ERROR e => ERROR e | |
1242 | OK { id, tags, ... } => | |
1243 OK (String.isPrefix id_or_tag id orelse | |
1244 String.isPrefix id id_or_tag orelse | |
1245 List.exists (fn t => t = id_or_tag) tags) | |
1246 | |
1247 fun is_on_branch context (libname, b) = | |
1248 case current_state context libname of | |
1249 ERROR e => ERROR e | |
1250 | OK { branch, ... } => OK (branch = branch_name b) | |
1251 | |
1252 fun is_newest_locally context (libname, branch) = | |
1253 case hg_command_output context libname | |
1254 ["log", "-l1", | |
1255 "-b", branch_name branch, | |
1256 "--template", "{node}"] of | |
1257 ERROR e => OK false (* desired branch does not exist *) | |
1258 | OK newest_in_repo => is_at context (libname, newest_in_repo) | |
1259 | |
1260 fun pull context (libname, source) = | |
1261 let val url = remote_for context (libname, source) | |
1262 in | |
1263 hg_command context libname | |
1264 (if FileBits.verbose () | |
1265 then ["pull", url] | |
1266 else ["pull", "-q", url]) | |
1267 end | |
1268 | |
1269 fun is_newest context (libname, source, branch) = | |
1270 case is_newest_locally context (libname, branch) of | |
1271 ERROR e => ERROR e | |
1272 | OK false => OK false | |
1273 | OK true => | |
1274 case pull context (libname, source) of | |
1275 ERROR e => ERROR e | |
1276 | _ => is_newest_locally context (libname, branch) | |
1277 | |
1278 fun is_modified_locally context libname = | |
1279 case current_state context libname of | |
1280 ERROR e => ERROR e | |
1281 | OK { modified, ... } => OK modified | |
1282 | |
1283 fun checkout context (libname, source, branch) = | |
1284 let val url = remote_for context (libname, source) | |
1285 in | |
1286 (* make the lib dir rather than just the ext dir, since | |
1287 the lib dir might be nested and hg will happily check | |
1288 out into an existing empty dir anyway *) | |
1289 case FileBits.mkpath (FileBits.libpath context libname) of | |
1290 ERROR e => ERROR e | |
1291 | _ => hg_command context "" | |
1292 ["clone", "-u", branch_name branch, | |
1293 url, libname] | |
1294 end | |
1295 | |
1296 fun update context (libname, source, branch) = | |
1297 let val pull_result = pull context (libname, source) | |
1298 in | |
1299 case hg_command context libname ["update", branch_name branch] of | |
1300 ERROR e => ERROR e | |
1301 | _ => | |
1302 case pull_result of | |
1303 ERROR e => ERROR e | |
1304 | _ => OK () | |
1305 end | |
1306 | |
1307 fun update_to context (libname, _, "") = | |
1308 ERROR "Non-empty id (tag or revision id) required for update_to" | |
1309 | update_to context (libname, source, id) = | |
1310 let val pull_result = pull context (libname, source) | |
1311 in | |
1312 case hg_command context libname ["update", "-r", id] of | |
1313 OK _ => OK () | |
1314 | ERROR e => | |
1315 case pull_result of | |
1316 ERROR e' => ERROR e' (* this was the ur-error *) | |
1317 | _ => ERROR e | |
1318 end | |
1319 | |
1320 fun copy_url_for context libname = | |
1321 OK (FileBits.file_url (FileBits.libpath context libname)) | |
1322 | |
1323 end | |
1324 | |
1325 structure GitControl :> VCS_CONTROL = struct | |
1326 | |
1327 (* With Git repos we always operate in detached HEAD state. Even | |
1328 the master branch is checked out using a remote reference | |
1329 (repoint/master). The remote we use is always named repoint, and we | |
1330 update it to the expected URL each time we fetch, in order to | |
1331 ensure we update properly if the location given in the project | |
1332 file changes. The origin remote is unused. *) | |
1333 | |
1334 val git_program = "git" | |
1335 | |
1336 fun git_command context libname args = | |
1337 FileBits.command context libname (git_program :: args) | |
1338 | |
1339 fun git_command_output context libname args = | |
1340 FileBits.command_output context libname (git_program :: args) | |
1341 | |
1342 fun is_working context = | |
1343 case git_command_output context "" ["--version"] of | |
1344 OK "" => OK false | |
1345 | OK _ => OK true | |
1346 | ERROR e => ERROR e | |
1347 | |
1348 fun exists context libname = | |
1349 OK (OS.FileSys.isDir (FileBits.subpath context libname ".git")) | |
1350 handle _ => OK false | |
1351 | |
1352 fun remote_for context (libname, source) = | |
1353 Provider.remote_url context GIT source libname | |
1354 | |
1355 fun branch_name branch = case branch of | |
1356 DEFAULT_BRANCH => "master" | |
1357 | BRANCH "" => "master" | |
1358 | BRANCH b => b | |
1359 | |
1360 val our_remote = "repoint" | |
1361 | |
1362 fun remote_branch_name branch = our_remote ^ "/" ^ branch_name branch | |
1363 | |
1364 fun checkout context (libname, source, branch) = | |
1365 let val url = remote_for context (libname, source) | |
1366 in | |
1367 (* make the lib dir rather than just the ext dir, since | |
1368 the lib dir might be nested and git will happily check | |
1369 out into an existing empty dir anyway *) | |
1370 case FileBits.mkpath (FileBits.libpath context libname) of | |
1371 OK () => git_command context "" | |
1372 ["clone", "--origin", our_remote, | |
1373 "--branch", branch_name branch, | |
1374 url, libname] | |
1375 | ERROR e => ERROR e | |
1376 end | |
1377 | |
1378 fun add_our_remote context (libname, source) = | |
1379 (* When we do the checkout ourselves (above), we add the | |
1380 remote at the same time. But if the repo was cloned by | |
1381 someone else, we'll need to do it after the fact. Git | |
1382 doesn't seem to have a means to add a remote or change its | |
1383 url if it already exists; seems we have to do this: *) | |
1384 let val url = remote_for context (libname, source) | |
1385 in | |
1386 case git_command context libname | |
1387 ["remote", "set-url", our_remote, url] of | |
1388 OK () => OK () | |
1389 | ERROR e => git_command context libname | |
1390 ["remote", "add", "-f", our_remote, url] | |
1391 end | |
1392 | |
1393 (* NB git rev-parse HEAD shows revision id of current checkout; | |
1394 git rev-list -1 <tag> shows revision id of revision with that tag *) | |
1395 | |
1396 fun id_of context libname = | |
1397 git_command_output context libname ["rev-parse", "HEAD"] | |
1398 | |
1399 fun is_at context (libname, id_or_tag) = | |
1400 case id_of context libname of | |
1401 ERROR e => OK false (* HEAD nonexistent, expected in empty repo *) | |
1402 | OK id => | |
1403 if String.isPrefix id_or_tag id orelse | |
1404 String.isPrefix id id_or_tag | |
1405 then OK true | |
1406 else is_at_tag context (libname, id, id_or_tag) | |
1407 | |
1408 and is_at_tag context (libname, id, tag) = | |
1409 (* For annotated tags (with message) show-ref returns the tag | |
1410 object ref rather than that of the revision being tagged; | |
1411 we need the subsequent rev-list to chase that up. In fact | |
1412 the rev-list on its own is enough to get us the id direct | |
1413 from the tag name, but it fails with an error if the tag | |
1414 doesn't exist, whereas we want to handle that quietly in | |
1415 case the tag simply hasn't been pulled yet *) | |
1416 case git_command_output context libname | |
1417 ["show-ref", "refs/tags/" ^ tag, "--"] of | |
1418 OK "" => OK false (* Not a tag *) | |
1419 | ERROR _ => OK false | |
1420 | OK s => | |
1421 let val tag_ref = hd (String.tokens (fn c => c = #" ") s) | |
1422 in | |
1423 case git_command_output context libname | |
1424 ["rev-list", "-1", tag_ref] of | |
1425 OK tagged => OK (id = tagged) | |
1426 | ERROR _ => OK false | |
1427 end | |
1428 | |
1429 fun branch_tip context (libname, branch) = | |
1430 (* We don't have access to the source info or the network | |
1431 here, as this is used by status (e.g. via is_on_branch) as | |
1432 well as review. It's possible the remote branch won't exist, | |
1433 e.g. if the repo was checked out by something other than | |
1434 Repoint, and if that's the case, we can't add it here; we'll | |
1435 just have to fail, since checking against local branches | |
1436 instead could produce the wrong result. *) | |
1437 git_command_output context libname | |
1438 ["rev-list", "-1", | |
1439 remote_branch_name branch, "--"] | |
1440 | |
1441 fun is_newest_locally context (libname, branch) = | |
1442 case branch_tip context (libname, branch) of | |
1443 ERROR e => OK false | |
1444 | OK rev => is_at context (libname, rev) | |
1445 | |
1446 fun is_on_branch context (libname, branch) = | |
1447 case branch_tip context (libname, branch) of | |
1448 ERROR e => OK false | |
1449 | OK rev => | |
1450 case is_at context (libname, rev) of | |
1451 ERROR e => ERROR e | |
1452 | OK true => OK true | |
1453 | OK false => | |
1454 case git_command context libname | |
1455 ["merge-base", "--is-ancestor", | |
1456 "HEAD", remote_branch_name branch] of | |
1457 ERROR e => OK false (* cmd returns non-zero for no *) | |
1458 | _ => OK true | |
1459 | |
1460 fun fetch context (libname, source) = | |
1461 case add_our_remote context (libname, source) of | |
1462 ERROR e => ERROR e | |
1463 | _ => git_command context libname ["fetch", our_remote] | |
1464 | |
1465 fun is_newest context (libname, source, branch) = | |
1466 case add_our_remote context (libname, source) of | |
1467 ERROR e => ERROR e | |
1468 | OK () => | |
1469 case is_newest_locally context (libname, branch) of | |
1470 ERROR e => ERROR e | |
1471 | OK false => OK false | |
1472 | OK true => | |
1473 case fetch context (libname, source) of | |
1474 ERROR e => ERROR e | |
1475 | _ => is_newest_locally context (libname, branch) | |
1476 | |
1477 fun is_modified_locally context libname = | |
1478 case git_command_output context libname ["status", "--porcelain"] of | |
1479 ERROR e => ERROR e | |
1480 | OK "" => OK false | |
1481 | OK _ => OK true | |
1482 | |
1483 (* This function updates to the latest revision on a branch rather | |
1484 than to a specific id or tag. We can't just checkout the given | |
1485 branch, as that will succeed even if the branch isn't up to | |
1486 date. We could checkout the branch and then fetch and merge, | |
1487 but it's perhaps cleaner not to maintain a local branch at all, | |
1488 but instead checkout the remote branch as a detached head. *) | |
1489 | |
1490 fun update context (libname, source, branch) = | |
1491 case fetch context (libname, source) of | |
1492 ERROR e => ERROR e | |
1493 | _ => | |
1494 case git_command context libname ["checkout", "--detach", | |
1495 remote_branch_name branch] of | |
1496 ERROR e => ERROR e | |
1497 | _ => OK () | |
1498 | |
1499 (* This function is dealing with a specific id or tag, so if we | |
1500 can successfully check it out (detached) then that's all we | |
1501 need to do, regardless of whether fetch succeeded or not. We do | |
1502 attempt the fetch first, though, purely in order to avoid ugly | |
1503 error messages in the common case where we're being asked to | |
1504 update to a new pin (from the lock file) that hasn't been | |
1505 fetched yet. *) | |
1506 | |
1507 fun update_to context (libname, _, "") = | |
1508 ERROR "Non-empty id (tag or revision id) required for update_to" | |
1509 | update_to context (libname, source, id) = | |
1510 let val fetch_result = fetch context (libname, source) | |
1511 in | |
1512 case git_command context libname ["checkout", "--detach", id] of | |
1513 OK _ => OK () | |
1514 | ERROR e => | |
1515 case fetch_result of | |
1516 ERROR e' => ERROR e' (* this was the ur-error *) | |
1517 | _ => ERROR e | |
1518 end | |
1519 | |
1520 fun copy_url_for context libname = | |
1521 OK (FileBits.file_url (FileBits.libpath context libname)) | |
1522 | |
1523 end | |
1524 | |
1525 (* SubXml - A parser for a subset of XML | |
1526 https://bitbucket.org/cannam/sml-subxml | |
1527 Copyright 2018 Chris Cannam. BSD licence. | |
1528 *) | |
1529 | |
1530 signature SUBXML = sig | |
1531 | |
1532 datatype node = ELEMENT of { name : string, children : node list } | |
1533 | ATTRIBUTE of { name : string, value : string } | |
1534 | TEXT of string | |
1535 | CDATA of string | |
1536 | COMMENT of string | |
1537 | |
1538 datatype document = DOCUMENT of { name : string, children : node list } | |
1539 | |
1540 datatype 'a result = OK of 'a | |
1541 | ERROR of string | |
1542 | |
1543 val parse : string -> document result | |
1544 val serialise : document -> string | |
1545 | |
1546 end | |
1547 | |
1548 structure SubXml :> SUBXML = struct | |
1549 | |
1550 datatype node = ELEMENT of { name : string, children : node list } | |
1551 | ATTRIBUTE of { name : string, value : string } | |
1552 | TEXT of string | |
1553 | CDATA of string | |
1554 | COMMENT of string | |
1555 | |
1556 datatype document = DOCUMENT of { name : string, children : node list } | |
1557 | |
1558 datatype 'a result = OK of 'a | |
1559 | ERROR of string | |
1560 | |
1561 structure T = struct | |
1562 datatype token = ANGLE_L | |
1563 | ANGLE_R | |
1564 | ANGLE_SLASH_L | |
1565 | SLASH_ANGLE_R | |
1566 | EQUAL | |
1567 | NAME of string | |
1568 | TEXT of string | |
1569 | CDATA of string | |
1570 | COMMENT of string | |
1571 | |
1572 fun name t = | |
1573 case t of ANGLE_L => "<" | |
1574 | ANGLE_R => ">" | |
1575 | ANGLE_SLASH_L => "</" | |
1576 | SLASH_ANGLE_R => "/>" | |
1577 | EQUAL => "=" | |
1578 | NAME s => "name \"" ^ s ^ "\"" | |
1579 | TEXT s => "text" | |
1580 | CDATA _ => "CDATA section" | |
1581 | COMMENT _ => "comment" | |
1582 end | |
1583 | |
1584 structure Lex :> sig | |
1585 val lex : string -> T.token list result | |
1586 end = struct | |
1587 | |
1588 fun error pos text = | |
1589 ERROR (text ^ " at character position " ^ Int.toString (pos-1)) | |
1590 fun tokenError pos token = | |
1591 error pos ("Unexpected token '" ^ Char.toString token ^ "'") | |
1592 | |
1593 val nameEnd = explode " \t\n\r\"'</>!=?" | |
1594 | |
1595 fun quoted quote pos acc cc = | |
1596 let fun quoted' pos text [] = | |
1597 error pos "Document ends during quoted string" | |
1598 | quoted' pos text (x::xs) = | |
1599 if x = quote | |
1600 then OK (rev text, xs, pos+1) | |
1601 else quoted' (pos+1) (x::text) xs | |
1602 in | |
1603 case quoted' pos [] cc of | |
1604 ERROR e => ERROR e | |
1605 | OK (text, rest, newpos) => | |
1606 inside newpos (T.TEXT (implode text) :: acc) rest | |
1607 end | |
1608 | |
1609 and name first pos acc cc = | |
1610 let fun name' pos text [] = | |
1611 error pos "Document ends during name" | |
1612 | name' pos text (x::xs) = | |
1613 if List.find (fn c => c = x) nameEnd <> NONE | |
1614 then OK (rev text, (x::xs), pos) | |
1615 else name' (pos+1) (x::text) xs | |
1616 in | |
1617 case name' (pos-1) [] (first::cc) of | |
1618 ERROR e => ERROR e | |
1619 | OK ([], [], pos) => error pos "Document ends before name" | |
1620 | OK ([], (x::xs), pos) => tokenError pos x | |
1621 | OK (text, rest, pos) => | |
1622 inside pos (T.NAME (implode text) :: acc) rest | |
1623 end | |
1624 | |
1625 and comment pos acc cc = | |
1626 let fun comment' pos text cc = | |
1627 case cc of | |
1628 #"-" :: #"-" :: #">" :: xs => OK (rev text, xs, pos+3) | |
1629 | x :: xs => comment' (pos+1) (x::text) xs | |
1630 | [] => error pos "Document ends during comment" | |
1631 in | |
1632 case comment' pos [] cc of | |
1633 ERROR e => ERROR e | |
1634 | OK (text, rest, pos) => | |
1635 outside pos (T.COMMENT (implode text) :: acc) rest | |
1636 end | |
1637 | |
1638 and instruction pos acc cc = | |
1639 case cc of | |
1640 #"?" :: #">" :: xs => outside (pos+2) acc xs | |
1641 | #">" :: _ => tokenError pos #">" | |
1642 | x :: xs => instruction (pos+1) acc xs | |
1643 | [] => error pos "Document ends during processing instruction" | |
1644 | |
1645 and cdata pos acc cc = | |
1646 let fun cdata' pos text cc = | |
1647 case cc of | |
1648 #"]" :: #"]" :: #">" :: xs => OK (rev text, xs, pos+3) | |
1649 | x :: xs => cdata' (pos+1) (x::text) xs | |
1650 | [] => error pos "Document ends during CDATA section" | |
1651 in | |
1652 case cdata' pos [] cc of | |
1653 ERROR e => ERROR e | |
1654 | OK (text, rest, pos) => | |
1655 outside pos (T.CDATA (implode text) :: acc) rest | |
1656 end | |
1657 | |
1658 and doctype pos acc cc = | |
1659 case cc of | |
1660 #">" :: xs => outside (pos+1) acc xs | |
1661 | x :: xs => doctype (pos+1) acc xs | |
1662 | [] => error pos "Document ends during DOCTYPE" | |
1663 | |
1664 and declaration pos acc cc = | |
1665 case cc of | |
1666 #"-" :: #"-" :: xs => | |
1667 comment (pos+2) acc xs | |
1668 | #"[" :: #"C" :: #"D" :: #"A" :: #"T" :: #"A" :: #"[" :: xs => | |
1669 cdata (pos+7) acc xs | |
1670 | #"D" :: #"O" :: #"C" :: #"T" :: #"Y" :: #"P" :: #"E" :: xs => | |
1671 doctype (pos+7) acc xs | |
1672 | [] => error pos "Document ends during declaration" | |
1673 | _ => error pos "Unsupported declaration type" | |
1674 | |
1675 and left pos acc cc = | |
1676 case cc of | |
1677 #"/" :: xs => inside (pos+1) (T.ANGLE_SLASH_L :: acc) xs | |
1678 | #"!" :: xs => declaration (pos+1) acc xs | |
1679 | #"?" :: xs => instruction (pos+1) acc xs | |
1680 | xs => inside pos (T.ANGLE_L :: acc) xs | |
1681 | |
1682 and slash pos acc cc = | |
1683 case cc of | |
1684 #">" :: xs => outside (pos+1) (T.SLASH_ANGLE_R :: acc) xs | |
1685 | x :: _ => tokenError pos x | |
1686 | [] => error pos "Document ends before element closed" | |
1687 | |
1688 and close pos acc xs = outside pos (T.ANGLE_R :: acc) xs | |
1689 | |
1690 and equal pos acc xs = inside pos (T.EQUAL :: acc) xs | |
1691 | |
1692 and outside pos acc [] = OK acc | |
1693 | outside pos acc cc = | |
1694 let fun textOf text = T.TEXT (implode (rev text)) | |
1695 fun outside' pos [] acc [] = OK acc | |
1696 | outside' pos text acc [] = OK (textOf text :: acc) | |
1697 | outside' pos text acc (x::xs) = | |
1698 case x of | |
1699 #"<" => if text = [] | |
1700 then left (pos+1) acc xs | |
1701 else left (pos+1) (textOf text :: acc) xs | |
1702 | x => outside' (pos+1) (x::text) acc xs | |
1703 in | |
1704 outside' pos [] acc cc | |
1705 end | |
1706 | |
1707 and inside pos acc [] = error pos "Document ends within tag" | |
1708 | inside pos acc (#"<"::_) = tokenError pos #"<" | |
1709 | inside pos acc (x::xs) = | |
1710 (case x of | |
1711 #" " => inside | #"\t" => inside | |
1712 | #"\n" => inside | #"\r" => inside | |
1713 | #"\"" => quoted x | #"'" => quoted x | |
1714 | #"/" => slash | #">" => close | #"=" => equal | |
1715 | x => name x) (pos+1) acc xs | |
1716 | |
1717 fun lex str = | |
1718 case outside 1 [] (explode str) of | |
1719 ERROR e => ERROR e | |
1720 | OK tokens => OK (rev tokens) | |
1721 end | |
1722 | |
1723 structure Parse :> sig | |
1724 val parse : string -> document result | |
1725 end = struct | |
1726 | |
1727 fun show [] = "end of input" | |
1728 | show (tok :: _) = T.name tok | |
1729 | |
1730 fun error toks text = ERROR (text ^ " before " ^ show toks) | |
1731 | |
1732 fun attribute elt name toks = | |
1733 case toks of | |
1734 T.EQUAL :: T.TEXT value :: xs => | |
1735 namedElement { | |
1736 name = #name elt, | |
1737 children = ATTRIBUTE { name = name, value = value } :: | |
1738 #children elt | |
1739 } xs | |
1740 | T.EQUAL :: xs => error xs "Expected attribute value" | |
1741 | toks => error toks "Expected attribute assignment" | |
1742 | |
1743 and content elt toks = | |
1744 case toks of | |
1745 T.ANGLE_SLASH_L :: T.NAME n :: T.ANGLE_R :: xs => | |
1746 if n = #name elt | |
1747 then OK (elt, xs) | |
1748 else ERROR ("Closing tag </" ^ n ^ "> " ^ | |
1749 "does not match opening <" ^ #name elt ^ ">") | |
1750 | T.TEXT text :: xs => | |
1751 content { | |
1752 name = #name elt, | |
1753 children = TEXT text :: #children elt | |
1754 } xs | |
1755 | T.CDATA text :: xs => | |
1756 content { | |
1757 name = #name elt, | |
1758 children = CDATA text :: #children elt | |
1759 } xs | |
1760 | T.COMMENT text :: xs => | |
1761 content { | |
1762 name = #name elt, | |
1763 children = COMMENT text :: #children elt | |
1764 } xs | |
1765 | T.ANGLE_L :: xs => | |
1766 (case element xs of | |
1767 ERROR e => ERROR e | |
1768 | OK (child, xs) => | |
1769 content { | |
1770 name = #name elt, | |
1771 children = ELEMENT child :: #children elt | |
1772 } xs) | |
1773 | tok :: xs => | |
1774 error xs ("Unexpected token " ^ T.name tok) | |
1775 | [] => | |
1776 ERROR ("Document ends within element \"" ^ #name elt ^ "\"") | |
1777 | |
1778 and namedElement elt toks = | |
1779 case toks of | |
1780 T.SLASH_ANGLE_R :: xs => OK (elt, xs) | |
1781 | T.NAME name :: xs => attribute elt name xs | |
1782 | T.ANGLE_R :: xs => content elt xs | |
1783 | x :: xs => error xs ("Unexpected token " ^ T.name x) | |
1784 | [] => ERROR "Document ends within opening tag" | |
1785 | |
1786 and element toks = | |
1787 case toks of | |
1788 T.NAME name :: xs => | |
1789 (case namedElement { name = name, children = [] } xs of | |
1790 ERROR e => ERROR e | |
1791 | OK ({ name, children }, xs) => | |
1792 OK ({ name = name, children = rev children }, xs)) | |
1793 | toks => error toks "Expected element name" | |
1794 | |
1795 and document [] = ERROR "Empty document" | |
1796 | document (tok :: xs) = | |
1797 case tok of | |
1798 T.TEXT _ => document xs | |
1799 | T.COMMENT _ => document xs | |
1800 | T.ANGLE_L => | |
1801 (case element xs of | |
1802 ERROR e => ERROR e | |
1803 | OK (elt, []) => OK (DOCUMENT elt) | |
1804 | OK (elt, (T.TEXT _ :: xs)) => OK (DOCUMENT elt) | |
1805 | OK (elt, xs) => error xs "Extra data after document") | |
1806 | _ => error xs ("Unexpected token " ^ T.name tok) | |
1807 | |
1808 fun parse str = | |
1809 case Lex.lex str of | |
1810 ERROR e => ERROR e | |
1811 | OK tokens => document tokens | |
1812 end | |
1813 | |
1814 structure Serialise :> sig | |
1815 val serialise : document -> string | |
1816 end = struct | |
1817 | |
1818 fun attributes nodes = | |
1819 String.concatWith | |
1820 " " | |
1821 (map node (List.filter | |
1822 (fn ATTRIBUTE _ => true | _ => false) | |
1823 nodes)) | |
1824 | |
1825 and nonAttributes nodes = | |
1826 String.concat | |
1827 (map node (List.filter | |
1828 (fn ATTRIBUTE _ => false | _ => true) | |
1829 nodes)) | |
1830 | |
1831 and node n = | |
1832 case n of | |
1833 TEXT string => | |
1834 string | |
1835 | CDATA string => | |
1836 "<![CDATA[" ^ string ^ "]]>" | |
1837 | COMMENT string => | |
1838 "<!-- " ^ string ^ "-->" | |
1839 | ATTRIBUTE { name, value } => | |
1840 name ^ "=" ^ "\"" ^ value ^ "\"" (*!!!*) | |
1841 | ELEMENT { name, children } => | |
1842 "<" ^ name ^ | |
1843 (case (attributes children) of | |
1844 "" => "" | |
1845 | s => " " ^ s) ^ | |
1846 (case (nonAttributes children) of | |
1847 "" => "/>" | |
1848 | s => ">" ^ s ^ "</" ^ name ^ ">") | |
1849 | |
1850 fun serialise (DOCUMENT { name, children }) = | |
1851 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" ^ | |
1852 node (ELEMENT { name = name, children = children }) | |
1853 end | |
1854 | |
1855 val parse = Parse.parse | |
1856 val serialise = Serialise.serialise | |
1857 | |
1858 end | |
1859 | |
1860 | |
1861 structure SvnControl :> VCS_CONTROL = struct | |
1862 | |
1863 val svn_program = "svn" | |
1864 | |
1865 fun svn_command context libname args = | |
1866 FileBits.command context libname (svn_program :: args) | |
1867 | |
1868 fun svn_command_output context libname args = | |
1869 FileBits.command_output context libname (svn_program :: args) | |
1870 | |
1871 fun svn_command_lines context libname args = | |
1872 case svn_command_output context libname args of | |
1873 ERROR e => ERROR e | |
1874 | OK s => OK (String.tokens (fn c => c = #"\n" orelse c = #"\r") s) | |
1875 | |
1876 fun split_line_pair line = | |
1877 let fun strip_leading_ws str = case explode str of | |
1878 #" "::rest => implode rest | |
1879 | _ => str | |
1880 in | |
1881 case String.tokens (fn c => c = #":") line of | |
1882 [] => ("", "") | |
1883 | first::rest => | |
1884 (first, strip_leading_ws (String.concatWith ":" rest)) | |
1885 end | |
1886 | |
1887 fun is_working context = | |
1888 case svn_command_output context "" ["--version"] of | |
1889 OK "" => OK false | |
1890 | OK _ => OK true | |
1891 | ERROR e => ERROR e | |
1892 | |
1893 structure X = SubXml | |
1894 | |
1895 fun svn_info context libname route = | |
1896 (* SVN 1.9 has info --show-item which is just what we need, | |
1897 but at this point we still have 1.8 on the CI boxes so we | |
1898 might as well aim to support it. For that we really have to | |
1899 use the XML output format, since the default info output is | |
1900 localised. This is the only thing our mini-XML parser is | |
1901 used for though, so it would be good to trim it at some | |
1902 point *) | |
1903 let fun find elt [] = OK elt | |
1904 | find { children, ... } (first :: rest) = | |
1905 case List.find (fn (X.ELEMENT { name, ... }) => name = first | |
1906 | _ => false) | |
1907 children of | |
1908 NONE => ERROR ("No element \"" ^ first ^ "\" in SVN XML") | |
1909 | SOME (X.ELEMENT e) => find e rest | |
1910 | SOME _ => ERROR "Internal error" | |
1911 in | |
1912 case svn_command_output context libname ["info", "--xml"] of | |
1913 ERROR e => ERROR e | |
1914 | OK xml => | |
1915 case X.parse xml of | |
1916 X.ERROR e => ERROR e | |
1917 | X.OK (X.DOCUMENT doc) => find doc route | |
1918 end | |
1919 | |
1920 fun exists context libname = | |
1921 OK (OS.FileSys.isDir (FileBits.subpath context libname ".svn")) | |
1922 handle _ => OK false | |
1923 | |
1924 fun remote_for context (libname, source) = | |
1925 Provider.remote_url context SVN source libname | |
1926 | |
1927 (* Remote the checkout came from, not necessarily the one we want *) | |
1928 fun actual_remote_for context libname = | |
1929 case svn_info context libname ["entry", "url"] of | |
1930 ERROR e => ERROR e | |
1931 | OK { children, ... } => | |
1932 case List.find (fn (X.TEXT _) => true | _ => false) children of | |
1933 NONE => ERROR "No content for URL in SVN info XML" | |
1934 | SOME (X.TEXT url) => OK url | |
1935 | SOME _ => ERROR "Internal error" | |
1936 | |
1937 fun id_of context libname = | |
1938 case svn_info context libname ["entry"] of | |
1939 ERROR e => ERROR e | |
1940 | OK { children, ... } => | |
1941 case List.find | |
1942 (fn (X.ATTRIBUTE { name = "revision", ... }) => true | |
1943 | _ => false) | |
1944 children of | |
1945 NONE => ERROR "No revision for entry in SVN info XML" | |
1946 | SOME (X.ATTRIBUTE { value, ... }) => OK value | |
1947 | SOME _ => ERROR "Internal error" | |
1948 | |
1949 fun is_at context (libname, id_or_tag) = | |
1950 case id_of context libname of | |
1951 ERROR e => ERROR e | |
1952 | OK id => OK (id = id_or_tag) | |
1953 | |
1954 fun is_on_branch context (libname, b) = | |
1955 OK (b = DEFAULT_BRANCH) | |
1956 | |
1957 fun check_remote context (libname, source) = | |
1958 case (remote_for context (libname, source), | |
1959 actual_remote_for context libname) of | |
1960 (_, ERROR e) => ERROR e | |
1961 | (url, OK actual) => | |
1962 if actual = url | |
1963 then OK () | |
1964 else svn_command context libname ["relocate", url] | |
1965 | |
1966 fun is_newest context (libname, source, branch) = | |
1967 case check_remote context (libname, source) of | |
1968 ERROR e => ERROR e | |
1969 | OK () => | |
1970 case svn_command_lines context libname | |
1971 ["status", "--show-updates"] of | |
1972 ERROR e => ERROR e | |
1973 | OK lines => | |
1974 case rev lines of | |
1975 [] => ERROR "No result returned for server status" | |
1976 | last_line::_ => | |
1977 case rev (String.tokens (fn c => c = #" ") last_line) of | |
1978 [] => ERROR "No revision field found in server status" | |
1979 | server_id::_ => is_at context (libname, server_id) | |
1980 | |
1981 fun is_newest_locally context (libname, branch) = | |
1982 OK true (* no local history *) | |
1983 | |
1984 fun is_modified_locally context libname = | |
1985 case svn_command_output context libname ["status"] of | |
1986 ERROR e => ERROR e | |
1987 | OK "" => OK false | |
1988 | OK _ => OK true | |
1989 | |
1990 fun checkout context (libname, source, branch) = | |
1991 let val url = remote_for context (libname, source) | |
1992 val path = FileBits.libpath context libname | |
1993 in | |
1994 if FileBits.nonempty_dir_exists path | |
1995 then (* Surprisingly, SVN itself has no problem with | |
1996 this. But for consistency with other VCSes we | |
1997 don't allow it *) | |
1998 ERROR ("Refusing checkout to nonempty dir \"" ^ path ^ "\"") | |
1999 else | |
2000 (* make the lib dir rather than just the ext dir, since | |
2001 the lib dir might be nested and svn will happily check | |
2002 out into an existing empty dir anyway *) | |
2003 case FileBits.mkpath (FileBits.libpath context libname) of | |
2004 ERROR e => ERROR e | |
2005 | _ => svn_command context "" ["checkout", url, libname] | |
2006 end | |
2007 | |
2008 fun update context (libname, source, branch) = | |
2009 case check_remote context (libname, source) of | |
2010 ERROR e => ERROR e | |
2011 | OK () => | |
2012 case svn_command context libname | |
2013 ["update", "--accept", "postpone"] of | |
2014 ERROR e => ERROR e | |
2015 | _ => OK () | |
2016 | |
2017 fun update_to context (libname, _, "") = | |
2018 ERROR "Non-empty id (tag or revision id) required for update_to" | |
2019 | update_to context (libname, source, id) = | |
2020 case check_remote context (libname, source) of | |
2021 ERROR e => ERROR e | |
2022 | OK () => | |
2023 case svn_command context libname | |
2024 ["update", "-r", id, "--accept", "postpone"] of | |
2025 ERROR e => ERROR e | |
2026 | OK _ => OK () | |
2027 | |
2028 fun copy_url_for context libname = | |
2029 actual_remote_for context libname | |
2030 | |
2031 end | |
2032 | |
2033 structure AnyLibControl :> LIB_CONTROL = struct | |
2034 | |
2035 structure H = LibControlFn(HgControl) | |
2036 structure G = LibControlFn(GitControl) | |
2037 structure S = LibControlFn(SvnControl) | |
2038 | |
2039 fun review context (spec as { vcs, ... } : libspec) = | |
2040 (fn HG => H.review | GIT => G.review | SVN => S.review) vcs context spec | |
2041 | |
2042 fun status context (spec as { vcs, ... } : libspec) = | |
2043 (fn HG => H.status | GIT => G.status | SVN => S.status) vcs context spec | |
2044 | |
2045 fun update context (spec as { vcs, ... } : libspec) = | |
2046 (fn HG => H.update | GIT => G.update | SVN => S.update) vcs context spec | |
2047 | |
2048 fun id_of context (spec as { vcs, ... } : libspec) = | |
2049 (fn HG => H.id_of | GIT => G.id_of | SVN => S.id_of) vcs context spec | |
2050 | |
2051 fun is_working context vcs = | |
2052 (fn HG => H.is_working | GIT => G.is_working | SVN => S.is_working) | |
2053 vcs context vcs | |
2054 | |
2055 end | |
2056 | |
2057 | |
2058 type exclusions = string list | |
2059 | |
2060 structure Archive :> sig | |
2061 | |
2062 val archive : string * exclusions -> project -> OS.Process.status | |
2063 | |
2064 end = struct | |
2065 | |
2066 (* The idea of "archive" is to replace hg/git archive, which won't | |
2067 include files, like the Repoint-introduced external libraries, | |
2068 that are not under version control with the main repo. | |
2069 | |
2070 The process goes like this: | |
2071 | |
2072 - Make sure we have a target filename from the user, and take | |
2073 its basename as our archive directory name | |
2074 | |
2075 - Make an "archive root" subdir of the project repo, named | |
2076 typically .repoint-archive | |
2077 | |
2078 - Identify the VCS used for the project repo. Note that any | |
2079 explicit references to VCS type in this structure are to | |
2080 the VCS used for the project (something Repoint doesn't | |
2081 otherwise care about), not for an individual library | |
2082 | |
2083 - Synthesise a Repoint project with the archive root as its | |
2084 root path, "." as its extdir, with one library whose | |
2085 name is the user-supplied basename and whose explicit | |
2086 source URL is the original project root; update that | |
2087 project -- thus cloning the original project to a subdir | |
2088 of the archive root | |
2089 | |
2090 - Synthesise a Repoint project identical to the original one for | |
2091 this project, but with the newly-cloned copy as its root | |
2092 path; update that project -- thus checking out clean copies | |
2093 of the external library dirs | |
2094 | |
2095 - Call out to an archive program to archive up the new copy, | |
2096 running e.g. | |
2097 tar cvzf project-release.tar.gz \ | |
2098 --exclude=.hg --exclude=.git project-release | |
2099 in the archive root dir | |
2100 | |
2101 - (We also omit the repoint-project.json file and any trace of | |
2102 Repoint. It can't properly be run in a directory where the | |
2103 external project folders already exist but their repo history | |
2104 does not. End users shouldn't get to see Repoint) | |
2105 | |
2106 - Clean up by deleting the new copy | |
2107 *) | |
2108 | |
2109 fun project_vcs_id_and_url dir = | |
2110 let val context = { | |
2111 rootpath = dir, | |
2112 extdir = ".", | |
2113 providers = [], | |
2114 accounts = [] | |
2115 } | |
2116 val vcs_maybe = | |
2117 case [HgControl.exists context ".", | |
2118 GitControl.exists context ".", | |
2119 SvnControl.exists context "."] of | |
2120 [OK true, OK false, OK false] => OK HG | |
2121 | [OK false, OK true, OK false] => OK GIT | |
2122 | [OK false, OK false, OK true] => OK SVN | |
2123 | _ => ERROR ("Unable to identify VCS for directory " ^ dir) | |
2124 in | |
2125 case vcs_maybe of | |
2126 ERROR e => ERROR e | |
2127 | OK vcs => | |
2128 case (fn HG => HgControl.id_of | |
2129 | GIT => GitControl.id_of | |
2130 | SVN => SvnControl.id_of) | |
2131 vcs context "." of | |
2132 ERROR e => ERROR ("Unable to find id of project repo: " ^ e) | |
2133 | OK id => | |
2134 case (fn HG => HgControl.copy_url_for | |
2135 | GIT => GitControl.copy_url_for | |
2136 | SVN => SvnControl.copy_url_for) | |
2137 vcs context "." of | |
2138 ERROR e => ERROR ("Unable to find URL of project repo: " | |
2139 ^ e) | |
2140 | OK url => OK (vcs, id, url) | |
2141 end | |
2142 | |
2143 fun make_archive_root (context : context) = | |
2144 let val path = OS.Path.joinDirFile { | |
2145 dir = #rootpath context, | |
2146 file = RepointFilenames.archive_dir | |
2147 } | |
2148 in | |
2149 case FileBits.mkpath path of | |
2150 ERROR e => raise Fail ("Failed to create archive directory \"" | |
2151 ^ path ^ "\": " ^ e) | |
2152 | OK () => path | |
2153 end | |
2154 | |
2155 fun archive_path archive_dir target_name = | |
2156 OS.Path.joinDirFile { | |
2157 dir = archive_dir, | |
2158 file = target_name | |
2159 } | |
2160 | |
2161 fun check_nonexistent path = | |
2162 case SOME (OS.FileSys.fileSize path) handle OS.SysErr _ => NONE of | |
2163 NONE => () | |
2164 | _ => raise Fail ("Path " ^ path ^ " exists, not overwriting") | |
2165 | |
2166 fun make_archive_copy target_name (vcs, project_id, source_url) | |
2167 ({ context, ... } : project) = | |
2168 let val archive_root = make_archive_root context | |
2169 val synthetic_context = { | |
2170 rootpath = archive_root, | |
2171 extdir = ".", | |
2172 providers = [], | |
2173 accounts = [] | |
2174 } | |
2175 val synthetic_library = { | |
2176 libname = target_name, | |
2177 vcs = vcs, | |
2178 source = URL_SOURCE source_url, | |
2179 branch = DEFAULT_BRANCH, (* overridden by pinned id below *) | |
2180 project_pin = PINNED project_id, | |
2181 lock_pin = PINNED project_id | |
2182 } | |
2183 val path = archive_path archive_root target_name | |
2184 val _ = print ("Cloning original project to " ^ path | |
2185 ^ " at revision " ^ project_id ^ "...\n"); | |
2186 val _ = check_nonexistent path | |
2187 in | |
2188 case AnyLibControl.update synthetic_context synthetic_library of | |
2189 ERROR e => ERROR ("Failed to clone original project to " | |
2190 ^ path ^ ": " ^ e) | |
2191 | OK _ => OK archive_root | |
2192 end | |
2193 | |
2194 fun update_archive archive_root target_name | |
2195 (project as { context, ... } : project) = | |
2196 let val synthetic_context = { | |
2197 rootpath = archive_path archive_root target_name, | |
2198 extdir = #extdir context, | |
2199 providers = #providers context, | |
2200 accounts = #accounts context | |
2201 } | |
2202 in | |
2203 foldl (fn (lib, acc) => | |
2204 case acc of | |
2205 ERROR e => ERROR e | |
2206 | OK () => AnyLibControl.update synthetic_context lib) | |
2207 (OK ()) | |
2208 (#libs project) | |
2209 end | |
2210 | |
2211 datatype packer = TAR | |
2212 | TAR_GZ | |
2213 | TAR_BZ2 | |
2214 | TAR_XZ | |
2215 (* could add other packers, e.g. zip, if we knew how to | |
2216 handle the file omissions etc properly in pack_archive *) | |
2217 | |
2218 fun packer_and_basename path = | |
2219 let val extensions = [ (".tar", TAR), | |
2220 (".tar.gz", TAR_GZ), | |
2221 (".tar.bz2", TAR_BZ2), | |
2222 (".tar.xz", TAR_XZ)] | |
2223 val filename = OS.Path.file path | |
2224 in | |
2225 foldl (fn ((ext, packer), acc) => | |
2226 if String.isSuffix ext filename | |
2227 then SOME (packer, | |
2228 String.substring (filename, 0, | |
2229 String.size filename - | |
2230 String.size ext)) | |
2231 else acc) | |
2232 NONE | |
2233 extensions | |
2234 end | |
2235 | |
2236 fun pack_archive archive_root target_name target_path packer exclusions = | |
2237 case FileBits.command { | |
2238 rootpath = archive_root, | |
2239 extdir = ".", | |
2240 providers = [], | |
2241 accounts = [] | |
2242 } "" ([ | |
2243 "tar", | |
2244 case packer of | |
2245 TAR => "cf" | |
2246 | TAR_GZ => "czf" | |
2247 | TAR_BZ2 => "cjf" | |
2248 | TAR_XZ => "cJf", | |
2249 target_path, | |
2250 "--exclude=.hg", | |
2251 "--exclude=.git", | |
2252 "--exclude=.svn", | |
2253 "--exclude=repoint", | |
2254 "--exclude=repoint.sml", | |
2255 "--exclude=repoint.ps1", | |
2256 "--exclude=repoint.bat", | |
2257 "--exclude=repoint-project.json", | |
2258 "--exclude=repoint-lock.json" | |
2259 ] @ (map (fn e => "--exclude=" ^ e) exclusions) @ | |
2260 [ target_name ]) | |
2261 of | |
2262 ERROR e => ERROR e | |
2263 | OK _ => FileBits.rmpath (archive_path archive_root target_name) | |
2264 | |
2265 fun archive (target_path, exclusions) (project : project) = | |
2266 let val _ = check_nonexistent target_path | |
2267 val (packer, name) = | |
2268 case packer_and_basename target_path of | |
2269 NONE => raise Fail ("Unsupported archive file extension in " | |
2270 ^ target_path) | |
2271 | SOME pn => pn | |
2272 val details = | |
2273 case project_vcs_id_and_url (#rootpath (#context project)) of | |
2274 ERROR e => raise Fail e | |
2275 | OK details => details | |
2276 val archive_root = | |
2277 case make_archive_copy name details project of | |
2278 ERROR e => raise Fail e | |
2279 | OK archive_root => archive_root | |
2280 val outcome = | |
2281 case update_archive archive_root name project of | |
2282 ERROR e => ERROR e | |
2283 | OK _ => | |
2284 case pack_archive archive_root name | |
2285 target_path packer exclusions of | |
2286 ERROR e => ERROR e | |
2287 | OK _ => OK () | |
2288 in | |
2289 case outcome of | |
2290 ERROR e => raise Fail e | |
2291 | OK () => OS.Process.success | |
2292 end | |
2293 | |
2294 end | |
2295 | |
2296 val libobjname = "libraries" | |
2297 | |
2298 fun load_libspec spec_json lock_json libname : libspec = | |
2299 let open JsonBits | |
2300 val libobj = lookup_mandatory spec_json [libobjname, libname] | |
2301 val vcs = lookup_mandatory_string libobj ["vcs"] | |
2302 val retrieve = lookup_optional_string libobj | |
2303 val service = retrieve ["service"] | |
2304 val owner = retrieve ["owner"] | |
2305 val repo = retrieve ["repository"] | |
2306 val url = retrieve ["url"] | |
2307 val branch = retrieve ["branch"] | |
2308 val project_pin = case retrieve ["pin"] of | |
2309 NONE => UNPINNED | |
2310 | SOME p => PINNED p | |
2311 val lock_pin = case lookup_optional lock_json [libobjname, libname] of | |
2312 NONE => UNPINNED | |
2313 | SOME ll => case lookup_optional_string ll ["pin"] of | |
2314 SOME p => PINNED p | |
2315 | NONE => UNPINNED | |
2316 in | |
2317 { | |
2318 libname = libname, | |
2319 vcs = case vcs of | |
2320 "hg" => HG | |
2321 | "git" => GIT | |
2322 | "svn" => SVN | |
2323 | other => raise Fail ("Unknown version-control system \"" ^ | |
2324 other ^ "\""), | |
2325 source = case (url, service, owner, repo) of | |
2326 (SOME u, NONE, _, _) => URL_SOURCE u | |
2327 | (NONE, SOME ss, owner, repo) => | |
2328 SERVICE_SOURCE { service = ss, owner = owner, repo = repo } | |
2329 | _ => raise Fail ("Must have exactly one of service " ^ | |
2330 "or url string"), | |
2331 project_pin = project_pin, | |
2332 lock_pin = lock_pin, | |
2333 branch = case branch of | |
2334 NONE => DEFAULT_BRANCH | |
2335 | SOME b => | |
2336 case vcs of | |
2337 "svn" => raise Fail ("Branches not supported for " ^ | |
2338 "svn repositories; change " ^ | |
2339 "URL instead") | |
2340 | _ => BRANCH b | |
2341 } | |
2342 end | |
2343 | |
2344 fun load_userconfig () : userconfig = | |
2345 let val home = FileBits.homedir () | |
2346 val conf_json = | |
2347 JsonBits.load_json_from | |
2348 (OS.Path.joinDirFile { | |
2349 dir = home, | |
2350 file = RepointFilenames.user_config_file }) | |
2351 handle IO.Io _ => Json.OBJECT [] | |
2352 in | |
2353 { | |
2354 accounts = case JsonBits.lookup_optional conf_json ["accounts"] of | |
2355 NONE => [] | |
2356 | SOME (Json.OBJECT aa) => | |
2357 map (fn (k, (Json.STRING v)) => | |
2358 { service = k, login = v } | |
2359 | _ => raise Fail | |
2360 "String expected for account name") | |
2361 aa | |
2362 | _ => raise Fail "Array expected for accounts", | |
2363 providers = Provider.load_providers conf_json | |
2364 } | |
2365 end | |
2366 | |
2367 datatype pintype = | |
2368 NO_LOCKFILE | | |
2369 USE_LOCKFILE | |
2370 | |
2371 fun load_project (userconfig : userconfig) rootpath pintype : project = | |
2372 let val spec_file = FileBits.project_spec_path rootpath | |
2373 val lock_file = FileBits.project_lock_path rootpath | |
2374 val _ = if OS.FileSys.access (spec_file, [OS.FileSys.A_READ]) | |
2375 handle OS.SysErr _ => false | |
2376 then () | |
2377 else raise Fail ("Failed to open project spec file " ^ | |
2378 (RepointFilenames.project_file) ^ " in " ^ | |
2379 rootpath ^ | |
2380 ".\nPlease ensure the spec file is in the " ^ | |
2381 "project root and run this from there.") | |
2382 val spec_json = JsonBits.load_json_from spec_file | |
2383 val lock_json = if pintype = USE_LOCKFILE | |
2384 then JsonBits.load_json_from lock_file | |
2385 handle IO.Io _ => Json.OBJECT [] | |
2386 else Json.OBJECT [] | |
2387 val extdir = JsonBits.lookup_mandatory_string spec_json | |
2388 ["config", "extdir"] | |
2389 val spec_libs = JsonBits.lookup_optional spec_json [libobjname] | |
2390 val lock_libs = JsonBits.lookup_optional lock_json [libobjname] | |
2391 val providers = Provider.load_more_providers | |
2392 (#providers userconfig) spec_json | |
2393 val libnames = case spec_libs of | |
2394 NONE => [] | |
2395 | SOME (Json.OBJECT ll) => map (fn (k, v) => k) ll | |
2396 | _ => raise Fail "Object expected for libs" | |
2397 in | |
2398 { | |
2399 context = { | |
2400 rootpath = rootpath, | |
2401 extdir = extdir, | |
2402 providers = providers, | |
2403 accounts = #accounts userconfig | |
2404 }, | |
2405 libs = map (load_libspec spec_json lock_json) libnames | |
2406 } | |
2407 end | |
2408 | |
2409 fun save_lock_file rootpath locks = | |
2410 let val lock_file = FileBits.project_lock_path rootpath | |
2411 open Json | |
2412 val lock_json = | |
2413 OBJECT [ | |
2414 (libobjname, | |
2415 OBJECT (map (fn { libname, id_or_tag } => | |
2416 (libname, | |
2417 OBJECT [ ("pin", STRING id_or_tag) ])) | |
2418 locks)) | |
2419 ] | |
2420 in | |
2421 JsonBits.save_json_to lock_file lock_json | |
2422 end | |
2423 | |
2424 fun pad_to n str = | |
2425 if n <= String.size str then str | |
2426 else pad_to n (str ^ " ") | |
2427 | |
2428 fun hline_to 0 = "" | |
2429 | hline_to n = "-" ^ hline_to (n-1) | |
2430 | |
2431 val libname_width = 28 | |
2432 val libstate_width = 11 | |
2433 val localstate_width = 17 | |
2434 val notes_width = 5 | |
2435 val divider = " | " | |
2436 val clear_line = "\r" ^ pad_to 80 ""; | |
2437 | |
2438 fun print_status_header () = | |
2439 print (clear_line ^ "\n " ^ | |
2440 pad_to libname_width "Library" ^ divider ^ | |
2441 pad_to libstate_width "State" ^ divider ^ | |
2442 pad_to localstate_width "Local" ^ divider ^ | |
2443 "Notes" ^ "\n " ^ | |
2444 hline_to libname_width ^ "-+-" ^ | |
2445 hline_to libstate_width ^ "-+-" ^ | |
2446 hline_to localstate_width ^ "-+-" ^ | |
2447 hline_to notes_width ^ "\n") | |
2448 | |
2449 fun print_outcome_header () = | |
2450 print (clear_line ^ "\n " ^ | |
2451 pad_to libname_width "Library" ^ divider ^ | |
2452 pad_to libstate_width "Outcome" ^ divider ^ | |
2453 "Notes" ^ "\n " ^ | |
2454 hline_to libname_width ^ "-+-" ^ | |
2455 hline_to libstate_width ^ "-+-" ^ | |
2456 hline_to notes_width ^ "\n") | |
2457 | |
2458 fun print_status with_network (lib : libspec, status) = | |
2459 let val libstate_str = | |
2460 case status of | |
2461 OK (ABSENT, _) => "Absent" | |
2462 | OK (CORRECT, _) => if with_network then "Correct" else "Present" | |
2463 | OK (SUPERSEDED, _) => "Superseded" | |
2464 | OK (WRONG, _) => "Wrong" | |
2465 | ERROR _ => "Error" | |
2466 val localstate_str = | |
2467 case status of | |
2468 OK (_, MODIFIED) => "Modified" | |
2469 | OK (_, LOCK_MISMATCHED) => "Differs from Lock" | |
2470 | OK (_, CLEAN) => "Clean" | |
2471 | ERROR _ => "" | |
2472 val error_str = | |
2473 case status of | |
2474 ERROR e => e | |
2475 | _ => "" | |
2476 in | |
2477 print (" " ^ | |
2478 pad_to libname_width (#libname lib) ^ divider ^ | |
2479 pad_to libstate_width libstate_str ^ divider ^ | |
2480 pad_to localstate_width localstate_str ^ divider ^ | |
2481 error_str ^ "\n") | |
2482 end | |
2483 | |
2484 fun print_update_outcome (lib : libspec, outcome) = | |
2485 let val outcome_str = | |
2486 case outcome of | |
2487 OK id => "Ok" | |
2488 | ERROR e => "Failed" | |
2489 val error_str = | |
2490 case outcome of | |
2491 ERROR e => e | |
2492 | _ => "" | |
2493 in | |
2494 print (" " ^ | |
2495 pad_to libname_width (#libname lib) ^ divider ^ | |
2496 pad_to libstate_width outcome_str ^ divider ^ | |
2497 error_str ^ "\n") | |
2498 end | |
2499 | |
2500 fun vcs_name HG = ("Mercurial", "hg") | |
2501 | vcs_name GIT = ("Git", "git") | |
2502 | vcs_name SVN = ("Subversion", "svn") | |
2503 | |
2504 fun print_problem_summary context lines = | |
2505 let val failed_vcs = | |
2506 foldl (fn (({ vcs, ... } : libspec, ERROR _), acc) => vcs::acc | |
2507 | (_, acc) => acc) [] lines | |
2508 fun report_nonworking vcs error = | |
2509 print ((if error = "" then "" else error ^ "\n\n") ^ | |
2510 "Error: The project uses the " ^ (#1 (vcs_name vcs)) ^ | |
2511 " version control system, but its\n" ^ | |
2512 "executable program (" ^ (#2 (vcs_name vcs)) ^ | |
2513 ") does not appear to be installed in the program path\n\n") | |
2514 fun check_working [] checked = () | |
2515 | check_working (vcs::rest) checked = | |
2516 if List.exists (fn v => vcs = v) checked | |
2517 then check_working rest checked | |
2518 else | |
2519 case AnyLibControl.is_working context vcs of | |
2520 OK true => check_working rest checked | |
2521 | OK false => (report_nonworking vcs ""; | |
2522 check_working rest (vcs::checked)) | |
2523 | ERROR e => (report_nonworking vcs e; | |
2524 check_working rest (vcs::checked)) | |
2525 in | |
2526 print "\nError: Some operations failed\n\n"; | |
2527 check_working failed_vcs [] | |
2528 end | |
2529 | |
2530 fun act_and_print action print_header print_line context (libs : libspec list) = | |
2531 let val lines = map (fn lib => (lib, action lib)) libs | |
2532 val imperfect = List.exists (fn (_, ERROR _) => true | _ => false) lines | |
2533 val _ = print_header () | |
2534 in | |
2535 app print_line lines; | |
2536 if imperfect then print_problem_summary context lines else (); | |
2537 lines | |
2538 end | |
2539 | |
2540 fun return_code_for outcomes = | |
2541 foldl (fn ((_, result), acc) => | |
2542 case result of | |
2543 ERROR _ => OS.Process.failure | |
2544 | _ => acc) | |
2545 OS.Process.success | |
2546 outcomes | |
2547 | |
2548 fun status_of_project ({ context, libs } : project) = | |
2549 return_code_for (act_and_print (AnyLibControl.status context) | |
2550 print_status_header (print_status false) | |
2551 context libs) | |
2552 | |
2553 fun review_project ({ context, libs } : project) = | |
2554 return_code_for (act_and_print (AnyLibControl.review context) | |
2555 print_status_header (print_status true) | |
2556 context libs) | |
2557 | |
2558 fun lock_project ({ context, libs } : project) = | |
2559 let val _ = if FileBits.verbose () | |
2560 then print ("Scanning IDs for lock file...\n") | |
2561 else () | |
2562 val outcomes = map (fn lib => (lib, AnyLibControl.id_of context lib)) | |
2563 libs | |
2564 val locks = | |
2565 List.concat | |
2566 (map (fn (lib : libspec, result) => | |
2567 case result of | |
2568 ERROR _ => [] | |
2569 | OK id => [{ libname = #libname lib, | |
2570 id_or_tag = id }]) | |
2571 outcomes) | |
2572 val return_code = return_code_for outcomes | |
2573 val _ = print clear_line | |
2574 in | |
2575 if OS.Process.isSuccess return_code | |
2576 then save_lock_file (#rootpath context) locks | |
2577 else (); | |
2578 return_code | |
2579 end | |
2580 | |
2581 fun update_project (project as { context, libs }) = | |
2582 let val outcomes = act_and_print | |
2583 (AnyLibControl.update context) | |
2584 print_outcome_header print_update_outcome | |
2585 context libs | |
2586 val _ = if List.exists (fn (_, OK _) => true | _ => false) outcomes | |
2587 then lock_project project | |
2588 else OS.Process.success | |
2589 in | |
2590 return_code_for outcomes | |
2591 end | |
2592 | |
2593 fun load_local_project pintype = | |
2594 let val userconfig = load_userconfig () | |
2595 val rootpath = OS.FileSys.getDir () | |
2596 in | |
2597 load_project userconfig rootpath pintype | |
2598 end | |
2599 | |
2600 fun with_local_project pintype f = | |
2601 let open OS.Process | |
2602 val return_code = | |
2603 f (load_local_project pintype) | |
2604 handle Fail msg => | |
2605 failure before print ("Error: " ^ msg) | |
2606 | JsonBits.Config msg => | |
2607 failure before print ("Error in configuration: " ^ msg) | |
2608 | e => | |
2609 failure before print ("Error: " ^ exnMessage e) | |
2610 val _ = print "\n"; | |
2611 in | |
2612 return_code | |
2613 end | |
2614 | |
2615 fun review () = with_local_project USE_LOCKFILE review_project | |
2616 fun status () = with_local_project USE_LOCKFILE status_of_project | |
2617 fun update () = with_local_project NO_LOCKFILE update_project | |
2618 fun lock () = with_local_project NO_LOCKFILE lock_project | |
2619 fun install () = with_local_project USE_LOCKFILE update_project | |
2620 | |
2621 fun version () = | |
2622 (print ("v" ^ repoint_version ^ "\n"); | |
2623 OS.Process.success) | |
2624 | |
2625 fun usage () = | |
2626 (print "\nRepoint "; | |
2627 version (); | |
2628 print ("\nA simple manager for third-party source code dependencies.\n\n" | |
2629 ^ "Usage:\n\n" | |
2630 ^ " repoint <command>\n\n" | |
2631 ^ "where <command> is one of:\n\n" | |
2632 ^ " status print quick report on local status only, without using network\n" | |
2633 ^ " review check configured libraries against their providers, and report\n" | |
2634 ^ " install update configured libraries according to project specs and lock file\n" | |
2635 ^ " update update configured libraries and lock file according to project specs\n" | |
2636 ^ " lock update lock file to match local library status\n" | |
2637 ^ " archive pack up project and all libraries into an archive file\n" | |
2638 ^ " (invoke as 'repoint archive target-file.tar.gz')\n" | |
2639 ^ " version print the Repoint version number and exit\n\n"); | |
2640 OS.Process.failure) | |
2641 | |
2642 fun archive target args = | |
2643 case args of | |
2644 [] => | |
2645 with_local_project USE_LOCKFILE (Archive.archive (target, [])) | |
2646 | "--exclude"::xs => | |
2647 with_local_project USE_LOCKFILE (Archive.archive (target, xs)) | |
2648 | _ => usage () | |
2649 | |
2650 fun repoint args = | |
2651 let val return_code = | |
2652 case args of | |
2653 ["review"] => review () | |
2654 | ["status"] => status () | |
2655 | ["install"] => install () | |
2656 | ["update"] => update () | |
2657 | ["lock"] => lock () | |
2658 | ["version"] => version () | |
2659 | "archive"::target::args => archive target args | |
2660 | arg::_ => (print ("Error: unknown argument \"" ^ arg ^ "\"\n"); | |
2661 usage ()) | |
2662 | _ => usage () | |
2663 in | |
2664 OS.Process.exit return_code; | |
2665 () | |
2666 end | |
2667 | |
2668 fun main () = | |
2669 repoint (CommandLine.arguments ()) |