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 ())