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 / vext.sml @ 70:2d3e1d1f99c0

History | View | Annotate | Download (100 KB)

1
(*
2
    DO NOT EDIT THIS FILE.
3
    This file is automatically generated from the individual
4
    source files in the Vext repository.
5
*)
6

    
7
(* 
8
    Vext
9

    
10
    A simple manager for third-party source code dependencies
11

    
12
    Copyright 2018 Chris Cannam, Particular Programs Ltd,
13
    and Queen Mary, University of London
14

    
15
    Permission is hereby granted, free of charge, to any person
16
    obtaining a copy of this software and associated documentation
17
    files (the "Software"), to deal in the Software without
18
    restriction, including without limitation the rights to use, copy,
19
    modify, merge, publish, distribute, sublicense, and/or sell copies
20
    of the Software, and to permit persons to whom the Software is
21
    furnished to do so, subject to the following conditions:
22

    
23
    The above copyright notice and this permission notice shall be
24
    included in all copies or substantial portions of the Software.
25

    
26
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
27
    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
28
    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
29
    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
30
    ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
31
    CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
32
    WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
33

    
34
    Except as contained in this notice, the names of Chris Cannam,
35
    Particular Programs Ltd, and Queen Mary, University of London
36
    shall not be used in advertising or otherwise to promote the sale,
37
    use or other dealings in this Software without prior written
38
    authorization.
39
*)
40

    
41
val vext_version = "0.9.95"
42

    
43

    
44
datatype vcs =
45
         HG |
46
         GIT |
47
         SVN
48

    
49
datatype source =
50
         URL_SOURCE of string |
51
         SERVICE_SOURCE of {
52
             service : string,
53
             owner : string option,
54
             repo : string option
55
         }
56

    
57
type id_or_tag = string
58

    
59
datatype pin =
60
         UNPINNED |
61
         PINNED of id_or_tag
62

    
63
datatype libstate =
64
         ABSENT |
65
         CORRECT |
66
         SUPERSEDED |
67
         WRONG
68

    
69
datatype localstate =
70
         MODIFIED |
71
         LOCK_MISMATCHED |
72
         CLEAN
73

    
74
datatype branch =
75
         BRANCH of string |
76
         DEFAULT_BRANCH
77
             
78
(* If we can recover from an error, for example by reporting failure
79
   for this one thing and going on to the next thing, then the error
80
   should usually be returned through a result type rather than an
81
   exception. *)
82
             
83
datatype 'a result =
84
         OK of 'a |
85
         ERROR of string
86

    
87
type libname = string
88

    
89
type libspec = {
90
    libname : libname,
91
    vcs : vcs,
92
    source : source,
93
    branch : branch,
94
    project_pin : pin,
95
    lock_pin : pin
96
}
97

    
98
type lock = {
99
    libname : libname,
100
    id_or_tag : id_or_tag
101
}
102

    
103
type remote_spec = {
104
    anon : string option,
105
    auth : string option
106
}
107

    
108
type provider = {
109
    service : string,
110
    supports : vcs list,
111
    remote_spec : remote_spec
112
}
113

    
114
type account = {
115
    service : string,
116
    login : string
117
}
118
                    
119
type context = {
120
    rootpath : string,
121
    extdir : string,
122
    providers : provider list,
123
    accounts : account list
124
}
125

    
126
type userconfig = {
127
    providers : provider list,
128
    accounts : account list
129
}
130
                   
131
type project = {
132
    context : context,
133
    libs : libspec list
134
}
135

    
136
structure VextFilenames = struct
137
    val project_file = "vext-project.json"
138
    val project_lock_file = "vext-lock.json"
139
    val user_config_file = ".vext.json"
140
    val archive_dir = ".vext-archive"
141
end
142
                   
143
signature VCS_CONTROL = sig
144

    
145
    (** Test whether the library is present locally at all *)
146
    val exists : context -> libname -> bool result
147
                                            
148
    (** Return the id (hash) of the current revision for the library *)
149
    val id_of : context -> libname -> id_or_tag result
150

    
151
    (** Test whether the library is at the given id *)
152
    val is_at : context -> libname * id_or_tag -> bool result
153

    
154
    (** Test whether the library is on the given branch, i.e. is at
155
        the branch tip or an ancestor of it *)
156
    val is_on_branch : context -> libname * branch -> bool result
157

    
158
    (** Test whether the library is at the newest revision for the
159
        given branch. False may indicate that the branch has advanced
160
        or that the library is not on the branch at all. This function
161
        may use the network to check for new revisions *)
162
    val is_newest : context -> libname * source * branch -> bool result
163

    
164
    (** Test whether the library is at the newest revision available
165
        locally for the given branch. False may indicate that the
166
        branch has advanced or that the library is not on the branch
167
        at all. This function must not use the network *)
168
    val is_newest_locally : context -> libname * branch -> bool result
169

    
170
    (** Test whether the library has been modified in the local
171
        working copy *)
172
    val is_modified_locally : context -> libname -> bool result
173

    
174
    (** Check out, i.e. clone a fresh copy of, the repo for the given
175
        library on the given branch *)
176
    val checkout : context -> libname * source * branch -> unit result
177

    
178
    (** Update the library to the given branch tip. Assumes that a
179
        local copy of the library already exists *)
180
    val update : context -> libname * source * branch -> unit result
181

    
182
    (** Update the library to the given specific id or tag *)
183
    val update_to : context -> libname * source * id_or_tag -> unit result
184

    
185
    (** Return a URL from which the library can be cloned, given that
186
        the local copy already exists. For a DVCS this can be the
187
        local copy, but for a centralised VCS it will have to be the
188
        remote repository URL. Used for archiving *)
189
    val copy_url_for : context -> libname -> string result
190
end
191

    
192
signature LIB_CONTROL = sig
193
    val review : context -> libspec -> (libstate * localstate) result
194
    val status : context -> libspec -> (libstate * localstate) result
195
    val update : context -> libspec -> unit result
196
    val id_of : context -> libspec -> id_or_tag result
197
end
198

    
199
structure FileBits :> sig
200
    val extpath : context -> string
201
    val libpath : context -> libname -> string
202
    val subpath : context -> libname -> string -> string
203
    val command_output : context -> libname -> string list -> string result
204
    val command : context -> libname -> string list -> unit result
205
    val file_url : string -> string
206
    val file_contents : string -> string
207
    val mydir : unit -> string
208
    val homedir : unit -> string
209
    val mkpath : string -> unit result
210
    val rmpath : string -> unit result
211
    val nonempty_dir_exists : string -> bool
212
    val project_spec_path : string -> string
213
    val project_lock_path : string -> string
214
    val verbose : unit -> bool
215
end = struct
216

    
217
    fun verbose () =
218
        case OS.Process.getEnv "VEXT_VERBOSE" of
219
            SOME "0" => false
220
          | SOME _ => true
221
          | NONE => false
222

    
223
    fun split_relative path desc =
224
        case OS.Path.fromString path of
225
            { isAbs = true, ... } => raise Fail (desc ^ " may not be absolute")
226
          | { arcs, ... } => arcs
227
                        
228
    fun extpath ({ rootpath, extdir, ... } : context) =
229
        let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
230
        in OS.Path.toString {
231
                isAbs = isAbs,
232
                vol = vol,
233
                arcs = arcs @
234
                       split_relative extdir "extdir"
235
            }
236
        end
237
    
238
    fun subpath ({ rootpath, extdir, ... } : context) libname remainder =
239
        (* NB libname is allowed to be a path fragment, e.g. foo/bar *)
240
        let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
241
        in OS.Path.toString {
242
                isAbs = isAbs,
243
                vol = vol,
244
                arcs = arcs @
245
                       split_relative extdir "extdir" @
246
                       split_relative libname "library path" @
247
                       split_relative remainder "subpath"
248
            }
249
        end
250

    
251
    fun libpath context "" =
252
        extpath context
253
      | libpath context libname =
254
        subpath context libname ""
255

    
256
    fun project_file_path rootpath filename =
257
        let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
258
        in OS.Path.toString {
259
                isAbs = isAbs,
260
                vol = vol,
261
                arcs = arcs @ [ filename ]
262
            }
263
        end
264
                
265
    fun project_spec_path rootpath =
266
        project_file_path rootpath (VextFilenames.project_file)
267

    
268
    fun project_lock_path rootpath =
269
        project_file_path rootpath (VextFilenames.project_lock_file)
270

    
271
    fun trim str =
