To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Tag: | Revision:

root / repoint.sml @ 76:a6c9a0ca493e

History | View | Annotate | Download (103 KB)

1 70:2d3e1d1f99c0 Chris
(*
2
    DO NOT EDIT THIS FILE.
3
    This file is automatically generated from the individual
4 76:a6c9a0ca493e Chris
    source files in the Repoint repository.
5 70:2d3e1d1f99c0 Chris
*)
6
7
(*
8 76:a6c9a0ca493e Chris
    Repoint
9 70:2d3e1d1f99c0 Chris
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 76:a6c9a0ca493e Chris
val repoint_version = "0.9.98"
42 70:2d3e1d1f99c0 Chris
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 76:a6c9a0ca493e Chris
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 70:2d3e1d1f99c0 Chris
end
142
143
signature VCS_CONTROL = sig
144
145 76:a6c9a0ca493e Chris
    (** Check whether the given VCS is installed and working *)
146
    val is_working : context -> bool result
147
148 70:2d3e1d1f99c0 Chris
    (** 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 76:a6c9a0ca493e Chris
    val is_working : context -> vcs -> bool result
201 70:2d3e1d1f99c0 Chris
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 76:a6c9a0ca493e Chris
        case OS.Process.getEnv "REPOINT_VERBOSE" of
223 70:2d3e1d1f99c0 Chris
            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 76:a6c9a0ca493e Chris
        project_file_path rootpath (RepointFilenames.project_file)
271 70:2d3e1d1f99c0 Chris
272
    fun project_lock_path rootpath =
273 76:a6c9a0ca493e Chris
        project_file_path rootpath (RepointFilenames.project_lock_file)
274 70:2d3e1d1f99c0 Chris
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 76:a6c9a0ca493e Chris
        (* We are quite strict about what we accept here, except
305 70:2d3e1d1f99c0 Chris
           for the first element in cmdlist which is assumed to be a
306 76:a6c9a0ca493e Chris
           known command location rather than arbitrary user input. *)
307 70:2d3e1d1f99c0 Chris
        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 76:a6c9a0ca493e Chris
587
    fun is_working context vcs =
588
        V.is_working context
589 70:2d3e1d1f99c0 Chris
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 76:a6c9a0ca493e Chris
    exception Config of string
948 70:2d3e1d1f99c0 Chris
    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 76:a6c9a0ca493e Chris
    exception Config of string
957
958 70:2d3e1d1f99c0 Chris
    fun load_json_from filename =
959
        case Json.parse (FileBits.file_contents filename) of
960
            Json.OK json => json
961 76:a6c9a0ca493e Chris
          | Json.ERROR e => raise Config ("Failed to parse file: " ^ e)
962 70:2d3e1d1f99c0 Chris
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 76:a6c9a0ca493e Chris
                    (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 70:2d3e1d1f99c0 Chris
        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 76:a6c9a0ca493e Chris
          | SOME _ => raise Config ("Value (if present) must be string: " ^
995
                                    (String.concatWith " -> " kk))
996 70:2d3e1d1f99c0 Chris
          | NONE => NONE
997
998
    fun lookup_mandatory json kk =
999
        case lookup_optional json kk of
1000
            SOME v => v
1001 76:a6c9a0ca493e Chris
          | NONE => raise Config ("Value is mandatory: " ^
1002
                                  (String.concatWith " -> " kk))
1003 70:2d3e1d1f99c0 Chris
1004
    fun lookup_mandatory_string json kk =
1005
        case lookup_optional json kk of
1006
            SOME (Json.STRING s) => s
1007 76:a6c9a0ca493e Chris
          | _ => raise Config ("Value must be string: " ^
1008
                               (String.concatWith " -> " kk))
1009 70:2d3e1d1f99c0 Chris
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 76:a6c9a0ca493e Chris
    val hg_program = "hg"
1176
1177 70:2d3e1d1f99c0 Chris
    val hg_args = [ "--config", "ui.interactive=true",
1178
                    "--config", "ui.merge=:merge" ]
1179
1180
    fun hg_command context libname args =
1181 76:a6c9a0ca493e Chris
        FileBits.command context libname (hg_program :: hg_args @ args)
1182 70:2d3e1d1f99c0 Chris
1183
    fun hg_command_output context libname args =
1184 76:a6c9a0ca493e Chris
        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 70:2d3e1d1f99c0 Chris
    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 76:a6c9a0ca493e Chris
       (repoint/master). The remote we use is always named repoint, and we
1330 70:2d3e1d1f99c0 Chris
       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 76:a6c9a0ca493e Chris
    val git_program = "git"
1335
1336 70:2d3e1d1f99c0 Chris
    fun git_command context libname args =
1337 76:a6c9a0ca493e Chris
        FileBits.command context libname (git_program :: args)
1338 70:2d3e1d1f99c0 Chris
1339
    fun git_command_output context libname args =
1340 76:a6c9a0ca493e Chris
        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 70:2d3e1d1f99c0 Chris
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 76:a6c9a0ca493e Chris
    val our_remote = "repoint"
1361 70:2d3e1d1f99c0 Chris
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 76:a6c9a0ca493e Chris
           Repoint, and if that's the case, we can't add it here; we'll
1435 70:2d3e1d1f99c0 Chris
           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 76:a6c9a0ca493e Chris
   https://bitbucket.org/cannam/sml-subxml
1527 70:2d3e1d1f99c0 Chris
   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 76:a6c9a0ca493e Chris
    val svn_program = "svn"
1864
1865 70:2d3e1d1f99c0 Chris
    fun svn_command context libname args =
1866 76:a6c9a0ca493e Chris
        FileBits.command context libname (svn_program :: args)
1867 70:2d3e1d1f99c0 Chris
1868
    fun svn_command_output context libname args =
1869 76:a6c9a0ca493e Chris
        FileBits.command_output context libname (svn_program :: args)
1870 70:2d3e1d1f99c0 Chris
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 76:a6c9a0ca493e Chris
    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 70:2d3e1d1f99c0 Chris
    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 76:a6c9a0ca493e Chris
    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 70:2d3e1d1f99c0 Chris
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 76:a6c9a0ca493e Chris
       include files, like the Repoint-introduced external libraries,
2068 70:2d3e1d1f99c0 Chris
       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 76:a6c9a0ca493e Chris
         typically .repoint-archive
2077 70:2d3e1d1f99c0 Chris
2078
       - Identify the VCS used for the project repo. Note that any
2079
         explicit references to VCS type in this structure are to
2080 76:a6c9a0ca493e Chris
         the VCS used for the project (something Repoint doesn't
2081 70:2d3e1d1f99c0 Chris
         otherwise care about), not for an individual library
2082
2083 76:a6c9a0ca493e Chris
       - Synthesise a Repoint project with the archive root as its
2084 70:2d3e1d1f99c0 Chris
         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 76:a6c9a0ca493e Chris
       - Synthesise a Repoint project identical to the original one for
2091 70:2d3e1d1f99c0 Chris
         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 76:a6c9a0ca493e Chris
       - (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 70:2d3e1d1f99c0 Chris
         external project folders already exist but their repo history
2104 76:a6c9a0ca493e Chris
         does not. End users shouldn't get to see Repoint)
2105 70:2d3e1d1f99c0 Chris
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 76:a6c9a0ca493e Chris
                    file = RepointFilenames.archive_dir
2147 70:2d3e1d1f99c0 Chris
                }
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 76:a6c9a0ca493e Chris
                     "--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 70:2d3e1d1f99c0 Chris
                 ] @ (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 76:a6c9a0ca493e Chris
                      file = RepointFilenames.user_config_file })
2351 70:2d3e1d1f99c0 Chris
            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 76:a6c9a0ca493e Chris
                                 (RepointFilenames.project_file) ^ " in " ^
2379 70:2d3e1d1f99c0 Chris
                                 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 76:a6c9a0ca493e Chris
fun print_status with_network (lib : libspec, status) =
2459 70:2d3e1d1f99c0 Chris
    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 76:a6c9a0ca493e Chris
               pad_to libname_width (#libname lib) ^ divider ^
2479 70:2d3e1d1f99c0 Chris
               pad_to libstate_width libstate_str ^ divider ^
2480
               pad_to localstate_width localstate_str ^ divider ^
2481
               error_str ^ "\n")
2482
    end
2483
2484 76:a6c9a0ca493e Chris
fun print_update_outcome (lib : libspec, outcome) =
2485 70:2d3e1d1f99c0 Chris
    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 76:a6c9a0ca493e Chris
               pad_to libname_width (#libname lib) ^ divider ^
2496 70:2d3e1d1f99c0 Chris
               pad_to libstate_width outcome_str ^ divider ^
2497
               error_str ^ "\n")
2498
    end
2499
2500 76:a6c9a0ca493e Chris
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 70:2d3e1d1f99c0 Chris
        val _ = print_header ()
2534
    in
2535
        app print_line lines;
2536 76:a6c9a0ca493e Chris
        if imperfect then print_problem_summary context lines else ();
2537 70:2d3e1d1f99c0 Chris
        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 76:a6c9a0ca493e Chris
                                   context libs)
2552 70:2d3e1d1f99c0 Chris
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 76:a6c9a0ca493e Chris
                                   context libs)
2557 70:2d3e1d1f99c0 Chris
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 76:a6c9a0ca493e Chris
        val outcomes = map (fn lib => (lib, AnyLibControl.id_of context lib))
2563 70:2d3e1d1f99c0 Chris
                           libs
2564
        val locks =
2565
            List.concat
2566 76:a6c9a0ca493e Chris
                (map (fn (lib : libspec, result) =>
2567 70:2d3e1d1f99c0 Chris
                         case result of
2568
                             ERROR _ => []
2569 76:a6c9a0ca493e Chris
                           | OK id => [{ libname = #libname lib,
2570
                                         id_or_tag = id }])
2571 70:2d3e1d1f99c0 Chris
                     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 76:a6c9a0ca493e Chris
                           print_outcome_header print_update_outcome
2585
                           context libs
2586 70:2d3e1d1f99c0 Chris
        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 76:a6c9a0ca493e Chris
  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 70:2d3e1d1f99c0 Chris
        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 76:a6c9a0ca493e Chris
    (print ("v" ^ repoint_version ^ "\n");
2623 70:2d3e1d1f99c0 Chris
     OS.Process.success)
2624
2625
fun usage () =
2626 76:a6c9a0ca493e Chris
    (print "\nRepoint ";
2627 70:2d3e1d1f99c0 Chris
     version ();
2628
     print ("\nA simple manager for third-party source code dependencies.\n\n"
2629
            ^ "Usage:\n\n"
2630 76:a6c9a0ca493e Chris
            ^ "  repoint <command>\n\n"
2631 70:2d3e1d1f99c0 Chris
            ^ "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 76:a6c9a0ca493e Chris
            ^ "           (invoke as 'repoint archive target-file.tar.gz')\n"
2639
            ^ "  version  print the Repoint version number and exit\n\n");
2640 70:2d3e1d1f99c0 Chris
    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 76:a6c9a0ca493e Chris
fun repoint args =
2651 70:2d3e1d1f99c0 Chris
    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 76:a6c9a0ca493e Chris
              | arg::_ => (print ("Error: unknown argument \"" ^ arg ^ "\"\n");
2661
                           usage ())
2662 70:2d3e1d1f99c0 Chris
              | _ => usage ()
2663
    in
2664
        OS.Process.exit return_code;
2665
        ()
2666
    end
2667
2668
fun main () =
2669 76:a6c9a0ca493e Chris
    repoint (CommandLine.arguments ())