272
        hd (String.fields (fn x => x = #"\n" orelse x = #"\r") str)
273
            
274
    fun file_url path =
275
        let val forward_path = 
276
                String.translate (fn #"\\" => "/" |
277
                                  c => Char.toString c)
278
                                 (OS.Path.mkCanonical path)
279
        in
280
            (* Path is expected to be absolute already, but if it
281
               starts with a drive letter, we'll need an extra slash *)
282
            case explode forward_path of
283
                #"/"::rest => "file:///" ^ implode rest
284
              | _ => "file:///" ^ forward_path
285
        end
286
        
287
    fun file_contents filename =
288
        let val stream = TextIO.openIn filename
289
            fun read_all str acc =
290
                case TextIO.inputLine str of
291
                    SOME line => read_all str (trim line :: acc)
292
                  | NONE => rev acc
293
            val contents = read_all stream []
294
            val _ = TextIO.closeIn stream
295
        in
296
            String.concatWith "\n" contents
297
        end
298

    
299
    fun expand_commandline cmdlist =
300
        (* We are quite [too] strict about what we accept here, except
301
           for the first element in cmdlist which is assumed to be a
302
           known command location rather than arbitrary user input. NB
303
           only ASCII accepted at this point. *)
304
        let open Char
305
            fun quote arg =
306
                if List.all
307
                       (fn c => isAlphaNum c orelse c = #"-" orelse c = #"_")
308
                       (explode arg)
309
                then arg
310
                else "\"" ^ arg ^ "\""
311
            fun check arg =
312
                let val valid = explode " /#:;?,._-{}@=+"
313
                in
314
                    app (fn c =>
315
                            if isAlphaNum c orelse
316
                               List.exists (fn v => v = c) valid orelse
317
                               c > chr 127
318
                            then ()
319
                            else raise Fail ("Invalid character '" ^
320
                                             (Char.toString c) ^
321
                                             "' in command list"))
322
                        (explode arg);
323
                    arg
324
                end
325
        in
326
            String.concatWith " "
327
                              (map quote
328
                                   (hd cmdlist :: map check (tl cmdlist)))
329
        end
330

    
331
    val tick_cycle = ref 0
332
    val tick_chars = Vector.fromList (map String.str (explode "|/-\\"))
333

    
334
    fun tick libname cmdlist =
335
        let val n = Vector.length tick_chars
336
            fun pad_to n str =
337
                if n <= String.size str then str
338
                else pad_to n (str ^ " ")
339
            val name = if libname <> "" then libname
340
                       else if cmdlist = nil then ""
341
                       else hd (rev cmdlist)
342
        in
343
            print ("  " ^
344
                   Vector.sub(tick_chars, !tick_cycle) ^ " " ^
345
                   pad_to 70 name ^
346
                   "\r");
347
            tick_cycle := (if !tick_cycle = n - 1 then 0 else 1 + !tick_cycle)
348
        end
349
            
350
    fun run_command context libname cmdlist redirect =
351
        let open OS
352
            val dir = libpath context libname
353
            val cmd = expand_commandline cmdlist
354
            val _ = if verbose ()
355
                    then print ("\n=== " ^ dir ^ "\n<<< " ^ cmd ^ "\n")
356
                    else tick libname cmdlist
357
            val _ = FileSys.chDir dir
358
            val status = case redirect of
359
                             NONE => Process.system cmd
360
                           | SOME file => Process.system (cmd ^ ">" ^ file)
361
        in
362
            if Process.isSuccess status
363
            then OK ()
364
            else ERROR ("Command failed: " ^ cmd ^ " (in dir " ^ dir ^ ")")
365
        end
366
        handle ex => ERROR ("Unable to run command: " ^ exnMessage ex)
367

    
368
    fun command context libname cmdlist =
369
        run_command context libname cmdlist NONE
370
            
371
    fun command_output context libname cmdlist =
372
        let open OS
373
            val tmpFile = FileSys.tmpName ()
374
            val result = run_command context libname cmdlist (SOME tmpFile)
375
            val contents = file_contents tmpFile
376
            val _ = if verbose ()
377
                    then print (">>> \"" ^ contents ^ "\"\n")
378
                    else ()
379
        in
380
            FileSys.remove tmpFile handle _ => ();
381
            case result of
382
                OK () => OK contents
383
              | ERROR e => ERROR e
384
        end
385

    
386
    fun mydir () =
387
        let open OS
388
            val { dir, file } = Path.splitDirFile (CommandLine.name ())
389
        in
390
            FileSys.realPath
391
                (if Path.isAbsolute dir
392
                 then dir
393
                 else Path.concat (FileSys.getDir (), dir))
394
        end
395

    
396
    fun homedir () =
397
        (* Failure is not routine, so we use an exception here *)
398
        case (OS.Process.getEnv "HOME",
399
              OS.Process.getEnv "HOMEPATH") of
400
            (SOME home, _) => home
401
          | (NONE, SOME home) => home
402
          | (NONE, NONE) =>
403
            raise Fail "Failed to look up home directory from environment"
404

    
405
    fun mkpath' path =
406
        if OS.FileSys.isDir path handle _ => false
407
        then OK ()
408
        else case OS.Path.fromString path of
409
                 { arcs = nil, ... } => OK ()
410
               | { isAbs = false, ... } => ERROR "mkpath requires absolute path"
411
               | { isAbs, vol, arcs } => 
412
                 case mkpath' (OS.Path.toString {      (* parent *)
413
                                    isAbs = isAbs,
414
                                    vol = vol,
415
                                    arcs = rev (tl (rev arcs)) }) of
416
                     ERROR e => ERROR e
417
                   | OK () => ((OS.FileSys.mkDir path; OK ())
418
                               handle OS.SysErr (e, _) =>
419
                                      ERROR ("Directory creation failed: " ^ e))
420

    
421
    fun mkpath path =
422
        mkpath' (OS.Path.mkCanonical path)
423

    
424
    fun dir_contents dir =
425
        let open OS
426
            fun files_from dirstream =
427
                case FileSys.readDir dirstream of
428
                    NONE => []
429
                  | SOME file =>
430
                    (* readDir is supposed to filter these, 
431
                       but let's be extra cautious: *)
432
                    if file = Path.parentArc orelse file = Path.currentArc
433
                    then files_from dirstream
434
                    else file :: files_from dirstream
435
            val stream = FileSys.openDir dir
436
            val files = map (fn f => Path.joinDirFile
437
                                         { dir = dir, file = f })
438
                            (files_from stream)
439
            val _ = FileSys.closeDir stream
440
        in
441
            files
442
        end
443

    
444
    fun rmpath' path =
445
        let open OS
446
            fun remove path =
447
                if FileSys.isLink path (* dangling links bother isDir *)
448
                then FileSys.remove path
449
                else if FileSys.isDir path
450
                then (app remove (dir_contents path); FileSys.rmDir path)
451
                else FileSys.remove path
452
        in
453
            (remove path; OK ())
454
            handle SysErr (e, _) => ERROR ("Path removal failed: " ^ e)
455
        end
456

    
457
    fun rmpath path =
458
        rmpath' (OS.Path.mkCanonical path)
459

    
460
    fun nonempty_dir_exists path =
461
        let open OS.FileSys
462
        in
463
            (not (isLink path) andalso
464
             isDir path andalso
465
             dir_contents path <> [])
466
            handle _ => false
467
        end                                        
468
                
469
end
470
                                         
471
functor LibControlFn (V: VCS_CONTROL) :> LIB_CONTROL = struct
472

    
473
    (* Valid states for unpinned libraries:
474

    
475
       - CORRECT: We are on the right branch and are up-to-date with
476
         it as far as we can tell. (If not using the network, this
477
         should be reported to user as "Present" rather than "Correct"
478
         as the remote repo may have advanced without us knowing.)
479

    
480
       - SUPERSEDED: We are on the right branch but we can see that
481
         there is a newer revision either locally or on the remote (in
482
         Git terms, we are at an ancestor of the desired branch tip).
483

    
484
       - WRONG: We are on the wrong branch (in Git terms, we are not
485
         at the desired branch tip or any ancestor of it).
486

    
487
       - ABSENT: Repo doesn't exist here at all.
488

    
489
       Valid states for pinned libraries:
490

    
491
       - CORRECT: We are at the pinned revision.
492

    
493
       - WRONG: We are at any revision other than the pinned one.
494

    
495
       - ABSENT: Repo doesn't exist here at all.
496
    *)
497

    
498
    fun check with_network context
499
              ({ libname, source, branch,
500
                 project_pin, lock_pin, ... } : libspec) =
501
        let fun check_unpinned () =
502
                let val newest =
503
                        if with_network
504
                        then V.is_newest context (libname, source, branch)
505
                        else V.is_newest_locally context (libname, branch)
506
                in
507
                    case newest of
508
                         ERROR e => ERROR e
509
                       | OK true => OK CORRECT
510
                       | OK false =>
511
                         case V.is_on_branch context (libname, branch) of
512
                             ERROR e => ERROR e
513
                           | OK true => OK SUPERSEDED
514
                           | OK false => OK WRONG
515
                end
516
            fun check_pinned target =
517
                case V.is_at context (libname, target) of
518
                    ERROR e => ERROR e
519
                  | OK true => OK CORRECT
520
                  | OK false => OK WRONG
521
            fun check_remote () =
522
                case project_pin of
523
                    UNPINNED => check_unpinned ()
524
                  | PINNED target => check_pinned target
525
            fun check_local () =
526
                case V.is_modified_locally context libname of
527
                    ERROR e => ERROR e
528
                  | OK true  => OK MODIFIED
529
                  | OK false => 
530
                    case lock_pin of
531
                        UNPINNED => OK CLEAN
532
                      | PINNED target =>
533
                        case V.is_at context (libname, target) of
534
                            ERROR e => ERROR e
535
                          | OK true => OK CLEAN
536
                          | OK false => OK LOCK_MISMATCHED
537
        in
538
            case V.exists context libname of
539
                ERROR e => ERROR e
540
              | OK false => OK (ABSENT, CLEAN)
541
              | OK true =>
542
                case (check_remote (), check_local ()) of
543
                    (ERROR e, _) => ERROR e
544
                  | (_, ERROR e) => ERROR e
545
                  | (OK r, OK l) => OK (r, l)
546
        end
547

    
548
    val review = check true
549
    val status = check false
550

    
551
    fun update context
552
               ({ libname, source, branch,
553
                  project_pin, lock_pin, ... } : libspec) =
554
        let fun update_unpinned () =
555
                case V.is_newest context (libname, source, branch) of
556
                    ERROR e => ERROR e
557
                  | OK true => OK ()
558
                  | OK false => V.update context (libname, source, branch)
559
            fun update_pinned target =
560
                case V.is_at context (libname, target) of
561
                    ERROR e => ERROR e
562
                  | OK true => OK ()
563
                  | OK false => V.update_to context (libname, source, target)
564
            fun update' () =
565
                case lock_pin of
566
                    PINNED target => update_pinned target
567
                  | UNPINNED =>
568
                    case project_pin of
569
                        PINNED target => update_pinned target
570
                      | UNPINNED => update_unpinned ()
571
        in
572
            case V.exists context libname of
573
                ERROR e => ERROR e
574
              | OK true => update' ()
575
              | OK false =>
576
                case V.checkout context (libname, source, branch) of
577
                    ERROR e => ERROR e
578
                  | OK () => update' ()
579
        end
580

    
581
    fun id_of context ({ libname, ... } : libspec) =
582
        V.id_of context libname
583
                
584
end
585

    
586
(* Simple Standard ML JSON parser
587
   https://bitbucket.org/cannam/sml-simplejson
588
   Copyright 2017 Chris Cannam. BSD licence.
589
   Parts based on the JSON parser in the Ponyo library by Phil Eaton.
590
*)
591

    
592
signature JSON = sig
593

    
594
    datatype json = OBJECT of (string * json) list
595
                  | ARRAY of json list
596
                  | NUMBER of real
597
                  | STRING of string
598
                  | BOOL of bool
599
                  | NULL
600

    
601
    datatype 'a result = OK of 'a
602
                       | ERROR of string
603

    
604
    val parse : string -> json result
605
    val serialise : json -> string
606
    val serialiseIndented : json -> string
607

    
608
end
609

    
610
structure Json :> JSON = struct
611

    
612
    datatype json = OBJECT of (string * json) list
613
                  | ARRAY of json list
614
                  | NUMBER of real
615
                  | STRING of string
616
                  | BOOL of bool
617
                  | NULL
618

    
619
    datatype 'a result = OK of 'a
620
                       | ERROR of string
621

    
622
    structure T = struct
623
        datatype token = NUMBER of char list
624
                       | STRING of string
625
                       | BOOL of bool
626
                       | NULL
627
                       | CURLY_L
628
                       | CURLY_R
629
                       | SQUARE_L
630
                       | SQUARE_R
631
                       | COLON
632
                       | COMMA
633

    
634
        fun toString t =
635
            case t of NUMBER digits => implode digits
636
                    | STRING s => s
637
                    | BOOL b => Bool.toString b
638
                    | NULL => "null"
639
                    | CURLY_L => "{"
640
                    | CURLY_R => "}"
641
                    | SQUARE_L => "["
642
                    | SQUARE_R => "]"
643
                    | COLON => ":"
644
                    | COMMA => ","
645
    end
646

    
647
    fun bmpToUtf8 cp =  (* convert a codepoint in Unicode BMP to utf8 bytes *)
648
        let open Word
649
	    infix 6 orb andb >>
650
        in
651
            map (Char.chr o toInt)
652
                (if cp < 0wx80 then
653
                     [cp]
654
                 else if cp < 0wx800 then
655
                     [0wxc0 orb (cp >> 0w6), 0wx80 orb (cp andb 0wx3f)]
656
                 else if cp < 0wx10000 then
657
                     [0wxe0 orb (cp >> 0w12),
658
                      0wx80 orb ((cp >> 0w6) andb 0wx3f),
659
		      0wx80 orb (cp andb 0wx3f)]
660
                 else raise Fail ("Invalid BMP point " ^ (Word.toString cp)))
661
        end
662
                      
663
    fun error pos text = ERROR (text ^ " at character position " ^
664
                                Int.toString (pos - 1))
665
    fun token_error pos = error pos ("Unexpected token")
666

    
667
    fun lexNull pos acc (#"u" :: #"l" :: #"l" :: xs) =
668
        lex (pos + 3) (T.NULL :: acc) xs
669
      | lexNull pos acc _ = token_error pos
670

    
671
    and lexTrue pos acc (#"r" :: #"u" :: #"e" :: xs) =
672
        lex (pos + 3) (T.BOOL true :: acc) xs
673
      | lexTrue pos acc _ = token_error pos
674

    
675
    and lexFalse pos acc (#"a" :: #"l" :: #"s" :: #"e" :: xs) =
676
        lex (pos + 4) (T.BOOL false :: acc) xs
677
      | lexFalse pos acc _ = token_error pos
678

    
679
    and lexChar tok pos acc xs =
680
        lex pos (tok :: acc) xs
681
        
682
    and lexString pos acc cc =
683
        let datatype escaped = ESCAPED | NORMAL
684
            fun lexString' pos text ESCAPED [] =
685
                error pos "End of input during escape sequence"
686
              | lexString' pos text NORMAL [] = 
687
                error pos "End of input during string"
688
              | lexString' pos text ESCAPED (x :: xs) =
689
                let fun esc c = lexString' (pos + 1) (c :: text) NORMAL xs
690
                in case x of
691
                       #"\"" => esc x
692
                     | #"\\" => esc x
693
                     | #"/"  => esc x
694
                     | #"b"  => esc #"\b"
695
                     | #"f"  => esc #"\f"
696
                     | #"n"  => esc #"\n"
697
                     | #"r"  => esc #"\r"
698
                     | #"t"  => esc #"\t"
699
                     | _     => error pos ("Invalid escape \\" ^
700
                                           Char.toString x)
701
                end
702
              | lexString' pos text NORMAL (#"\\" :: #"u" ::a::b::c::d:: xs) =
703
                if List.all Char.isHexDigit [a,b,c,d]
704
                then case Word.fromString ("0wx" ^ (implode [a,b,c,d])) of
705
                         SOME w => (let val utf = rev (bmpToUtf8 w) in
706
                                        lexString' (pos + 6) (utf @ text)
707
                                                   NORMAL xs
708
                                    end
709
                                    handle Fail err => error pos err)
710
                       | NONE => error pos "Invalid Unicode BMP escape sequence"
711
                else error pos "Invalid Unicode BMP escape sequence"
712
              | lexString' pos text NORMAL (x :: xs) =
713
                if Char.ord x < 0x20
714
                then error pos "Invalid unescaped control character"
715
                else
716
                    case x of
717
                        #"\"" => OK (rev text, xs, pos + 1)
718
                      | #"\\" => lexString' (pos + 1) text ESCAPED xs
719
                      | _     => lexString' (pos + 1) (x :: text) NORMAL xs
720
        in
721
            case lexString' pos [] NORMAL cc of
722
                OK (text, rest, newpos) =>
723
                lex newpos (T.STRING (implode text) :: acc) rest
724
              | ERROR e => ERROR e
725
        end
726

    
727
    and lexNumber firstChar pos acc cc =
728
        let val valid = explode ".+-e"
729
            fun lexNumber' pos digits [] = (rev digits, [], pos)
730
              | lexNumber' pos digits (x :: xs) =
731
                if x = #"E" then lexNumber' (pos + 1) (#"e" :: digits) xs
732
                else if Char.isDigit x orelse List.exists (fn c => x = c) valid
733
                then lexNumber' (pos + 1) (x :: digits) xs
734
                else (rev digits, x :: xs, pos)
735
            val (digits, rest, newpos) =
736
                lexNumber' (pos - 1) [] (firstChar :: cc)
737
        in
738
            case digits of
739
                [] => token_error pos
740
              | _ => lex newpos (T.NUMBER digits :: acc) rest
741
        end
742
                                           
743
    and lex pos acc [] = OK (rev acc)
744
      | lex pos acc (x::xs) = 
745
        (case x of
746
             #" "  => lex
747
           | #"\t" => lex
748
           | #"\n" => lex
749
           | #"\r" => lex
750
           | #"{"  => lexChar T.CURLY_L
751
           | #"}"  => lexChar T.CURLY_R
752
           | #"["  => lexChar T.SQUARE_L
753
           | #"]"  => lexChar T.SQUARE_R
754
           | #":"  => lexChar T.COLON
755
           | #","  => lexChar T.COMMA
756
           | #"\"" => lexString
757
           | #"t"  => lexTrue
758
           | #"f"  => lexFalse
759
           | #"n"  => lexNull
760
           | x     => lexNumber x) (pos + 1) acc xs
761

    
762
    fun show [] = "end of input"
763
      | show (tok :: _) = T.toString tok
764

    
765
    fun parseNumber digits =
766
        (* Note lexNumber already case-insensitised the E for us *)
767
        let open Char
768

    
769
            fun okExpDigits [] = false
770
              | okExpDigits (c :: []) = isDigit c
771
              | okExpDigits (c :: cs) = isDigit c andalso okExpDigits cs
772

    
773
            fun okExponent [] = false
774
              | okExponent (#"+" :: cs) = okExpDigits cs
775
              | okExponent (#"-" :: cs) = okExpDigits cs
776
              | okExponent cc = okExpDigits cc
777

    
778
            fun okFracTrailing [] = true
779
              | okFracTrailing (c :: cs) =
780
                (isDigit c andalso okFracTrailing cs) orelse
781
                (c = #"e" andalso okExponent cs)
782

    
783
            fun okFraction [] = false
784
              | okFraction (c :: cs) =
785
                isDigit c andalso okFracTrailing cs
786

    
787
            fun okPosTrailing [] = true
788
              | okPosTrailing (#"." :: cs) = okFraction cs
789
              | okPosTrailing (#"e" :: cs) = okExponent cs
790
              | okPosTrailing (c :: cs) =
791
                isDigit c andalso okPosTrailing cs
792
                                                      
793
            fun okPositive [] = false
794
              | okPositive (#"0" :: []) = true
795
              | okPositive (#"0" :: #"." :: cs) = okFraction cs
796
              | okPositive (#"0" :: #"e" :: cs) = okExponent cs
797
              | okPositive (#"0" :: cs) = false
798
              | okPositive (c :: cs) = isDigit c andalso okPosTrailing cs
799
                    
800
            fun okNumber (#"-" :: cs) = okPositive cs
801
              | okNumber cc = okPositive cc
802
        in
803
            if okNumber digits
804
            then case Real.fromString (implode digits) of
805
                     NONE => ERROR "Number out of range"
806
                   | SOME r => OK r
807
            else ERROR ("Invalid number \"" ^ (implode digits) ^ "\"")
808
        end
809
                                     
810
    fun parseObject (T.CURLY_R :: xs) = OK (OBJECT [], xs)
811
      | parseObject tokens =
812
        let fun parsePair (T.STRING key :: T.COLON :: xs) =
813
                (case parseTokens xs of
814
                     ERROR e => ERROR e
815
                   | OK (j, xs) => OK ((key, j), xs))
816
              | parsePair other =
817
                ERROR ("Object key/value pair expected around \"" ^
818
                       show other ^ "\"")
819
            fun parseObject' acc [] = ERROR "End of input during object"
820
              | parseObject' acc tokens =
821
                case parsePair tokens of
822
                    ERROR e => ERROR e
823
                  | OK (pair, T.COMMA :: xs) =>
824
                    parseObject' (pair :: acc) xs
825
                  | OK (pair, T.CURLY_R :: xs) =>
826
                    OK (OBJECT (rev (pair :: acc)), xs)
827
                  | OK (_, _) => ERROR "Expected , or } after object element"
828
        in
829
            parseObject' [] tokens
830
        end
831

    
832
    and parseArray (T.SQUARE_R :: xs) = OK (ARRAY [], xs)
833
      | parseArray tokens =
834
        let fun parseArray' acc [] = ERROR "End of input during array"
835
              | parseArray' acc tokens =
836
                case parseTokens tokens of
837
                    ERROR e => ERROR e
838
                  | OK (j, T.COMMA :: xs) => parseArray' (j :: acc) xs
839
                  | OK (j, T.SQUARE_R :: xs) => OK (ARRAY (rev (j :: acc)), xs)
840
                  | OK (_, _) => ERROR "Expected , or ] after array element"
841
        in
842
            parseArray' [] tokens
843
        end
844

    
845
    and parseTokens [] = ERROR "Value expected"
846
      | parseTokens (tok :: xs) =
847
        (case tok of
848
             T.NUMBER d => (case parseNumber d of
849
                                OK r => OK (NUMBER r, xs)
850
                              | ERROR e => ERROR e)
851
           | T.STRING s => OK (STRING s, xs)
852
           | T.BOOL b   => OK (BOOL b, xs)
853
           | T.NULL     => OK (NULL, xs)
854
           | T.CURLY_L  => parseObject xs
855
           | T.SQUARE_L => parseArray xs
856
           | _ => ERROR ("Unexpected token " ^ T.toString tok ^
857
                         " before " ^ show xs))
858
                                   
859
    fun parse str =
860
        case lex 1 [] (explode str) of
861
           ERROR e => ERROR e
862
         | OK tokens => case parseTokens tokens of
863
                            OK (value, []) => OK value
864
                          | OK (_, _) => ERROR "Extra data after input"
865
                          | ERROR e => ERROR e
866

    
867
    fun stringEscape s =
868
        let fun esc x = [x, #"\\"]
869
            fun escape' acc [] = rev acc
870
              | escape' acc (x :: xs) =
871
                escape' (case x of
872
                             #"\"" => esc x @ acc
873
                           | #"\\" => esc x @ acc
874
                           | #"\b" => esc #"b" @ acc
875
                           | #"\f" => esc #"f" @ acc
876
                           | #"\n" => esc #"n" @ acc
877
                           | #"\r" => esc #"r" @ acc
878
                           | #"\t" => esc #"t" @ acc
879
                           | _ =>
880
                             let val c = Char.ord x
881
                             in
882
                                 if c < 0x20
883
                                 then let val hex = Word.toString (Word.fromInt c)
884
                                      in (rev o explode) (if c < 0x10
885
                                                          then ("\\u000" ^ hex)
886
                                                          else ("\\u00" ^ hex))
887
                                      end @ acc
888
                                 else 
889
                                     x :: acc
890
                             end)
891
                        xs
892
        in
893
            implode (escape' [] (explode s))
894
        end
895
        
896
    fun serialise json =
897
        case json of
898
            OBJECT pp => "{" ^ String.concatWith
899
                                   "," (map (fn (key, value) =>
900
                                                serialise (STRING key) ^ ":" ^
901
                                                serialise value) pp) ^
902
                         "}"
903
          | ARRAY arr => "[" ^ String.concatWith "," (map serialise arr) ^ "]"
904
          | NUMBER n => implode (map (fn #"~" => #"-" | c => c) 
905
                                     (explode (Real.toString n)))
906
          | STRING s => "\"" ^ stringEscape s ^ "\""
907
          | BOOL b => Bool.toString b
908
          | NULL => "null"
909
        
910
    fun serialiseIndented json =
911
        let fun indent 0 = ""
912
              | indent i = "  " ^ indent (i - 1)
913
            fun serialiseIndented' i json =
914
                let val ser = serialiseIndented' (i + 1)
915
                in
916
                    case json of
917
                        OBJECT [] => "{}"
918
                      | ARRAY [] => "[]"
919
                      | OBJECT pp => "{\n" ^ indent (i + 1) ^
920
                                     String.concatWith
921
                                         (",\n" ^ indent (i + 1))
922
                                         (map (fn (key, value) =>
923
                                                  ser (STRING key) ^ ": " ^
924
                                                  ser value) pp) ^
925
                                     "\n" ^ indent i ^ "}"
926
                      | ARRAY arr => "[\n" ^ indent (i + 1) ^
927
                                     String.concatWith
928
                                         (",\n" ^ indent (i + 1))
929
                                         (map ser arr) ^
930
                                     "\n" ^ indent i ^ "]"
931
                      | other => serialise other
932
                end
933
        in
934
            serialiseIndented' 0 json ^ "\n"
935
        end
936
                                             
937
end
938

    
939

    
940
structure JsonBits :> sig
941
    val load_json_from : string -> Json.json (* filename -> json *)
942
    val save_json_to : string -> Json.json -> unit
943
    val lookup_optional : Json.json -> string list -> Json.json option
944
    val lookup_optional_string : Json.json -> string list -> string option
945
    val lookup_mandatory : Json.json -> string list -> Json.json
946
    val lookup_mandatory_string : Json.json -> string list -> string
947
end = struct
948

    
949
    fun load_json_from filename =
950
        case Json.parse (FileBits.file_contents filename) of
951
            Json.OK json => json
952
          | Json.ERROR e => raise Fail ("Failed to parse file: " ^ e)
953

    
954
    fun save_json_to filename json =
955
        (* using binary I/O to avoid ever writing CR/LF line endings *)
956
        let val jstr = Json.serialiseIndented json
957
            val stream = BinIO.openOut filename
958
        in
959
            BinIO.output (stream, Byte.stringToBytes jstr);
960
            BinIO.closeOut stream
961
        end
962
                                  
963
    fun lookup_optional json kk =
964
        let fun lookup key =
965
                case json of
966
                    Json.OBJECT kvs =>
967
                    (case List.find (fn (k, v) => k = key) kvs of
968
                         SOME (k, v) => SOME v
969
                       | NONE => NONE)
970
                  | _ => raise Fail "Object expected"
971
        in
972
            case kk of
973
                [] => NONE
974
              | key::[] => lookup key
975
              | key::kk => case lookup key of
976
                               NONE => NONE
977
                             | SOME j => lookup_optional j kk
978
        end
979
                       
980
    fun lookup_optional_string json kk =
981
        case lookup_optional json kk of
982
            SOME (Json.STRING s) => SOME s
983
          | SOME _ => raise Fail ("Value (if present) must be string: " ^
984
                                  (String.concatWith " -> " kk))
985
          | NONE => NONE
986

    
987
    fun lookup_mandatory json kk =
988
        case lookup_optional json kk of
989
            SOME v => v
990
          | NONE => raise Fail ("Value is mandatory: " ^
991
                                (String.concatWith " -> " kk) ^ " in json: " ^
992
                                (Json.serialise json))
993
                          
994
    fun lookup_mandatory_string json kk =
995
        case lookup_optional json kk of
996
            SOME (Json.STRING s) => s
997
          | _ => raise Fail ("Value must be string: " ^
998
                             (String.concatWith " -> " kk))
999
end
1000

    
1001
structure Provider :> sig
1002
    val load_providers : Json.json -> provider list
1003
    val load_more_providers : provider list -> Json.json -> provider list
1004
    val remote_url : context -> vcs -> source -> libname -> string
1005
end = struct
1006

    
1007
    val known_providers : provider list =
1008
        [ {
1009
            service = "bitbucket",
1010
            supports = [HG, GIT],
1011
            remote_spec = {
1012
                anon = SOME "https://bitbucket.org/{owner}/{repository}",
1013
                auth = SOME "ssh://{vcs}@bitbucket.org/{owner}/{repository}"
1014
            }
1015
          },
1016
          {
1017
            service = "github",
1018
            supports = [GIT],
1019
            remote_spec = {
1020
                anon = SOME "https://github.com/{owner}/{repository}",
1021
                auth = SOME "ssh://{vcs}@github.com/{owner}/{repository}"
1022
            }
1023
          }
1024
        ]
1025

    
1026
    fun vcs_name vcs =
1027
        case vcs of HG => "hg"
1028
                  | GIT => "git"
1029
                  | SVN => "svn"
1030
                                             
1031
    fun vcs_from_name name =
1032
        case name of "hg" => HG
1033
                   | "git" => GIT 
1034
                   | "svn" => SVN
1035
                   | other => raise Fail ("Unknown vcs name \"" ^ name ^ "\"")
1036

    
1037
    fun load_more_providers previously_loaded json =
1038
        let open JsonBits
1039
            fun load pjson pname : provider =
1040
                {
1041
                  service = pname,
1042
                  supports =
1043
                  case lookup_mandatory pjson ["vcs"] of
1044
                      Json.ARRAY vv =>
1045
                      map (fn (Json.STRING v) => vcs_from_name v
1046
                          | _ => raise Fail "Strings expected in vcs array")
1047
                          vv
1048
                    | _ => raise Fail "Array expected for vcs",
1049
                  remote_spec = {
1050
                      anon = lookup_optional_string pjson ["anonymous"],
1051
                      auth = lookup_optional_string pjson ["authenticated"]
1052
                  }
1053
                }
1054
            val loaded = 
1055
                case lookup_optional json ["services"] of
1056
                    NONE => []
1057
                  | SOME (Json.OBJECT pl) => map (fn (k, v) => load v k) pl
1058
                  | _ => raise Fail "Object expected for services in config"
1059
            val newly_loaded =
1060
                List.filter (fn p => not (List.exists (fn pp => #service p =
1061
                                                                #service pp)
1062
                                                      previously_loaded))
1063
                            loaded
1064
        in
1065
            previously_loaded @ newly_loaded
1066
        end
1067

    
1068
    fun load_providers json =
1069
        load_more_providers known_providers json
1070
                                                    
1071
    fun expand_spec spec { vcs, service, owner, repo } login =
1072
        (* ugly *)
1073
        let fun replace str = 
1074
                case str of
1075
                    "vcs" => vcs_name vcs
1076
                  | "service" => service
1077
                  | "owner" =>
1078
                    (case owner of
1079
                         SOME ostr => ostr
1080
                       | NONE => raise Fail ("Owner not specified for service " ^
1081
                                             service))
1082
                  | "repository" => repo
1083
                  | "account" =>
1084
                    (case login of
1085
                         SOME acc => acc
1086
                       | NONE => raise Fail ("Account not given for service " ^
1087
                                             service))
1088
                  | other => raise Fail ("Unknown variable \"" ^ other ^
1089
                                         "\" in spec for service " ^ service)
1090
            fun expand' acc sstr =
1091
                case Substring.splitl (fn c => c <> #"{") sstr of
1092
                    (pfx, sfx) =>
1093
                    if Substring.isEmpty sfx
1094
                    then rev (pfx :: acc)
1095
                    else 
1096
                        case Substring.splitl (fn c => c <> #"}") sfx of
1097
                            (tok, remainder) =>
1098
                            if Substring.isEmpty remainder
1099
                            then rev (tok :: pfx :: acc)
1100
                            else let val replacement =
1101
                                         replace
1102
                                             (* tok begins with "{": *)
1103
                                             (Substring.string
1104
                                                  (Substring.triml 1 tok))
1105
                                 in
1106
                                     expand' (Substring.full replacement ::
1107
                                              pfx :: acc)
1108
                                             (* remainder begins with "}": *)
1109
                                             (Substring.triml 1 remainder)
1110
                                 end
1111
        in
1112
            Substring.concat (expand' [] (Substring.full spec))
1113
        end
1114
        
1115
    fun provider_url req login providers =
1116
        case providers of
1117
            [] => raise Fail ("Unknown service \"" ^ (#service req) ^
1118
                              "\" for vcs \"" ^ (vcs_name (#vcs req)) ^ "\"")
1119
          | ({ service, supports, remote_spec : remote_spec } :: rest) =>
1120
            if service <> (#service req) orelse
1121
               not (List.exists (fn v => v = (#vcs req)) supports)
1122
            then provider_url req login rest
1123
            else
1124
                case (login, #auth remote_spec, #anon remote_spec) of
1125
                    (SOME _, SOME auth, _) => expand_spec auth req login
1126
                  | (SOME _, _, SOME anon) => expand_spec anon req NONE
1127
                  | (NONE,   _, SOME anon) => expand_spec anon req NONE
1128
                  | _ => raise Fail ("No suitable anonymous or authenticated " ^
1129
                                     "URL spec provided for service \"" ^
1130
                                     service ^ "\"")
1131

    
1132
    fun login_for ({ accounts, ... } : context) service =
1133
        case List.find (fn a => service = #service a) accounts of
1134
            SOME { login, ... } => SOME login
1135
          | NONE => NONE
1136

    
1137
    fun reponame_for path =
1138
        case String.tokens (fn c => c = #"/") path of
1139
            [] => raise Fail "Non-empty library path required"
1140
          | toks => hd (rev toks)
1141
                        
1142
    fun remote_url (context : context) vcs source libname =
1143
        case source of
1144
            URL_SOURCE u => u
1145
          | SERVICE_SOURCE { service, owner, repo } =>
1146
            provider_url { vcs = vcs,
1147
                           service = service,
1148
                           owner = owner,
1149
                           repo = case repo of
1150
                                      SOME r => r
1151
                                    | NONE => reponame_for libname }
1152
                         (login_for context service)
1153
                         (#providers context)
1154
end
1155

    
1156
structure HgControl :> VCS_CONTROL = struct
1157

    
1158
    (* Pulls always use an explicit URL, never just the default
1159
       remote, in order to ensure we update properly if the location
1160
       given in the project file changes. *)
1161

    
1162
    type vcsstate = { id: string, modified: bool,
1163
                      branch: string, tags: string list }
1164

    
1165
    val hg_args = [ "--config", "ui.interactive=true",
1166
                    "--config", "ui.merge=:merge" ]
1167
                        
1168
    fun hg_command context libname args =
1169
        FileBits.command context libname ("hg" :: hg_args @ args)
1170

    
1171
    fun hg_command_output context libname args =
1172
        FileBits.command_output context libname ("hg" :: hg_args @ args)
1173
                        
1174
    fun exists context libname =
1175
        OK (OS.FileSys.isDir (FileBits.subpath context libname ".hg"))
1176
        handle _ => OK false
1177

    
1178
    fun remote_for context (libname, source) =
1179
        Provider.remote_url context HG source libname
1180

    
1181
    fun current_state context libname : vcsstate result =
1182
        let fun is_branch text = text <> "" andalso #"(" = hd (explode text)
1183
            and extract_branch b =
1184
                if is_branch b     (* need to remove enclosing parens *)
1185
                then (implode o rev o tl o rev o tl o explode) b
1186
                else "default"
1187
            and is_modified id = id <> "" andalso #"+" = hd (rev (explode id))
1188
            and extract_id id =
1189
                if is_modified id  (* need to remove trailing "+" *)
1190
                then (implode o rev o tl o rev o explode) id
1191
                else id
1192
            and split_tags tags = String.tokens (fn c => c = #"/") tags
1193
            and state_for (id, branch, tags) =
1194
                OK { id = extract_id id,
1195
                     modified = is_modified id,
1196
                     branch = extract_branch branch,
1197
                     tags = split_tags tags }
1198
        in        
1199
            case hg_command_output context libname ["id"] of
1200
                ERROR e => ERROR e
1201
              | OK out =>
1202
                case String.tokens (fn x => x = #" ") out of
1203
                    [id, branch, tags] => state_for (id, branch, tags)
1204
                  | [id, other] => if is_branch other
1205
                                   then state_for (id, other, "")
1206
                                   else state_for (id, "", other)
1207
                  | [id] => state_for (id, "", "")
1208
                  | _ => ERROR ("Unexpected output from hg id: " ^ out)
1209
        end
1210

    
1211
    fun branch_name branch = case branch of
1212
                                 DEFAULT_BRANCH => "default"
1213
                               | BRANCH "" => "default"
1214
                               | BRANCH b => b
1215

    
1216
    fun id_of context libname =
1217
        case current_state context libname of
1218
            ERROR e => ERROR e
1219
          | OK { id, ... } => OK id
1220

    
1221
    fun is_at context (libname, id_or_tag) =
1222
        case current_state context libname of
1223
            ERROR e => ERROR e
1224
          | OK { id, tags, ... } => 
1225
            OK (String.isPrefix id_or_tag id orelse
1226
                String.isPrefix id id_or_tag orelse
1227
                List.exists (fn t => t = id_or_tag) tags)
1228

    
1229
    fun is_on_branch context (libname, b) =
1230
        case current_state context libname of
1231
            ERROR e => ERROR e
1232
          | OK { branch, ... } => OK (branch = branch_name b)
1233
               
1234
    fun is_newest_locally context (libname, branch) =
1235
        case hg_command_output context libname
1236
                               ["log", "-l1",
1237
                                "-b", branch_name branch,
1238
                                "--template", "{node}"] of
1239
            ERROR e => OK false (* desired branch does not exist *)
1240
          | OK newest_in_repo => is_at context (libname, newest_in_repo)
1241

    
1242
    fun pull context (libname, source) =
1243
        let val url = remote_for context (libname, source)
1244
        in
1245
            hg_command context libname
1246
                       (if FileBits.verbose ()
1247
                        then ["pull", url]
1248
                        else ["pull", "-q", url])
1249
        end
1250

    
1251
    fun is_newest context (libname, source, branch) =
1252
        case is_newest_locally context (libname, branch) of
1253
            ERROR e => ERROR e
1254
          | OK false => OK false
1255
          | OK true =>
1256
            case pull context (libname, source) of
1257
                ERROR e => ERROR e
1258
              | _ => is_newest_locally context (libname, branch)
1259

    
1260
    fun is_modified_locally context libname =
1261
        case current_state context libname of
1262
            ERROR e => ERROR e
1263
          | OK { modified, ... } => OK modified
1264
                
1265
    fun checkout context (libname, source, branch) =
1266
        let val url = remote_for context (libname, source)
1267
        in
1268
            (* make the lib dir rather than just the ext dir, since
1269
               the lib dir might be nested and hg will happily check
1270
               out into an existing empty dir anyway *)
1271
            case FileBits.mkpath (FileBits.libpath context libname) of
1272
                ERROR e => ERROR e
1273
              | _ => hg_command context ""
1274
                                ["clone", "-u", branch_name branch,
1275
                                 url, libname]
1276
        end
1277
                                                    
1278
    fun update context (libname, source, branch) =
1279
        let val pull_result = pull context (libname, source)
1280
        in
1281
            case hg_command context libname ["update", branch_name branch] of
1282
                ERROR e => ERROR e
1283
              | _ =>
1284
                case pull_result of
1285
                    ERROR e => ERROR e
1286
                  | _ => OK ()
1287
        end
1288

    
1289
    fun update_to context (libname, _, "") =
1290
        ERROR "Non-empty id (tag or revision id) required for update_to"
1291
      | update_to context (libname, source, id) = 
1292
        let val pull_result = pull context (libname, source)
1293
        in
1294
            case hg_command context libname ["update", "-r", id] of
1295
                OK _ => OK ()
1296
              | ERROR e =>
1297
                case pull_result of
1298
                    ERROR e' => ERROR e' (* this was the ur-error *)
1299
                  | _ => ERROR e
1300
        end
1301

    
1302
    fun copy_url_for context libname =
1303
        OK (FileBits.file_url (FileBits.libpath context libname))
1304
            
1305
end
1306

    
1307
structure GitControl :> VCS_CONTROL = struct
1308

    
1309
    (* With Git repos we always operate in detached HEAD state. Even
1310
       the master branch is checked out using a remote reference
1311
       (vext/master). The remote we use is always named vext, and we
1312
       update it to the expected URL each time we fetch, in order to
1313
       ensure we update properly if the location given in the project
1314
       file changes. The origin remote is unused. *)
1315

    
1316
    fun git_command context libname args =
1317
        FileBits.command context libname ("git" :: args)
1318

    
1319
    fun git_command_output context libname args =
1320
        FileBits.command_output context libname ("git" :: args)
1321
                            
1322
    fun exists context libname =
1323
        OK (OS.FileSys.isDir (FileBits.subpath context libname ".git"))
1324
        handle _ => OK false
1325

    
1326
    fun remote_for context (libname, source) =
1327
        Provider.remote_url context GIT source libname
1328

    
1329
    fun branch_name branch = case branch of
1330
                                 DEFAULT_BRANCH => "master"
1331
                               | BRANCH "" => "master"
1332
                               | BRANCH b => b
1333

    
1334
    val our_remote = "vext"
1335
                                                 
1336
    fun remote_branch_name branch = our_remote ^ "/" ^ branch_name branch
1337

    
1338
    fun checkout context (libname, source, branch) =
1339
        let val url = remote_for context (libname, source)
1340
        in
1341
            (* make the lib dir rather than just the ext dir, since
1342
               the lib dir might be nested and git will happily check
1343
               out into an existing empty dir anyway *)
1344
            case FileBits.mkpath (FileBits.libpath context libname) of
1345
                OK () => git_command context ""
1346
                                     ["clone", "--origin", our_remote,
1347
                                      "--branch", branch_name branch,
1348
                                      url, libname]
1349
              | ERROR e => ERROR e
1350
        end
1351

    
1352
    fun add_our_remote context (libname, source) =
1353
        (* When we do the checkout ourselves (above), we add the
1354
           remote at the same time. But if the repo was cloned by
1355
           someone else, we'll need to do it after the fact. Git
1356
           doesn't seem to have a means to add a remote or change its
1357
           url if it already exists; seems we have to do this: *)
1358
        let val url = remote_for context (libname, source)
1359
        in
1360
            case git_command context libname
1361
                             ["remote", "set-url", our_remote, url] of
1362
                OK () => OK ()
1363
              | ERROR e => git_command context libname
1364
                                       ["remote", "add", "-f", our_remote, url]
1365
        end
1366

    
1367
    (* NB git rev-parse HEAD shows revision id of current checkout;
1368
       git rev-list -1 <tag> shows revision id of revision with that tag *)
1369

    
1370
    fun id_of context libname =
1371
        git_command_output context libname ["rev-parse", "HEAD"]
1372
            
1373
    fun is_at context (libname, id_or_tag) =
1374
        case id_of context libname of
1375
            ERROR e => OK false (* HEAD nonexistent, expected in empty repo *)
1376
          | OK id =>
1377
            if String.isPrefix id_or_tag id orelse
1378
               String.isPrefix id id_or_tag
1379
            then OK true
1380
            else is_at_tag context (libname, id, id_or_tag)
1381

    
1382
    and is_at_tag context (libname, id, tag) =
1383
        (* For annotated tags (with message) show-ref returns the tag
1384
           object ref rather than that of the revision being tagged;
1385
           we need the subsequent rev-list to chase that up. In fact
1386
           the rev-list on its own is enough to get us the id direct
1387
           from the tag name, but it fails with an error if the tag
1388
           doesn't exist, whereas we want to handle that quietly in
1389
           case the tag simply hasn't been pulled yet *)
1390
        case git_command_output context libname
1391
                                ["show-ref", "refs/tags/" ^ tag, "--"] of
1392
            OK "" => OK false (* Not a tag *)
1393
          | ERROR _ => OK false
1394
          | OK s =>
1395
            let val tag_ref = hd (String.tokens (fn c => c = #" ") s)
1396
            in
1397
                case git_command_output context libname
1398
                                        ["rev-list", "-1", tag_ref] of
1399
                    OK tagged => OK (id = tagged)
1400
                  | ERROR _ => OK false
1401
            end
1402
                           
1403
    fun branch_tip context (libname, branch) =
1404
        (* We don't have access to the source info or the network
1405
           here, as this is used by status (e.g. via is_on_branch) as
1406
           well as review. It's possible the remote branch won't exist,
1407
           e.g. if the repo was checked out by something other than
1408
           Vext, and if that's the case, we can't add it here; we'll
1409
           just have to fail, since checking against local branches
1410
           instead could produce the wrong result. *)
1411
        git_command_output context libname
1412
                           ["rev-list", "-1",
1413
                            remote_branch_name branch, "--"]
1414
                       
1415
    fun is_newest_locally context (libname, branch) =
1416
        case branch_tip context (libname, branch) of
1417
            ERROR e => OK false
1418
          | OK rev => is_at context (libname, rev)
1419

    
1420
    fun is_on_branch context (libname, branch) =
1421
        case branch_tip context (libname, branch) of
1422
            ERROR e => OK false
1423
          | OK rev =>
1424
            case is_at context (libname, rev) of
1425
                ERROR e => ERROR e
1426
              | OK true => OK true
1427
              | OK false =>
1428
                case git_command context libname
1429
                                 ["merge-base", "--is-ancestor",
1430
                                  "HEAD", remote_branch_name branch] of
1431
                    ERROR e => OK false  (* cmd returns non-zero for no *)
1432
                  | _ => OK true
1433

    
1434
    fun fetch context (libname, source) =
1435
        case add_our_remote context (libname, source) of
1436
            ERROR e => ERROR e
1437
          | _ => git_command context libname ["fetch", our_remote]
1438
                            
1439
    fun is_newest context (libname, source, branch) =
1440
        case add_our_remote context (libname, source) of
1441
            ERROR e => ERROR e
1442
          | OK () => 
1443
            case is_newest_locally context (libname, branch) of
1444
                ERROR e => ERROR e
1445
              | OK false => OK false
1446
              | OK true =>
1447
                case fetch context (libname, source) of
1448
                    ERROR e => ERROR e
1449
                  | _ => is_newest_locally context (libname, branch)
1450

    
1451
    fun is_modified_locally context libname =
1452
        case git_command_output context libname ["status", "--porcelain"] of
1453
            ERROR e => ERROR e
1454
          | OK "" => OK false
1455
          | OK _ => OK true
1456

    
1457
    (* This function updates to the latest revision on a branch rather
1458
       than to a specific id or tag. We can't just checkout the given
1459
       branch, as that will succeed even if the branch isn't up to
1460
       date. We could checkout the branch and then fetch and merge,
1461
       but it's perhaps cleaner not to maintain a local branch at all,
1462
       but instead checkout the remote branch as a detached head. *)
1463

    
1464
    fun update context (libname, source, branch) =
1465
        case fetch context (libname, source) of
1466
            ERROR e => ERROR e
1467
          | _ =>
1468
            case git_command context libname ["checkout", "--detach",
1469
                                              remote_branch_name branch] of
1470
                ERROR e => ERROR e
1471
              | _ => OK ()
1472

    
1473
    (* This function is dealing with a specific id or tag, so if we
1474
       can successfully check it out (detached) then that's all we
1475
       need to do, regardless of whether fetch succeeded or not. We do
1476
       attempt the fetch first, though, purely in order to avoid ugly
1477
       error messages in the common case where we're being asked to
1478
       update to a new pin (from the lock file) that hasn't been
1479
       fetched yet. *)
1480

    
1481
    fun update_to context (libname, _, "") = 
1482
        ERROR "Non-empty id (tag or revision id) required for update_to"
1483
      | update_to context (libname, source, id) =
1484
        let val fetch_result = fetch context (libname, source)
1485
        in
1486
            case git_command context libname ["checkout", "--detach", id] of
1487
                OK _ => OK ()
1488
              | ERROR e =>
1489
                case fetch_result of
1490
                    ERROR e' => ERROR e' (* this was the ur-error *)
1491
                  | _ => ERROR e
1492
        end
1493

    
1494
    fun copy_url_for context libname =
1495
        OK (FileBits.file_url (FileBits.libpath context libname))
1496
            
1497
end
1498

    
1499
(* SubXml - A parser for a subset of XML
1500
   https://bitbucket.org/cannam/sml-simplexml
1501
   Copyright 2018 Chris Cannam. BSD licence.
1502
*)
1503

    
1504
signature SUBXML = sig
1505

    
1506
    datatype node = ELEMENT of { name : string, children : node list }
1507
                  | ATTRIBUTE of { name : string, value : string }
1508
                  | TEXT of string
1509
                  | CDATA of string
1510
                  | COMMENT of string
1511

    
1512
    datatype document = DOCUMENT of { name : string, children : node list }
1513

    
1514
    datatype 'a result = OK of 'a
1515
                       | ERROR of string
1516

    
1517
    val parse : string -> document result
1518
    val serialise : document -> string
1519
                                  
1520
end
1521

    
1522
structure SubXml :> SUBXML = struct
1523

    
1524
    datatype node = ELEMENT of { name : string, children : node list }
1525
                  | ATTRIBUTE of { name : string, value : string }
1526
                  | TEXT of string
1527
                  | CDATA of string
1528
                  | COMMENT of string
1529

    
1530
    datatype document = DOCUMENT of { name : string, children : node list }
1531

    
1532
    datatype 'a result = OK of 'a
1533
                       | ERROR of string
1534

    
1535
    structure T = struct
1536
        datatype token = ANGLE_L
1537
                       | ANGLE_R
1538
                       | ANGLE_SLASH_L
1539
                       | SLASH_ANGLE_R
1540
                       | EQUAL
1541
                       | NAME of string
1542
                       | TEXT of string
1543
                       | CDATA of string
1544
                       | COMMENT of string
1545

    
1546
        fun name t =
1547
            case t of ANGLE_L => "<"
1548
                    | ANGLE_R => ">"
1549
                    | ANGLE_SLASH_L => "</"
1550
                    | SLASH_ANGLE_R => "/>"
1551
                    | EQUAL => "="
1552
                    | NAME s => "name \"" ^ s ^ "\""
1553
                    | TEXT s => "text"
1554
                    | CDATA _ => "CDATA section"
1555
                    | COMMENT _ => "comment"
1556
    end
1557

    
1558
    structure Lex :> sig
1559
                  val lex : string -> T.token list result
1560
              end = struct
1561
                      
1562
        fun error pos text =
1563
            ERROR (text ^ " at character position " ^ Int.toString (pos-1))
1564
        fun tokenError pos token =
1565
            error pos ("Unexpected token '" ^ Char.toString token ^ "'")
1566

    
1567
        val nameEnd = explode " \t\n\r\"'</>!=?"
1568
                              
1569
        fun quoted quote pos acc cc =
1570
            let fun quoted' pos text [] =
1571
                    error pos "Document ends during quoted string"
1572
                  | quoted' pos text (x::xs) =
1573
                    if x = quote
1574
                    then OK (rev text, xs, pos+1)
1575
                    else quoted' (pos+1) (x::text) xs
1576
            in
1577
                case quoted' pos [] cc of
1578
                    ERROR e => ERROR e
1579
                  | OK (text, rest, newpos) =>
1580
                    inside newpos (T.TEXT (implode text) :: acc) rest
1581
            end
1582

    
1583
        and name first pos acc cc =
1584
            let fun name' pos text [] =
1585
                    error pos "Document ends during name"
1586
                  | name' pos text (x::xs) =
1587
                    if List.find (fn c => c = x) nameEnd <> NONE
1588
                    then OK (rev text, (x::xs), pos)
1589
                    else name' (pos+1) (x::text) xs
1590
            in
1591
                case name' (pos-1) [] (first::cc) of
1592
                    ERROR e => ERROR e
1593
                  | OK ([], [], pos) => error pos "Document ends before name"
1594
                  | OK ([], (x::xs), pos) => tokenError pos x
1595
                  | OK (text, rest, pos) =>
1596
                    inside pos (T.NAME (implode text) :: acc) rest
1597
            end
1598

    
1599
        and comment pos acc cc =
1600
            let fun comment' pos text cc =
1601
                    case cc of
1602
                        #"-" :: #"-" :: #">" :: xs => OK (rev text, xs, pos+3)
1603
                      | x :: xs => comment' (pos+1) (x::text) xs
1604
                      | [] => error pos "Document ends during comment"
1605
            in
1606
                case comment' pos [] cc of
1607
                    ERROR e => ERROR e
1608
                  | OK (text, rest, pos) => 
1609
                    outside pos (T.COMMENT (implode text) :: acc) rest
1610
            end
1611

    
1612
        and instruction pos acc cc =
1613
            case cc of
1614
                #"?" :: #">" :: xs => outside (pos+2) acc xs
1615
              | #">" :: _ => tokenError pos #">"
1616
              | x :: xs => instruction (pos+1) acc xs
1617
              | [] => error pos "Document ends during processing instruction"
1618

    
1619
        and cdata pos acc cc =
1620
            let fun cdata' pos text cc =
1621
                    case cc of
1622
                        #"]" :: #"]" :: #">" :: xs => OK (rev text, xs, pos+3)
1623
                      | x :: xs => cdata' (pos+1) (x::text) xs
1624
                      | [] => error pos "Document ends during CDATA section"
1625
            in
1626
                case cdata' pos [] cc of
1627
                    ERROR e => ERROR e
1628
                  | OK (text, rest, pos) =>
1629
                    outside pos (T.CDATA (implode text) :: acc) rest
1630
            end
1631
                
1632
        and doctype pos acc cc =
1633
            case cc of
1634
                #">" :: xs => outside (pos+1) acc xs
1635
              | x :: xs => doctype (pos+1) acc xs
1636
              | [] => error pos "Document ends during DOCTYPE"
1637

    
1638
        and declaration pos acc cc =
1639
            case cc of
1640
                #"-" :: #"-" :: xs =>
1641
                comment (pos+2) acc xs
1642
              | #"[" :: #"C" :: #"D" :: #"A" :: #"T" :: #"A" :: #"[" :: xs =>
1643
                cdata (pos+7) acc xs
1644
              | #"D" :: #"O" :: #"C" :: #"T" :: #"Y" :: #"P" :: #"E" :: xs =>
1645
                doctype (pos+7) acc xs
1646
              | [] => error pos "Document ends during declaration"
1647
              | _ => error pos "Unsupported declaration type"
1648

    
1649
        and left pos acc cc =
1650
            case cc of
1651
                #"/" :: xs => inside (pos+1) (T.ANGLE_SLASH_L :: acc) xs
1652
              | #"!" :: xs => declaration (pos+1) acc xs
1653
              | #"?" :: xs => instruction (pos+1) acc xs
1654
              | xs => inside pos (T.ANGLE_L :: acc) xs
1655

    
1656
        and slash pos acc cc =
1657
            case cc of
1658
                #">" :: xs => outside (pos+1) (T.SLASH_ANGLE_R :: acc) xs
1659
              | x :: _ => tokenError pos x
1660
              | [] => error pos "Document ends before element closed"
1661

    
1662
        and close pos acc xs = outside pos (T.ANGLE_R :: acc) xs
1663

    
1664
        and equal pos acc xs = inside pos (T.EQUAL :: acc) xs
1665

    
1666
        and outside pos acc [] = OK acc
1667
          | outside pos acc cc =
1668
            let fun textOf text = T.TEXT (implode (rev text))
1669
                fun outside' pos [] acc [] = OK acc
1670
                  | outside' pos text acc [] = OK (textOf text :: acc)
1671
                  | outside' pos text acc (x::xs) =
1672
                    case x of
1673
                        #"<" => if text = []
1674
                                then left (pos+1) acc xs
1675
                                else left (pos+1) (textOf text :: acc) xs
1676
                      | x => outside' (pos+1) (x::text) acc xs
1677
            in
1678
                outside' pos [] acc cc
1679
            end
1680
                
1681
        and inside pos acc [] = error pos "Document ends within tag"
1682
          | inside pos acc (#"<"::_) = tokenError pos #"<"
1683
          | inside pos acc (x::xs) =
1684
            (case x of
1685
                 #" " => inside | #"\t" => inside
1686
               | #"\n" => inside | #"\r" => inside
1687
               | #"\"" => quoted x | #"'" => quoted x
1688
               | #"/" => slash | #">" => close | #"=" => equal
1689
               | x => name x) (pos+1) acc xs
1690

    
1691
        fun lex str =
1692
            case outside 1 [] (explode str) of
1693
                ERROR e => ERROR e
1694
              | OK tokens => OK (rev tokens)
1695
    end
1696

    
1697
    structure Parse :> sig
1698
                  val parse : string -> document result
1699
              end = struct                            
1700
                  
1701
        fun show [] = "end of input"
1702
          | show (tok :: _) = T.name tok
1703

    
1704
        fun error toks text = ERROR (text ^ " before " ^ show toks)
1705

    
1706
        fun attribute elt name toks =
1707
            case toks of
1708
                T.EQUAL :: T.TEXT value :: xs =>
1709
                namedElement {
1710
                    name = #name elt,
1711
                    children = ATTRIBUTE { name = name, value = value } ::
1712
                               #children elt
1713
                } xs
1714
              | T.EQUAL :: xs => error xs "Expected attribute value"
1715
              | toks => error toks "Expected attribute assignment"
1716

    
1717
        and content elt toks =
1718
            case toks of
1719
                T.ANGLE_SLASH_L :: T.NAME n :: T.ANGLE_R :: xs =>
1720
                if n = #name elt
1721
                then OK (elt, xs)
1722
                else ERROR ("Closing tag </" ^ n ^ "> " ^
1723
                            "does not match opening <" ^ #name elt ^ ">")
1724
              | T.TEXT text :: xs =>
1725
                content {
1726
                    name = #name elt,
1727
                    children = TEXT text :: #children elt
1728
                } xs
1729
              | T.CDATA text :: xs =>
1730
                content {
1731
                    name = #name elt,
1732
                    children = CDATA text :: #children elt
1733
                } xs
1734
              | T.COMMENT text :: xs =>
1735
                content {
1736
                    name = #name elt,
1737
                    children = COMMENT text :: #children elt
1738
                } xs
1739
              | T.ANGLE_L :: xs =>
1740
                (case element xs of
1741
                     ERROR e => ERROR e
1742
                   | OK (child, xs) =>
1743
                     content {
1744
                         name = #name elt,
1745
                         children = ELEMENT child :: #children elt
1746
                     } xs)
1747
              | tok :: xs =>
1748
                error xs ("Unexpected token " ^ T.name tok)
1749
              | [] =>
1750
                ERROR ("Document ends within element \"" ^ #name elt ^ "\"")
1751
                       
1752
        and namedElement elt toks =
1753
            case toks of
1754
                T.SLASH_ANGLE_R :: xs => OK (elt, xs)
1755
              | T.NAME name :: xs => attribute elt name xs
1756
              | T.ANGLE_R :: xs => content elt xs
1757
              | x :: xs => error xs ("Unexpected token " ^ T.name x)
1758
              | [] => ERROR "Document ends within opening tag"
1759
                       
1760
        and element toks =
1761
            case toks of
1762
                T.NAME name :: xs =>
1763
                (case namedElement { name = name, children = [] } xs of
1764
                     ERROR e => ERROR e 
1765
                   | OK ({ name, children }, xs) =>
1766
                     OK ({ name = name, children = rev children }, xs))
1767
              | toks => error toks "Expected element name"
1768

    
1769
        and document [] = ERROR "Empty document"
1770
          | document (tok :: xs) =
1771
            case tok of
1772
                T.TEXT _ => document xs
1773
              | T.COMMENT _ => document xs
1774
              | T.ANGLE_L =>
1775
                (case element xs of
1776
                     ERROR e => ERROR e
1777
                   | OK (elt, []) => OK (DOCUMENT elt)
1778
                   | OK (elt, (T.TEXT _ :: xs)) => OK (DOCUMENT elt)
1779
                   | OK (elt, xs) => error xs "Extra data after document")
1780
              | _ => error xs ("Unexpected token " ^ T.name tok)
1781

    
1782
        fun parse str =
1783
            case Lex.lex str of
1784
                ERROR e => ERROR e
1785
              | OK tokens => document tokens
1786
    end
1787

    
1788
    structure Serialise :> sig
1789
                  val serialise : document -> string
1790
              end = struct
1791

    
1792
        fun attributes nodes =
1793
            String.concatWith
1794
                " "
1795
                (map node (List.filter
1796
                               (fn ATTRIBUTE _ => true | _ => false)
1797
                               nodes))
1798

    
1799
        and nonAttributes nodes =
1800
            String.concat
1801
                (map node (List.filter
1802
                               (fn ATTRIBUTE _ => false | _ => true)
1803
                               nodes))
1804
                
1805
        and node n =
1806
            case n of
1807
                TEXT string =>
1808
                string
1809
              | CDATA string =>
1810
                "<![CDATA[" ^ string ^ "]]>"
1811
              | COMMENT string =>
1812
                "<!-- " ^ string ^ "-->"
1813
              | ATTRIBUTE { name, value } =>
1814
                name ^ "=" ^ "\"" ^ value ^ "\"" (*!!!*)
1815
              | ELEMENT { name, children } =>
1816
                "<" ^ name ^
1817
                (case (attributes children) of
1818
                     "" => ""
1819
                   | s => " " ^ s) ^
1820
                (case (nonAttributes children) of
1821
                     "" => "/>"
1822
                   | s => ">" ^ s ^ "</" ^ name ^ ">")
1823
                              
1824
        fun serialise (DOCUMENT { name, children }) =
1825
            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" ^
1826
            node (ELEMENT { name = name, children = children })
1827
    end
1828

    
1829
    val parse = Parse.parse
1830
    val serialise = Serialise.serialise
1831
                        
1832
end
1833

    
1834

    
1835
structure SvnControl :> VCS_CONTROL = struct
1836

    
1837
    fun svn_command context libname args =
1838
        FileBits.command context libname ("svn" :: args)
1839

    
1840
    fun svn_command_output context libname args =
1841
        FileBits.command_output context libname ("svn" :: args)
1842

    
1843
    fun svn_command_lines context libname args =
1844
        case svn_command_output context libname args of
1845
            ERROR e => ERROR e
1846
          | OK s => OK (String.tokens (fn c => c = #"\n" orelse c = #"\r") s)
1847

    
1848
    fun split_line_pair line =
1849
        let fun strip_leading_ws str = case explode str of
1850
                                           #" "::rest => implode rest
1851
                                         | _ => str
1852
        in
1853
            case String.tokens (fn c => c = #":") line of
1854
                [] => ("", "")
1855
              | first::rest =>
1856
                (first, strip_leading_ws (String.concatWith ":" rest))
1857
        end
1858

    
1859
    structure X = SubXml
1860
                      
1861
    fun svn_info context libname route =
1862
        (* SVN 1.9 has info --show-item which is just what we need,
1863
           but at this point we still have 1.8 on the CI boxes so we
1864
           might as well aim to support it. For that we really have to
1865
           use the XML output format, since the default info output is
1866
           localised. This is the only thing our mini-XML parser is
1867
           used for though, so it would be good to trim it at some
1868
           point *)
1869
        let fun find elt [] = OK elt
1870
              | find { children, ... } (first :: rest) =
1871
                case List.find (fn (X.ELEMENT { name, ... }) => name = first
1872
                               | _ => false)
1873
                               children of
1874
                    NONE => ERROR ("No element \"" ^ first ^ "\" in SVN XML")
1875
                  | SOME (X.ELEMENT e) => find e rest
1876
                  | SOME _ => ERROR "Internal error"
1877
        in
1878
            case svn_command_output context libname ["info", "--xml"] of
1879
                ERROR e => ERROR e
1880
              | OK xml =>
1881
                case X.parse xml of
1882
                    X.ERROR e => ERROR e
1883
                  | X.OK (X.DOCUMENT doc) => find doc route
1884
        end
1885
            
1886
    fun exists context libname =
1887
        OK (OS.FileSys.isDir (FileBits.subpath context libname ".svn"))
1888
        handle _ => OK false
1889

    
1890
    fun remote_for context (libname, source) =
1891
        Provider.remote_url context SVN source libname
1892

    
1893
    (* Remote the checkout came from, not necessarily the one we want *)
1894
    fun actual_remote_for context libname =
1895
        case svn_info context libname ["entry", "url"] of
1896
            ERROR e => ERROR e
1897
          | OK { children, ... } =>
1898
            case List.find (fn (X.TEXT _) => true | _ => false) children of
1899
                NONE => ERROR "No content for URL in SVN info XML"
1900
              | SOME (X.TEXT url) => OK url
1901
              | SOME _ => ERROR "Internal error"
1902

    
1903
    fun id_of context libname =
1904
        case svn_info context libname ["entry"] of
1905
            ERROR e => ERROR e
1906
          | OK { children, ... } => 
1907
            case List.find
1908
                     (fn (X.ATTRIBUTE { name = "revision", ... }) => true
1909
                     | _ => false)
1910
                     children of
1911
                NONE => ERROR "No revision for entry in SVN info XML"
1912
              | SOME (X.ATTRIBUTE { value, ... }) => OK value
1913
              | SOME _ => ERROR "Internal error"
1914

    
1915
    fun is_at context (libname, id_or_tag) =
1916
        case id_of context libname of
1917
            ERROR e => ERROR e
1918
          | OK id => OK (id = id_or_tag)
1919

    
1920
    fun is_on_branch context (libname, b) =
1921
        OK (b = DEFAULT_BRANCH)
1922

    
1923
    fun check_remote context (libname, source) =
1924
      case (remote_for context (libname, source),
1925
            actual_remote_for context libname) of
1926
          (_, ERROR e) => ERROR e
1927
        | (url, OK actual) => 
1928
          if actual = url
1929
          then OK ()
1930
          else svn_command context libname ["relocate", url]
1931
               
1932
    fun is_newest context (libname, source, branch) =
1933
        case check_remote context (libname, source) of
1934
            ERROR e => ERROR e
1935
          | OK () => 
1936
            case svn_command_lines context libname
1937
                                   ["status", "--show-updates"] of
1938
                ERROR e => ERROR e
1939
              | OK lines =>
1940
                case rev lines of
1941
                    [] => ERROR "No result returned for server status"
1942
                  | last_line::_ =>
1943
                    case rev (String.tokens (fn c => c = #" ") last_line) of
1944
                        [] => ERROR "No revision field found in server status"
1945
                      | server_id::_ => is_at context (libname, server_id)
1946

    
1947
    fun is_newest_locally context (libname, branch) =
1948
        OK true (* no local history *)
1949

    
1950
    fun is_modified_locally context libname =
1951
        case svn_command_output context libname ["status"] of
1952
            ERROR e => ERROR e
1953
          | OK "" => OK false
1954
          | OK _ => OK true
1955

    
1956
    fun checkout context (libname, source, branch) =
1957
        let val url = remote_for context (libname, source)
1958
            val path = FileBits.libpath context libname
1959
        in
1960
            if FileBits.nonempty_dir_exists path
1961
            then (* Surprisingly, SVN itself has no problem with
1962
                    this. But for consistency with other VCSes we 
1963
                    don't allow it *)
1964
                ERROR ("Refusing checkout to nonempty dir \"" ^ path ^ "\"")
1965
            else 
1966
                (* make the lib dir rather than just the ext dir, since
1967
                   the lib dir might be nested and svn will happily check
1968
                   out into an existing empty dir anyway *)
1969
                case FileBits.mkpath (FileBits.libpath context libname) of
1970
                    ERROR e => ERROR e
1971
                  | _ => svn_command context "" ["checkout", url, libname]
1972
        end
1973
                                                    
1974
    fun update context (libname, source, branch) =
1975
        case check_remote context (libname, source) of
1976
            ERROR e => ERROR e
1977
          | OK () => 
1978
            case svn_command context libname
1979
                             ["update", "--accept", "postpone"] of
1980
                ERROR e => ERROR e
1981
              | _ => OK ()
1982

    
1983
    fun update_to context (libname, _, "") =
1984
        ERROR "Non-empty id (tag or revision id) required for update_to"
1985
      | update_to context (libname, source, id) = 
1986
        case check_remote context (libname, source) of
1987
            ERROR e => ERROR e
1988
          | OK () => 
1989
            case svn_command context libname
1990
                             ["update", "-r", id, "--accept", "postpone"] of
1991
                ERROR e => ERROR e
1992
              | OK _ => OK ()
1993

    
1994
    fun copy_url_for context libname =
1995
        actual_remote_for context libname
1996

    
1997
end
1998

    
1999
structure AnyLibControl :> LIB_CONTROL = struct
2000

    
2001
    structure H = LibControlFn(HgControl)
2002
    structure G = LibControlFn(GitControl)
2003
    structure S = LibControlFn(SvnControl)
2004

    
2005
    fun review context (spec as { vcs, ... } : libspec) =
2006
        (fn HG => H.review | GIT => G.review | SVN => S.review) vcs context spec
2007

    
2008
    fun status context (spec as { vcs, ... } : libspec) =
2009
        (fn HG => H.status | GIT => G.status | SVN => S.status) vcs context spec
2010

    
2011
    fun update context (spec as { vcs, ... } : libspec) =
2012
        (fn HG => H.update | GIT => G.update | SVN => S.update) vcs context spec
2013

    
2014
    fun id_of context (spec as { vcs, ... } : libspec) =
2015
        (fn HG => H.id_of | GIT => G.id_of | SVN => S.id_of) vcs context spec
2016

    
2017
end
2018

    
2019

    
2020
type exclusions = string list
2021
              
2022
structure Archive :> sig
2023

    
2024
    val archive : string * exclusions -> project -> OS.Process.status
2025
        
2026
end = struct
2027

    
2028
    (* The idea of "archive" is to replace hg/git archive, which won't
2029
       include files, like the Vext-introduced external libraries,
2030
       that are not under version control with the main repo.
2031

    
2032
       The process goes like this:
2033

    
2034
       - Make sure we have a target filename from the user, and take
2035
         its basename as our archive directory name
2036

    
2037
       - Make an "archive root" subdir of the project repo, named
2038
         typically .vext-archive
2039
       
2040
       - Identify the VCS used for the project repo. Note that any
2041
         explicit references to VCS type in this structure are to
2042
         the VCS used for the project (something Vext doesn't 
2043
         otherwise care about), not for an individual library
2044

    
2045
       - Synthesise a Vext project with the archive root as its
2046
         root path, "." as its extdir, with one library whose
2047
         name is the user-supplied basename and whose explicit
2048
         source URL is the original project root; update that
2049
         project -- thus cloning the original project to a subdir
2050
         of the archive root
2051

    
2052
       - Synthesise a Vext project identical to the original one for
2053
         this project, but with the newly-cloned copy as its root
2054
         path; update that project -- thus checking out clean copies
2055
         of the external library dirs
2056

    
2057
       - Call out to an archive program to archive up the new copy,
2058
         running e.g.
2059
         tar cvzf project-release.tar.gz \
2060
             --exclude=.hg --exclude=.git project-release
2061
         in the archive root dir
2062

    
2063
       - (We also omit the vext-project.json file and any trace of
2064
         Vext. It can't properly be run in a directory where the
2065
         external project folders already exist but their repo history
2066
         does not. End users shouldn't get to see Vext)
2067

    
2068
       - Clean up by deleting the new copy
2069
    *)
2070

    
2071
    fun project_vcs_id_and_url dir =
2072
        let val context = {
2073
                rootpath = dir,
2074
                extdir = ".",
2075
                providers = [],
2076
                accounts = []
2077
            }
2078
            val vcs_maybe = 
2079
                case [HgControl.exists context ".",
2080
                      GitControl.exists context ".",
2081
                      SvnControl.exists context "."] of
2082
                    [OK true, OK false, OK false] => OK HG
2083
                  | [OK false, OK true, OK false] => OK GIT
2084
                  | [OK false, OK false, OK true] => OK SVN
2085
                  | _ => ERROR ("Unable to identify VCS for directory " ^ dir)
2086
        in
2087
            case vcs_maybe of
2088
                ERROR e => ERROR e
2089
              | OK vcs =>
2090
                case (fn HG => HgControl.id_of
2091
                       | GIT => GitControl.id_of 
2092
                       | SVN => SvnControl.id_of)
2093
                         vcs context "." of
2094
                    ERROR e => ERROR ("Unable to find id of project repo: " ^ e)
2095
                  | OK id =>
2096
                    case (fn HG => HgControl.copy_url_for
2097
                           | GIT => GitControl.copy_url_for
2098
                           | SVN => SvnControl.copy_url_for)
2099
                             vcs context "." of
2100
                        ERROR e => ERROR ("Unable to find URL of project repo: "
2101
                                          ^ e)
2102
                      | OK url => OK (vcs, id, url)
2103
        end
2104
            
2105
    fun make_archive_root (context : context) =
2106
        let val path = OS.Path.joinDirFile {
2107
                    dir = #rootpath context,
2108
                    file = VextFilenames.archive_dir
2109
                }
2110
        in
2111
            case FileBits.mkpath path of
2112
                ERROR e => raise Fail ("Failed to create archive directory \""
2113
                                       ^ path ^ "\": " ^ e)
2114
              | OK () => path
2115
        end
2116

    
2117
    fun archive_path archive_dir target_name =
2118
        OS.Path.joinDirFile {
2119
            dir = archive_dir,
2120
            file = target_name
2121
        }
2122

    
2123
    fun check_nonexistent path =
2124
        case SOME (OS.FileSys.fileSize path) handle OS.SysErr _ => NONE of
2125
            NONE => ()
2126
          | _ => raise Fail ("Path " ^ path ^ " exists, not overwriting")
2127
            
2128
    fun make_archive_copy target_name (vcs, project_id, source_url)
2129
                          ({ context, ... } : project) =
2130
        let val archive_root = make_archive_root context
2131
            val synthetic_context = {
2132
                rootpath = archive_root,
2133
                extdir = ".",
2134
                providers = [],
2135
                accounts = []
2136
            }
2137
            val synthetic_library = {
2138
                libname = target_name,
2139
                vcs = vcs,
2140
                source = URL_SOURCE source_url,
2141
                branch = DEFAULT_BRANCH, (* overridden by pinned id below *)
2142
                project_pin = PINNED project_id,
2143
                lock_pin = PINNED project_id
2144
            }
2145
            val path = archive_path archive_root target_name
2146
            val _ = print ("Cloning original project to " ^ path
2147
                           ^ " at revision " ^ project_id ^ "...\n");
2148
            val _ = check_nonexistent path
2149
        in
2150
            case AnyLibControl.update synthetic_context synthetic_library of
2151
                ERROR e => ERROR ("Failed to clone original project to "
2152
                                  ^ path ^ ": " ^ e)
2153
              | OK _ => OK archive_root
2154
        end
2155

    
2156
    fun update_archive archive_root target_name
2157
                       (project as { context, ... } : project) =
2158
        let val synthetic_context = {
2159
                rootpath = archive_path archive_root target_name,
2160
                extdir = #extdir context,
2161
                providers = #providers context,
2162
                accounts = #accounts context
2163
            }
2164
        in
2165
            foldl (fn (lib, acc) =>
2166
                      case acc of
2167
                          ERROR e => ERROR e
2168
                        | OK () => AnyLibControl.update synthetic_context lib)
2169
                  (OK ())
2170
                  (#libs project)
2171
        end
2172

    
2173
    datatype packer = TAR
2174
                    | TAR_GZ
2175
                    | TAR_BZ2
2176
                    | TAR_XZ
2177
    (* could add other packers, e.g. zip, if we knew how to
2178
       handle the file omissions etc properly in pack_archive *)
2179
                          
2180
    fun packer_and_basename path =
2181
        let val extensions = [ (".tar", TAR),
2182
                               (".tar.gz", TAR_GZ),
2183
                               (".tar.bz2", TAR_BZ2),
2184
                               (".tar.xz", TAR_XZ)]
2185
            val filename = OS.Path.file path
2186
        in
2187
            foldl (fn ((ext, packer), acc) =>
2188
                      if String.isSuffix ext filename
2189
                      then SOME (packer,
2190
                                 String.substring (filename, 0,
2191
                                                   String.size filename -
2192
                                                   String.size ext))
2193
                      else acc)
2194
                  NONE
2195
                  extensions
2196
        end
2197
            
2198
    fun pack_archive archive_root target_name target_path packer exclusions =
2199
        case FileBits.command {
2200
                rootpath = archive_root,
2201
                extdir = ".",
2202
                providers = [],
2203
                accounts = []
2204
            } "" ([
2205
                     "tar",
2206
                     case packer of
2207
                         TAR => "cf"
2208
                       | TAR_GZ => "czf"
2209
                       | TAR_BZ2 => "cjf"
2210
                       | TAR_XZ => "cJf",
2211
                     target_path,
2212
                     "--exclude=.hg",
2213
                     "--exclude=.git",
2214
                     "--exclude=.svn",
2215
                     "--exclude=vext",
2216
                     "--exclude=vext.sml",
2217
                     "--exclude=vext.ps1",
2218
                     "--exclude=vext.bat",
2219
                     "--exclude=vext-project.json",
2220
                     "--exclude=vext-lock.json"
2221
                 ] @ (map (fn e => "--exclude=" ^ e) exclusions) @
2222
                  [ target_name ])
2223
         of
2224
            ERROR e => ERROR e
2225
          | OK _ => FileBits.rmpath (archive_path archive_root target_name)
2226
            
2227
    fun archive (target_path, exclusions) (project : project) =
2228
        let val _ = check_nonexistent target_path
2229
            val (packer, name) =
2230
                case packer_and_basename target_path of
2231
                    NONE => raise Fail ("Unsupported archive file extension in "
2232
                                        ^ target_path)
2233
                  | SOME pn => pn
2234
            val details =
2235
                case project_vcs_id_and_url (#rootpath (#context project)) of
2236
                    ERROR e => raise Fail e
2237
                  | OK details => details
2238
            val archive_root =
2239
                case make_archive_copy name details project of
2240
                    ERROR e => raise Fail e
2241
                  | OK archive_root => archive_root
2242
            val outcome = 
2243
                case update_archive archive_root name project of
2244
                    ERROR e => ERROR e
2245
                  | OK _ =>
2246
                    case pack_archive archive_root name
2247
                                      target_path packer exclusions of
2248
                        ERROR e => ERROR e
2249
                      | OK _ => OK ()
2250
        in
2251
            case outcome of
2252
                ERROR e => raise Fail e
2253
              | OK () => OS.Process.success
2254
        end
2255
            
2256
end
2257

    
2258
val libobjname = "libraries"
2259
                                             
2260
fun load_libspec spec_json lock_json libname : libspec =
2261
    let open JsonBits
2262
        val libobj   = lookup_mandatory spec_json [libobjname, libname]
2263
        val vcs      = lookup_mandatory_string libobj ["vcs"]
2264
        val retrieve = lookup_optional_string libobj
2265
        val service  = retrieve ["service"]
2266
        val owner    = retrieve ["owner"]
2267
        val repo     = retrieve ["repository"]
2268
        val url      = retrieve ["url"]
2269
        val branch   = retrieve ["branch"]
2270
        val project_pin = case retrieve ["pin"] of
2271
                              NONE => UNPINNED
2272
                            | SOME p => PINNED p
2273
        val lock_pin = case lookup_optional lock_json [libobjname, libname] of
2274
                           NONE => UNPINNED
2275
                         | SOME ll => case lookup_optional_string ll ["pin"] of
2276
                                          SOME p => PINNED p
2277
                                        | NONE => UNPINNED
2278
    in
2279
        {
2280
          libname = libname,
2281
          vcs = case vcs of
2282
                    "hg" => HG
2283
                  | "git" => GIT
2284
                  | "svn" => SVN
2285
                  | other => raise Fail ("Unknown version-control system \"" ^
2286
                                         other ^ "\""),
2287
          source = case (url, service, owner, repo) of
2288
                       (SOME u, NONE, _, _) => URL_SOURCE u
2289
                     | (NONE, SOME ss, owner, repo) =>
2290
                       SERVICE_SOURCE { service = ss, owner = owner, repo = repo }
2291
                     | _ => raise Fail ("Must have exactly one of service " ^
2292
                                        "or url string"),
2293
          project_pin = project_pin,
2294
          lock_pin = lock_pin,
2295
          branch = case branch of
2296
                       NONE => DEFAULT_BRANCH
2297
                     | SOME b => 
2298
                       case vcs of
2299
                           "svn" => raise Fail ("Branches not supported for " ^
2300
                                                "svn repositories; change " ^
2301
                                                "URL instead")
2302
                         | _ => BRANCH b
2303
        }
2304
    end  
2305

    
2306
fun load_userconfig () : userconfig =
2307
    let val home = FileBits.homedir ()
2308
        val conf_json = 
2309
            JsonBits.load_json_from
2310
                (OS.Path.joinDirFile {
2311
                      dir = home,
2312
                      file = VextFilenames.user_config_file })
2313
            handle IO.Io _ => Json.OBJECT []
2314
    in
2315
        {
2316
          accounts = case JsonBits.lookup_optional conf_json ["accounts"] of
2317
                         NONE => []
2318
                       | SOME (Json.OBJECT aa) =>
2319
                         map (fn (k, (Json.STRING v)) =>
2320
                                 { service = k, login = v }
2321
                             | _ => raise Fail
2322
                                          "String expected for account name")
2323
                             aa
2324
                       | _ => raise Fail "Array expected for accounts",
2325
          providers = Provider.load_providers conf_json
2326
        }
2327
    end
2328

    
2329
datatype pintype =
2330
         NO_LOCKFILE |
2331
         USE_LOCKFILE
2332
        
2333
fun load_project (userconfig : userconfig) rootpath pintype : project =
2334
    let val spec_file = FileBits.project_spec_path rootpath
2335
        val lock_file = FileBits.project_lock_path rootpath
2336
        val _ = if OS.FileSys.access (spec_file, [OS.FileSys.A_READ])
2337
                   handle OS.SysErr _ => false
2338
                then ()
2339
                else raise Fail ("Failed to open project spec file " ^
2340
                                 (VextFilenames.project_file) ^ " in " ^
2341
                                 rootpath ^
2342
                                 ".\nPlease ensure the spec file is in the " ^
2343
                                 "project root and run this from there.")
2344
        val spec_json = JsonBits.load_json_from spec_file
2345
        val lock_json = if pintype = USE_LOCKFILE
2346
                        then JsonBits.load_json_from lock_file
2347
                             handle IO.Io _ => Json.OBJECT []
2348
                        else Json.OBJECT []
2349
        val extdir = JsonBits.lookup_mandatory_string spec_json
2350
                                                      ["config", "extdir"]
2351
        val spec_libs = JsonBits.lookup_optional spec_json [libobjname]
2352
        val lock_libs = JsonBits.lookup_optional lock_json [libobjname]
2353
        val providers = Provider.load_more_providers
2354
                            (#providers userconfig) spec_json
2355
        val libnames = case spec_libs of
2356
                           NONE => []
2357
                         | SOME (Json.OBJECT ll) => map (fn (k, v) => k) ll
2358
                         | _ => raise Fail "Object expected for libs"
2359
    in
2360
        {
2361
          context = {
2362
            rootpath = rootpath,
2363
            extdir = extdir,
2364
            providers = providers,
2365
            accounts = #accounts userconfig
2366
          },
2367
          libs = map (load_libspec spec_json lock_json) libnames
2368
        }
2369
    end
2370

    
2371
fun save_lock_file rootpath locks =
2372
    let val lock_file = FileBits.project_lock_path rootpath
2373
        open Json
2374
        val lock_json =
2375
            OBJECT [
2376
                (libobjname,
2377
                 OBJECT (map (fn { libname, id_or_tag } =>
2378
                                 (libname,
2379
                                  OBJECT [ ("pin", STRING id_or_tag) ]))
2380
                             locks))
2381
            ]
2382
    in
2383
        JsonBits.save_json_to lock_file lock_json
2384
    end
2385
        
2386
fun pad_to n str =
2387
    if n <= String.size str then str
2388
    else pad_to n (str ^ " ")
2389

    
2390
fun hline_to 0 = ""
2391
  | hline_to n = "-" ^ hline_to (n-1)
2392

    
2393
val libname_width = 28
2394
val libstate_width = 11
2395
val localstate_width = 17
2396
val notes_width = 5
2397
val divider = " | "
2398
val clear_line = "\r" ^ pad_to 80 "";
2399

    
2400
fun print_status_header () =
2401
    print (clear_line ^ "\n " ^
2402
           pad_to libname_width "Library" ^ divider ^
2403
           pad_to libstate_width "State" ^ divider ^
2404
           pad_to localstate_width "Local" ^ divider ^
2405
           "Notes" ^ "\n " ^
2406
           hline_to libname_width ^ "-+-" ^
2407
           hline_to libstate_width ^ "-+-" ^
2408
           hline_to localstate_width ^ "-+-" ^
2409
           hline_to notes_width ^ "\n")
2410

    
2411
fun print_outcome_header () =
2412
    print (clear_line ^ "\n " ^
2413
           pad_to libname_width "Library" ^ divider ^
2414
           pad_to libstate_width "Outcome" ^ divider ^
2415
           "Notes" ^ "\n " ^
2416
           hline_to libname_width ^ "-+-" ^
2417
           hline_to libstate_width ^ "-+-" ^
2418
           hline_to notes_width ^ "\n")
2419
                        
2420
fun print_status with_network (libname, status) =
2421
    let val libstate_str =
2422
            case status of
2423
                OK (ABSENT, _) => "Absent"
2424
              | OK (CORRECT, _) => if with_network then "Correct" else "Present"
2425
              | OK (SUPERSEDED, _) => "Superseded"
2426
              | OK (WRONG, _) => "Wrong"
2427
              | ERROR _ => "Error"
2428
        val localstate_str =
2429
            case status of
2430
                OK (_, MODIFIED) => "Modified"
2431
              | OK (_, LOCK_MISMATCHED) => "Differs from Lock"
2432
              | OK (_, CLEAN) => "Clean"
2433
              | ERROR _ => ""
2434
        val error_str =
2435
            case status of
2436
                ERROR e => e
2437
              | _ => ""
2438
    in
2439
        print (" " ^
2440
               pad_to libname_width libname ^ divider ^
2441
               pad_to libstate_width libstate_str ^ divider ^
2442
               pad_to localstate_width localstate_str ^ divider ^
2443
               error_str ^ "\n")
2444
    end
2445

    
2446
fun print_update_outcome (libname, outcome) =
2447
    let val outcome_str =
2448
            case outcome of
2449
                OK id => "Ok"
2450
              | ERROR e => "Failed"
2451
        val error_str =
2452
            case outcome of
2453
                ERROR e => e
2454
              | _ => ""
2455
    in
2456
        print (" " ^
2457
               pad_to libname_width libname ^ divider ^
2458
               pad_to libstate_width outcome_str ^ divider ^
2459
               error_str ^ "\n")
2460
    end
2461

    
2462
fun act_and_print action print_header print_line (libs : libspec list) =
2463
    let val lines = map (fn lib => (#libname lib, action lib)) libs
2464
        val _ = print_header ()
2465
    in
2466
        app print_line lines;
2467
        lines
2468
    end
2469

    
2470
fun return_code_for outcomes =
2471
    foldl (fn ((_, result), acc) =>
2472
              case result of
2473
                  ERROR _ => OS.Process.failure
2474
                | _ => acc)
2475
          OS.Process.success
2476
          outcomes
2477
        
2478
fun status_of_project ({ context, libs } : project) =
2479
    return_code_for (act_and_print (AnyLibControl.status context)
2480
                                   print_status_header (print_status false)
2481
                                   libs)
2482
                                             
2483
fun review_project ({ context, libs } : project) =
2484
    return_code_for (act_and_print (AnyLibControl.review context)
2485
                                   print_status_header (print_status true)
2486
                                   libs)
2487

    
2488
fun lock_project ({ context, libs } : project) =
2489
    let val _ = if FileBits.verbose ()
2490
                then print ("Scanning IDs for lock file...\n")
2491
                else ()
2492
        val outcomes = map (fn lib =>
2493
                               (#libname lib, AnyLibControl.id_of context lib))
2494
                           libs
2495
        val locks =
2496
            List.concat
2497
                (map (fn (libname, result) =>
2498
                         case result of
2499
                             ERROR _ => []
2500
                           | OK id => [{ libname = libname, id_or_tag = id }])
2501
                     outcomes)
2502
        val return_code = return_code_for outcomes
2503
        val _ = print clear_line
2504
    in
2505
        if OS.Process.isSuccess return_code
2506
        then save_lock_file (#rootpath context) locks
2507
        else ();
2508
        return_code
2509
    end
2510

    
2511
fun update_project (project as { context, libs }) =
2512
    let val outcomes = act_and_print
2513
                           (AnyLibControl.update context)
2514
                           print_outcome_header print_update_outcome libs
2515
        val _ = if List.exists (fn (_, OK _) => true | _ => false) outcomes
2516
                then lock_project project
2517
                else OS.Process.success
2518
    in
2519
        return_code_for outcomes
2520
    end
2521
    
2522
fun load_local_project pintype =
2523
    let val userconfig = load_userconfig ()
2524
        val rootpath = OS.FileSys.getDir ()
2525
    in
2526
        load_project userconfig rootpath pintype
2527
    end    
2528

    
2529
fun with_local_project pintype f =
2530
    let val return_code = f (load_local_project pintype)
2531
                          handle e => (print ("Error: " ^ exnMessage e);
2532
                                       OS.Process.failure)
2533
        val _ = print "\n";
2534
    in
2535
        return_code
2536
    end
2537
        
2538
fun review () = with_local_project USE_LOCKFILE review_project
2539
fun status () = with_local_project USE_LOCKFILE status_of_project
2540
fun update () = with_local_project NO_LOCKFILE update_project
2541
fun lock () = with_local_project NO_LOCKFILE lock_project
2542
fun install () = with_local_project USE_LOCKFILE update_project
2543

    
2544
fun version () =
2545
    (print ("v" ^ vext_version ^ "\n");
2546
     OS.Process.success)
2547
                      
2548
fun usage () =
2549
    (print "\nVext ";
2550
     version ();
2551
     print ("\nA simple manager for third-party source code dependencies.\n\n"
2552
            ^ "Usage:\n\n"
2553
            ^ "  vext <command>\n\n"
2554
            ^ "where <command> is one of:\n\n"
2555
            ^ "  status   print quick report on local status only, without using network\n"
2556
            ^ "  review   check configured libraries against their providers, and report\n"
2557
            ^ "  install  update configured libraries according to project specs and lock file\n"
2558
            ^ "  update   update configured libraries and lock file according to project specs\n"
2559
            ^ "  lock     update lock file to match local library status\n"
2560
            ^ "  archive  pack up project and all libraries into an archive file\n"
2561
            ^ "           (invoke as 'vext archive target-file.tar.gz')\n"
2562
            ^ "  version  print the Vext version number and exit\n\n");
2563
    OS.Process.failure)
2564

    
2565
fun archive target args =
2566
    case args of
2567
        [] =>
2568
        with_local_project USE_LOCKFILE (Archive.archive (target, []))
2569
      | "--exclude"::xs =>
2570
        with_local_project USE_LOCKFILE (Archive.archive (target, xs))
2571
      | _ => usage ()
2572

    
2573
fun vext args =
2574
    let val return_code = 
2575
            case args of
2576
                ["review"] => review ()
2577
              | ["status"] => status ()
2578
              | ["install"] => install ()
2579
              | ["update"] => update ()
2580
              | ["lock"] => lock ()
2581
              | ["version"] => version ()
2582
              | "archive"::target::args => archive target args
2583
              | _ => usage ()
2584
    in
2585
        OS.Process.exit return_code;
2586
        ()
2587
    end
2588
        
2589
fun main () =
2590
    vext (CommandLine.arguments ())