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

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

root / repoint.sml @ 110:125520c3dc05

History | View | Annotate | Download (105 KB)

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

    
7
(* 
8
    Repoint
9

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

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

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

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

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

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

    
41
val repoint_version = "1.1"
42

    
43

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

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

    
57
type id_or_tag = string
58

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

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

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

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

    
87
type libname = string
88

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

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

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

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

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

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

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

    
146
    (** Check whether the given VCS is installed and working *)
147
    val is_working : context -> bool result
148
    
149
    (** Test whether the library is present locally at all *)
150
    val exists : context -> libname -> bool result
151
                                            
152
    (** Return the id (hash) of the current revision for the library *)
153
    val id_of : context -> libname -> id_or_tag result
154

    
155
    (** Test whether the library is at the given id *)
156
    val is_at : context -> libname * id_or_tag -> bool result
157

    
158
    (** Test whether the library is on the given branch, i.e. is at
159
        the branch tip or an ancestor of it *)
160
    val is_on_branch : context -> libname * branch -> bool result
161

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

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

    
174
    (** Test whether the library has been modified in the local
175
        working copy *)
176
    val is_modified_locally : context -> libname -> bool result
177

    
178
    (** Check out, i.e. clone a fresh copy of, the repo for the given
179
        library on the given branch *)
180
    val checkout : context -> libname * source * branch -> unit result
181

    
182
    (** Update the library to the given branch tip. Assumes that a
183
        local copy of the library already exists *)
184
    val update : context -> libname * source * branch -> unit result
185

    
186
    (** Update the library to the given specific id or tag *)
187
    val update_to : context -> libname * source * id_or_tag -> unit result
188

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

    
196
signature LIB_CONTROL = sig
197
    val review : context -> libspec -> (libstate * localstate) result
198
    val status : context -> libspec -> (libstate * localstate) result
199
    val update : context -> libspec -> unit result
200
    val id_of : context -> libspec -> id_or_tag result
201
    val is_working : context -> vcs -> bool result
202
end
203

    
204
structure FileBits :> sig
205
    val extpath : context -> string
206
    val libpath : context -> libname -> string
207
    val subpath : context -> libname -> string -> string
208
    val command_output : context -> libname -> string list -> string result
209
    val command : context -> libname -> string list -> unit result
210
    val file_url : string -> string
211
    val file_contents : string -> string
212
    val mydir : unit -> string
213
    val homedir : unit -> string
214
    val mkpath : string -> unit result
215
    val rmpath : string -> unit result
216
    val nonempty_dir_exists : string -> bool
217
    val project_spec_path : string -> string
218
    val project_lock_path : string -> string
219
    val project_completion_path : string -> string
220
    val verbose : unit -> bool
221
end = struct
222

    
223
    fun verbose () =
224
        case OS.Process.getEnv "REPOINT_VERBOSE" of
225
            SOME "0" => false
226
          | SOME _ => true
227
          | NONE => false
228

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

    
257
    fun libpath context "" =
258
        extpath context
259
      | libpath context libname =
260
        subpath context libname ""
261

    
262
    fun project_file_path rootpath filename =
263
        let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
264
        in OS.Path.toString {
265
                isAbs = isAbs,
266
                vol = vol,
267
                arcs = arcs @ [ filename ]
268
            }
269
        end
270
                
271
    fun project_spec_path rootpath =
272
        project_file_path rootpath (RepointFilenames.project_file)
273

    
274
    fun project_lock_path rootpath =
275
        project_file_path rootpath (RepointFilenames.project_lock_file)
276

    
277
    fun project_completion_path rootpath =
278
        project_file_path rootpath (RepointFilenames.project_completion_file)
279

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

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

    
339
    val tick_cycle = ref 0
340
    val tick_chars = Vector.fromList (map String.str (explode "|/-\\"))
341

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

    
376
    fun command context libname cmdlist =
377
        run_command context libname cmdlist NONE
378
            
379
    fun command_output context libname cmdlist =
380
        let open OS
381
            val tmpFile = FileSys.tmpName ()
382
            val result = run_command context libname cmdlist (SOME tmpFile)
383
            val contents = file_contents tmpFile
384
            val _ = if verbose ()
385
                    then print (">>> \"" ^ contents ^ "\"\n")
386
                    else ()
387
        in
388
            FileSys.remove tmpFile handle _ => ();
389
            case result of
390
                OK () => OK contents
391
              | ERROR e => ERROR e
392
        end
393

    
394
    fun mydir () =
395
        let open OS
396
            val { dir, file } = Path.splitDirFile (CommandLine.name ())
397
        in
398
            FileSys.realPath
399
                (if Path.isAbsolute dir
400
                 then dir
401
                 else Path.concat (FileSys.getDir (), dir))
402
        end
403

    
404
    fun homedir () =
405
        (* Failure is not routine, so we use an exception here *)
406
        case (OS.Process.getEnv "HOME",
407
              OS.Process.getEnv "HOMEPATH") of
408
            (SOME home, _) => home
409
          | (NONE, SOME home) => home
410
          | (NONE, NONE) =>
411
            raise Fail "Failed to look up home directory from environment"
412

    
413
    fun mkpath' path =
414
        if OS.FileSys.isDir path handle _ => false
415
        then OK ()
416
        else case OS.Path.fromString path of
417
                 { arcs = nil, ... } => OK ()
418
               | { isAbs = false, ... } => ERROR "mkpath requires absolute path"
419
               | { isAbs, vol, arcs } => 
420
                 case mkpath' (OS.Path.toString {      (* parent *)
421
                                    isAbs = isAbs,
422
                                    vol = vol,
423
                                    arcs = rev (tl (rev arcs)) }) of
424
                     ERROR e => ERROR e
425
                   | OK () => ((OS.FileSys.mkDir path; OK ())
426
                               handle OS.SysErr (e, _) =>
427
                                      ERROR ("Directory creation failed: " ^ e))
428

    
429
    fun mkpath path =
430
        mkpath' (OS.Path.mkCanonical path)
431

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

    
452
    fun rmpath' path =
453
        let open OS
454
            fun remove path =
455
                if FileSys.isLink path (* dangling links bother isDir *)
456
                then FileSys.remove path
457
                else if FileSys.isDir path
458
                then (app remove (dir_contents path); FileSys.rmDir path)
459
                else FileSys.remove path
460
        in
461
            (remove path; OK ())
462
            handle SysErr (e, _) => ERROR ("Path removal failed: " ^ e)
463
        end
464

    
465
    fun rmpath path =
466
        rmpath' (OS.Path.mkCanonical path)
467

    
468
    fun nonempty_dir_exists path =
469
        let open OS.FileSys
470
        in
471
            (not (isLink path) andalso
472
             isDir path andalso
473
             dir_contents path <> [])
474
            handle _ => false
475
        end                                        
476
                
477
end
478
                                         
479
functor LibControlFn (V: VCS_CONTROL) :> LIB_CONTROL = struct
480

    
481
    (* Valid states for unpinned libraries:
482

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

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

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

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

    
497
       Valid states for pinned libraries:
498

    
499
       - CORRECT: We are at the pinned revision.
500

    
501
       - WRONG: We are at any revision other than the pinned one.
502

    
503
       - ABSENT: Repo doesn't exist here at all.
504
    *)
505

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

    
556
    val review = check true
557
    val status = check false
558

    
559
    fun update context
560
               ({ libname, source, branch,
561
                  project_pin, lock_pin, ... } : libspec) =
562
        let fun update_unpinned () =
563
                case V.is_newest context (libname, source, branch) of
564
                    ERROR e => ERROR e
565
                  | OK true => OK ()
566
                  | OK false => V.update context (libname, source, branch)
567
            fun update_pinned target =
568
                case V.is_at context (libname, target) of
569
                    ERROR e => ERROR e
570
                  | OK true => OK ()
571
                  | OK false => V.update_to context (libname, source, target)
572
            fun update' () =
573
                case lock_pin of
574
                    PINNED target => update_pinned target
575
                  | UNPINNED =>
576
                    case project_pin of
577
                        PINNED target => update_pinned target
578
                      | UNPINNED => update_unpinned ()
579
        in
580
            case V.exists context libname of
581
                ERROR e => ERROR e
582
              | OK true => update' ()
583
              | OK false =>
584
                case V.checkout context (libname, source, branch) of
585
                    ERROR e => ERROR e
586
                  | OK () => update' ()
587
        end
588

    
589
    fun id_of context ({ libname, ... } : libspec) =
590
        V.id_of context libname
591

    
592
    fun is_working context vcs =
593
        V.is_working context
594
                
595
end
596

    
597
(* Simple Standard ML JSON parser
598
   https://bitbucket.org/cannam/sml-simplejson
599
   Copyright 2017 Chris Cannam. BSD licence.
600
   Parts based on the JSON parser in the Ponyo library by Phil Eaton.
601
*)
602

    
603
signature JSON = sig
604

    
605
    datatype json = OBJECT of (string * json) list
606
                  | ARRAY of json list
607
                  | NUMBER of real
608
                  | STRING of string
609
                  | BOOL of bool
610
                  | NULL
611

    
612
    datatype 'a result = OK of 'a
613
                       | ERROR of string
614

    
615
    val parse : string -> json result
616
    val serialise : json -> string
617
    val serialiseIndented : json -> string
618

    
619
end
620

    
621
structure Json :> JSON = struct
622

    
623
    datatype json = OBJECT of (string * json) list
624
                  | ARRAY of json list
625
                  | NUMBER of real
626
                  | STRING of string
627
                  | BOOL of bool
628
                  | NULL
629

    
630
    datatype 'a result = OK of 'a
631
                       | ERROR of string
632

    
633
    structure T = struct
634
        datatype token = NUMBER of char list
635
                       | STRING of string
636
                       | BOOL of bool
637
                       | NULL
638
                       | CURLY_L
639
                       | CURLY_R
640
                       | SQUARE_L
641
                       | SQUARE_R
642
                       | COLON
643
                       | COMMA
644

    
645
        fun toString t =
646
            case t of NUMBER digits => implode digits
647
                    | STRING s => s
648
                    | BOOL b => Bool.toString b
649
                    | NULL => "null"
650
                    | CURLY_L => "{"
651
                    | CURLY_R => "}"
652
                    | SQUARE_L => "["
653
                    | SQUARE_R => "]"
654
                    | COLON => ":"
655
                    | COMMA => ","
656
    end
657

    
658
    fun bmpToUtf8 cp =  (* convert a codepoint in Unicode BMP to utf8 bytes *)
659
        let open Word
660
	    infix 6 orb andb >>
661
        in
662
            map (Char.chr o toInt)
663
                (if cp < 0wx80 then
664
                     [cp]
665
                 else if cp < 0wx800 then
666
                     [0wxc0 orb (cp >> 0w6), 0wx80 orb (cp andb 0wx3f)]
667
                 else if cp < 0wx10000 then
668
                     [0wxe0 orb (cp >> 0w12),
669
                      0wx80 orb ((cp >> 0w6) andb 0wx3f),
670
		      0wx80 orb (cp andb 0wx3f)]
671
                 else raise Fail ("Invalid BMP point " ^ (Word.toString cp)))
672
        end
673
                      
674
    fun error pos text = ERROR (text ^ " at character position " ^
675
                                Int.toString (pos - 1))
676
    fun token_error pos = error pos ("Unexpected token")
677

    
678
    fun lexNull pos acc (#"u" :: #"l" :: #"l" :: xs) =
679
        lex (pos + 3) (T.NULL :: acc) xs
680
      | lexNull pos acc _ = token_error pos
681

    
682
    and lexTrue pos acc (#"r" :: #"u" :: #"e" :: xs) =
683
        lex (pos + 3) (T.BOOL true :: acc) xs
684
      | lexTrue pos acc _ = token_error pos
685

    
686
    and lexFalse pos acc (#"a" :: #"l" :: #"s" :: #"e" :: xs) =
687
        lex (pos + 4) (T.BOOL false :: acc) xs
688
      | lexFalse pos acc _ = token_error pos
689

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

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

    
773
    fun show [] = "end of input"
774
      | show (tok :: _) = T.toString tok
775

    
776
    fun parseNumber digits =
777
        (* Note lexNumber already case-insensitised the E for us *)
778
        let open Char
779

    
780
            fun okExpDigits [] = false
781
              | okExpDigits (c :: []) = isDigit c
782
              | okExpDigits (c :: cs) = isDigit c andalso okExpDigits cs
783

    
784
            fun okExponent [] = false
785
              | okExponent (#"+" :: cs) = okExpDigits cs
786
              | okExponent (#"-" :: cs) = okExpDigits cs
787
              | okExponent cc = okExpDigits cc
788

    
789
            fun okFracTrailing [] = true
790
              | okFracTrailing (c :: cs) =
791
                (isDigit c andalso okFracTrailing cs) orelse
792
                (c = #"e" andalso okExponent cs)
793

    
794
            fun okFraction [] = false
795
              | okFraction (c :: cs) =
796
                isDigit c andalso okFracTrailing cs
797

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

    
843
    and parseArray (T.SQUARE_R :: xs) = OK (ARRAY [], xs)
844
      | parseArray tokens =
845
        let fun parseArray' acc [] = ERROR "End of input during array"
846
              | parseArray' acc tokens =
847
                case parseTokens tokens of
848
                    ERROR e => ERROR e
849
                  | OK (j, T.COMMA :: xs) => parseArray' (j :: acc) xs
850
                  | OK (j, T.SQUARE_R :: xs) => OK (ARRAY (rev (j :: acc)), xs)
851
                  | OK (_, _) => ERROR "Expected , or ] after array element"
852
        in
853
            parseArray' [] tokens
854
        end
855

    
856
    and parseTokens [] = ERROR "Value expected"
857
      | parseTokens (tok :: xs) =
858
        (case tok of
859
             T.NUMBER d => (case parseNumber d of
860
                                OK r => OK (NUMBER r, xs)
861
                              | ERROR e => ERROR e)
862
           | T.STRING s => OK (STRING s, xs)
863
           | T.BOOL b   => OK (BOOL b, xs)
864
           | T.NULL     => OK (NULL, xs)
865
           | T.CURLY_L  => parseObject xs
866
           | T.SQUARE_L => parseArray xs
867
           | _ => ERROR ("Unexpected token " ^ T.toString tok ^
868
                         " before " ^ show xs))
869
                                   
870
    fun parse str =
871
        case lex 1 [] (explode str) of
872
           ERROR e => ERROR e
873
         | OK tokens => case parseTokens tokens of
874
                            OK (value, []) => OK value
875
                          | OK (_, _) => ERROR "Extra data after input"
876
                          | ERROR e => ERROR e
877

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

    
950

    
951
structure JsonBits :> sig
952
    exception Config of string
953
    val load_json_from : string -> Json.json (* filename -> json *)
954
    val save_json_to : string -> Json.json -> unit
955
    val lookup_optional : Json.json -> string list -> Json.json option
956
    val lookup_optional_string : Json.json -> string list -> string option
957
    val lookup_mandatory : Json.json -> string list -> Json.json
958
    val lookup_mandatory_string : Json.json -> string list -> string
959
end = struct
960

    
961
    exception Config of string
962

    
963
    fun load_json_from filename =
964
        case Json.parse (FileBits.file_contents filename) of
965
            Json.OK json => json
966
          | Json.ERROR e => raise Config ("Failed to parse file: " ^ e)
967

    
968
    fun save_json_to filename json =
969
        (* using binary I/O to avoid ever writing CR/LF line endings *)
970
        let val jstr = Json.serialiseIndented json
971
            val stream = BinIO.openOut filename
972
        in
973
            BinIO.output (stream, Byte.stringToBytes jstr);
974
            BinIO.closeOut stream
975
        end
976
                                  
977
    fun lookup_optional json kk =
978
        let fun lookup key =
979
                case json of
980
                    Json.OBJECT kvs =>
981
                    (case List.filter (fn (k, v) => k = key) kvs of
982
                         [] => NONE
983
                       | [(_,v)] => SOME v
984
                       | _ => raise Config ("Duplicate key: " ^ 
985
                                            (String.concatWith " -> " kk)))
986
                  | _ => raise Config "Object expected"
987
        in
988
            case kk of
989
                [] => NONE
990
              | key::[] => lookup key
991
              | key::kk => case lookup key of
992
                               NONE => NONE
993
                             | SOME j => lookup_optional j kk
994
        end
995
                       
996
    fun lookup_optional_string json kk =
997
        case lookup_optional json kk of
998
            SOME (Json.STRING s) => SOME s
999
          | SOME _ => raise Config ("Value (if present) must be string: " ^
1000
                                    (String.concatWith " -> " kk))
1001
          | NONE => NONE
1002

    
1003
    fun lookup_mandatory json kk =
1004
        case lookup_optional json kk of
1005
            SOME v => v
1006
          | NONE => raise Config ("Value is mandatory: " ^
1007
                                  (String.concatWith " -> " kk))
1008
                          
1009
    fun lookup_mandatory_string json kk =
1010
        case lookup_optional json kk of
1011
            SOME (Json.STRING s) => s
1012
          | _ => raise Config ("Value must be string: " ^
1013
                               (String.concatWith " -> " kk))
1014
end
1015

    
1016
structure Provider :> sig
1017
    val load_providers : Json.json -> provider list
1018
    val load_more_providers : provider list -> Json.json -> provider list
1019
    val remote_url : context -> vcs -> source -> libname -> string
1020
end = struct
1021

    
1022
    val known_providers : provider list =
1023
        [ {
1024
            service = "bitbucket",
1025
            supports = [HG, GIT],
1026
            remote_spec = {
1027
                anon = SOME "https://bitbucket.org/{owner}/{repository}",
1028
                auth = SOME "ssh://{vcs}@bitbucket.org/{owner}/{repository}"
1029
            }
1030
          },
1031
          {
1032
            service = "github",
1033
            supports = [GIT],
1034
            remote_spec = {
1035
                anon = SOME "https://github.com/{owner}/{repository}",
1036
                auth = SOME "ssh://{vcs}@github.com/{owner}/{repository}"
1037
            }
1038
          }
1039
        ]
1040

    
1041
    fun vcs_name vcs =
1042
        case vcs of HG => "hg"
1043
                  | GIT => "git"
1044
                  | SVN => "svn"
1045
                                             
1046
    fun vcs_from_name name =
1047
        case name of "hg" => HG
1048
                   | "git" => GIT 
1049
                   | "svn" => SVN
1050
                   | other => raise Fail ("Unknown vcs name \"" ^ name ^ "\"")
1051

    
1052
    fun load_more_providers previously_loaded json =
1053
        let open JsonBits
1054
            fun load pjson pname : provider =
1055
                {
1056
                  service = pname,
1057
                  supports =
1058
                  case lookup_mandatory pjson ["vcs"] of
1059
                      Json.ARRAY vv =>
1060
                      map (fn (Json.STRING v) => vcs_from_name v
1061
                          | _ => raise Fail "Strings expected in vcs array")
1062
                          vv
1063
                    | _ => raise Fail "Array expected for vcs",
1064
                  remote_spec = {
1065
                      anon = lookup_optional_string pjson ["anonymous"],
1066
                      auth = lookup_optional_string pjson ["authenticated"]
1067
                  }
1068
                }
1069
            val loaded = 
1070
                case lookup_optional json ["services"] of
1071
                    NONE => []
1072
                  | SOME (Json.OBJECT pl) => map (fn (k, v) => load v k) pl
1073
                  | _ => raise Fail "Object expected for services in config"
1074
            val newly_loaded =
1075
                List.filter (fn p => not (List.exists (fn pp => #service p =
1076
                                                                #service pp)
1077
                                                      previously_loaded))
1078
                            loaded
1079
        in
1080
            previously_loaded @ newly_loaded
1081
        end
1082

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

    
1147
    fun login_for ({ accounts, ... } : context) service =
1148
        case List.find (fn a => service = #service a) accounts of
1149
            SOME { login, ... } => SOME login
1150
          | NONE => NONE
1151

    
1152
    fun reponame_for path =
1153
        case String.tokens (fn c => c = #"/") path of
1154
            [] => raise Fail "Non-empty library path required"
1155
          | toks => hd (rev toks)
1156
                        
1157
    fun remote_url (context : context) vcs source libname =
1158
        case source of
1159
            URL_SOURCE u => u
1160
          | SERVICE_SOURCE { service, owner, repo } =>
1161
            provider_url { vcs = vcs,
1162
                           service = service,
1163
                           owner = owner,
1164
                           repo = case repo of
1165
                                      SOME r => r
1166
                                    | NONE => reponame_for libname }
1167
                         (login_for context service)
1168
                         (#providers context)
1169
end
1170

    
1171
structure HgControl :> VCS_CONTROL = struct
1172

    
1173
    (* Pulls always use an explicit URL, never just the default
1174
       remote, in order to ensure we update properly if the location
1175
       given in the project file changes. *)
1176

    
1177
    type vcsstate = { id: string, modified: bool,
1178
                      branch: string, tags: string list }
1179

    
1180
    val hg_program = "hg"
1181
                        
1182
    val hg_args = [ "--config", "ui.interactive=true",
1183
                    "--config", "ui.merge=:merge" ]
1184
                        
1185
    fun hg_command context libname args =
1186
        FileBits.command context libname (hg_program :: hg_args @ args)
1187

    
1188
    fun hg_command_output context libname args =
1189
        FileBits.command_output context libname (hg_program :: hg_args @ args)
1190

    
1191
    fun is_working context =
1192
        case hg_command_output context "" ["--version"] of
1193
            OK "" => OK false
1194
          | OK _ => OK true
1195
          | ERROR e => ERROR e
1196

    
1197
    fun exists context libname =
1198
        OK (OS.FileSys.isDir (FileBits.subpath context libname ".hg"))
1199
        handle _ => OK false
1200

    
1201
    fun remote_for context (libname, source) =
1202
        Provider.remote_url context HG source libname
1203

    
1204
    fun current_state context libname : vcsstate result =
1205
        let fun is_branch text = text <> "" andalso #"(" = hd (explode text)
1206
            and extract_branch b =
1207
                if is_branch b     (* need to remove enclosing parens *)
1208
                then (implode o rev o tl o rev o tl o explode) b
1209
                else "default"
1210
            and is_modified id = id <> "" andalso #"+" = hd (rev (explode id))
1211
            and extract_id id =
1212
                if is_modified id  (* need to remove trailing "+" *)
1213
                then (implode o rev o tl o rev o explode) id
1214
                else id
1215
            and split_tags tags = String.tokens (fn c => c = #"/") tags
1216
            and state_for (id, branch, tags) =
1217
                OK { id = extract_id id,
1218
                     modified = is_modified id,
1219
                     branch = extract_branch branch,
1220
                     tags = split_tags tags }
1221
        in        
1222
            case hg_command_output context libname ["id"] of
1223
                ERROR e => ERROR e
1224
              | OK out =>
1225
                case String.tokens (fn x => x = #" ") out of
1226
                    [id, branch, tags] => state_for (id, branch, tags)
1227
                  | [id, other] => if is_branch other
1228
                                   then state_for (id, other, "")
1229
                                   else state_for (id, "", other)
1230
                  | [id] => state_for (id, "", "")
1231
                  | _ => ERROR ("Unexpected output from hg id: " ^ out)
1232
        end
1233

    
1234
    fun branch_name branch = case branch of
1235
                                 DEFAULT_BRANCH => "default"
1236
                               | BRANCH "" => "default"
1237
                               | BRANCH b => b
1238

    
1239
    fun id_of context libname =
1240
        case current_state context libname of
1241
            ERROR e => ERROR e
1242
          | OK { id, ... } => OK id
1243

    
1244
    fun is_at context (libname, id_or_tag) =
1245
        case current_state context libname of
1246
            ERROR e => ERROR e
1247
          | OK { id, tags, ... } => 
1248
            OK (String.isPrefix id_or_tag id orelse
1249
                String.isPrefix id id_or_tag orelse
1250
                List.exists (fn t => t = id_or_tag) tags)
1251

    
1252
    fun is_on_branch context (libname, b) =
1253
        case current_state context libname of
1254
            ERROR e => ERROR e
1255
          | OK { branch, ... } => OK (branch = branch_name b)
1256
               
1257
    fun is_newest_locally context (libname, branch) =
1258
        case hg_command_output context libname
1259
                               ["log", "-l1",
1260
                                "-b", branch_name branch,
1261
                                "--template", "{node}"] of
1262
            ERROR e => OK false (* desired branch does not exist *)
1263
          | OK newest_in_repo => is_at context (libname, newest_in_repo)
1264

    
1265
    fun pull context (libname, source) =
1266
        let val url = remote_for context (libname, source)
1267
        in
1268
            hg_command context libname
1269
                       (if FileBits.verbose ()
1270
                        then ["pull", url]
1271
                        else ["pull", "-q", url])
1272
        end
1273

    
1274
    fun is_newest context (libname, source, branch) =
1275
        case is_newest_locally context (libname, branch) of
1276
            ERROR e => ERROR e
1277
          | OK false => OK false
1278
          | OK true =>
1279
            case pull context (libname, source) of
1280
                ERROR e => ERROR e
1281
              | _ => is_newest_locally context (libname, branch)
1282

    
1283
    fun is_modified_locally context libname =
1284
        case current_state context libname of
1285
            ERROR e => ERROR e
1286
          | OK { modified, ... } => OK modified
1287
                
1288
    fun checkout context (libname, source, branch) =
1289
        let val url = remote_for context (libname, source)
1290
        in
1291
            (* make the lib dir rather than just the ext dir, since
1292
               the lib dir might be nested and hg will happily check
1293
               out into an existing empty dir anyway *)
1294
            case FileBits.mkpath (FileBits.libpath context libname) of
1295
                ERROR e => ERROR e
1296
              | _ => hg_command context ""
1297
                                ["clone", "-u", branch_name branch,
1298
                                 url, libname]
1299
        end
1300
                                                    
1301
    fun update context (libname, source, branch) =
1302
        let val pull_result = pull context (libname, source)
1303
        in
1304
            case hg_command context libname ["update", branch_name branch] of
1305
                ERROR e => ERROR e
1306
              | _ =>
1307
                case pull_result of
1308
                    ERROR e => ERROR e
1309
                  | _ => OK ()
1310
        end
1311

    
1312
    fun update_to context (libname, _, "") =
1313
        ERROR "Non-empty id (tag or revision id) required for update_to"
1314
      | update_to context (libname, source, id) = 
1315
        let val pull_result = pull context (libname, source)
1316
        in
1317
            case hg_command context libname ["update", "-r", id] of
1318
                OK _ => OK ()
1319
              | ERROR e =>
1320
                case pull_result of
1321
                    ERROR e' => ERROR e' (* this was the ur-error *)
1322
                  | _ => ERROR e
1323
        end
1324

    
1325
    fun copy_url_for context libname =
1326
        OK (FileBits.file_url (FileBits.libpath context libname))
1327
            
1328
end
1329

    
1330
structure GitControl :> VCS_CONTROL = struct
1331

    
1332
    (* With Git repos we always operate in detached HEAD state. Even
1333
       the master branch is checked out using a remote reference
1334
       (repoint/master). The remote we use is always named repoint, and we
1335
       update it to the expected URL each time we fetch, in order to
1336
       ensure we update properly if the location given in the project
1337
       file changes. The origin remote is unused. *)
1338

    
1339
    val git_program = "git"
1340
                      
1341
    fun git_command context libname args =
1342
        FileBits.command context libname (git_program :: args)
1343

    
1344
    fun git_command_output context libname args =
1345
        FileBits.command_output context libname (git_program :: args)
1346

    
1347
    fun is_working context =
1348
        case git_command_output context "" ["--version"] of
1349
            OK "" => OK false
1350
          | OK _ => OK true
1351
          | ERROR e => ERROR e
1352
                            
1353
    fun exists context libname =
1354
        OK (OS.FileSys.isDir (FileBits.subpath context libname ".git"))
1355
        handle _ => OK false
1356

    
1357
    fun remote_for context (libname, source) =
1358
        Provider.remote_url context GIT source libname
1359

    
1360
    fun branch_name branch = case branch of
1361
                                 DEFAULT_BRANCH => "master"
1362
                               | BRANCH "" => "master"
1363
                               | BRANCH b => b
1364

    
1365
    val our_remote = "repoint"
1366
                                                 
1367
    fun remote_branch_name branch = our_remote ^ "/" ^ branch_name branch
1368

    
1369
    fun checkout context (libname, source, branch) =
1370
        let val url = remote_for context (libname, source)
1371
        in
1372
            (* make the lib dir rather than just the ext dir, since
1373
               the lib dir might be nested and git will happily check
1374
               out into an existing empty dir anyway *)
1375
            case FileBits.mkpath (FileBits.libpath context libname) of
1376
                OK () => git_command context ""
1377
                                     ["clone", "--origin", our_remote,
1378
                                      "--branch", branch_name branch,
1379
                                      url, libname]
1380
              | ERROR e => ERROR e
1381
        end
1382

    
1383
    fun add_our_remote context (libname, source) =
1384
        (* When we do the checkout ourselves (above), we add the
1385
           remote at the same time. But if the repo was cloned by
1386
           someone else, we'll need to do it after the fact. Git
1387
           doesn't seem to have a means to add a remote or change its
1388
           url if it already exists; seems we have to do this: *)
1389
        let val url = remote_for context (libname, source)
1390
        in
1391
            case git_command context libname
1392
                             ["remote", "set-url", our_remote, url] of
1393
                OK () => OK ()
1394
              | ERROR e => git_command context libname
1395
                                       ["remote", "add", "-f", our_remote, url]
1396
        end
1397

    
1398
    (* NB git rev-parse HEAD shows revision id of current checkout;
1399
       git rev-list -1 <tag> shows revision id of revision with that tag *)
1400

    
1401
    fun id_of context libname =
1402
        git_command_output context libname ["rev-parse", "HEAD"]
1403
            
1404
    fun is_at context (libname, id_or_tag) =
1405
        case id_of context libname of
1406
            ERROR e => OK false (* HEAD nonexistent, expected in empty repo *)
1407
          | OK id =>
1408
            if String.isPrefix id_or_tag id orelse
1409
               String.isPrefix id id_or_tag
1410
            then OK true
1411
            else is_at_tag context (libname, id, id_or_tag)
1412

    
1413
    and is_at_tag context (libname, id, tag) =
1414
        (* For annotated tags (with message) show-ref returns the tag
1415
           object ref rather than that of the revision being tagged;
1416
           we need the subsequent rev-list to chase that up. In fact
1417
           the rev-list on its own is enough to get us the id direct
1418
           from the tag name, but it fails with an error if the tag
1419
           doesn't exist, whereas we want to handle that quietly in
1420
           case the tag simply hasn't been pulled yet *)
1421
        case git_command_output context libname
1422
                                ["show-ref", "refs/tags/" ^ tag, "--"] of
1423
            OK "" => OK false (* Not a tag *)
1424
          | ERROR _ => OK false
1425
          | OK s =>
1426
            let val tag_ref = hd (String.tokens (fn c => c = #" ") s)
1427
            in
1428
                case git_command_output context libname
1429
                                        ["rev-list", "-1", tag_ref] of
1430
                    OK tagged => OK (id = tagged)
1431
                  | ERROR _ => OK false
1432
            end
1433
                           
1434
    fun branch_tip context (libname, branch) =
1435
        (* We don't have access to the source info or the network
1436
           here, as this is used by status (e.g. via is_on_branch) as
1437
           well as review. It's possible the remote branch won't exist,
1438
           e.g. if the repo was checked out by something other than
1439
           Repoint, and if that's the case, we can't add it here; we'll
1440
           just have to fail, since checking against local branches
1441
           instead could produce the wrong result. *)
1442
        git_command_output context libname
1443
                           ["rev-list", "-1",
1444
                            remote_branch_name branch, "--"]
1445
                       
1446
    fun is_newest_locally context (libname, branch) =
1447
        case branch_tip context (libname, branch) of
1448
            ERROR e => OK false
1449
          | OK rev => is_at context (libname, rev)
1450

    
1451
    fun is_on_branch context (libname, branch) =
1452
        case branch_tip context (libname, branch) of
1453
            ERROR e => OK false
1454
          | OK rev =>
1455
            case is_at context (libname, rev) of
1456
                ERROR e => ERROR e
1457
              | OK true => OK true
1458
              | OK false =>
1459
                case git_command context libname
1460
                                 ["merge-base", "--is-ancestor",
1461
                                  "HEAD", remote_branch_name branch] of
1462
                    ERROR e => OK false  (* cmd returns non-zero for no *)
1463
                  | _ => OK true
1464

    
1465
    fun fetch context (libname, source) =
1466
        case add_our_remote context (libname, source) of
1467
            ERROR e => ERROR e
1468
          | _ => git_command context libname ["fetch", our_remote]
1469
                            
1470
    fun is_newest context (libname, source, branch) =
1471
        case add_our_remote context (libname, source) of
1472
            ERROR e => ERROR e
1473
          | OK () => 
1474
            case is_newest_locally context (libname, branch) of
1475
                ERROR e => ERROR e
1476
              | OK false => OK false
1477
              | OK true =>
1478
                case fetch context (libname, source) of
1479
                    ERROR e => ERROR e
1480
                  | _ => is_newest_locally context (libname, branch)
1481

    
1482
    fun is_modified_locally context libname =
1483
        case git_command_output context libname ["status", "--porcelain"] of
1484
            ERROR e => ERROR e
1485
          | OK "" => OK false
1486
          | OK _ => OK true
1487

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

    
1495
    fun update context (libname, source, branch) =
1496
        case fetch context (libname, source) of
1497
            ERROR e => ERROR e
1498
          | _ =>
1499
            case git_command context libname ["checkout", "--detach",
1500
                                              remote_branch_name branch] of
1501
                ERROR e => ERROR e
1502
              | _ => OK ()
1503

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

    
1512
    fun update_to context (libname, _, "") = 
1513
        ERROR "Non-empty id (tag or revision id) required for update_to"
1514
      | update_to context (libname, source, id) =
1515
        let val fetch_result = fetch context (libname, source)
1516
        in
1517
            case git_command context libname ["checkout", "--detach", id] of
1518
                OK _ => OK ()
1519
              | ERROR e =>
1520
                case fetch_result of
1521
                    ERROR e' => ERROR e' (* this was the ur-error *)
1522
                  | _ => ERROR e
1523
        end
1524

    
1525
    fun copy_url_for context libname =
1526
        OK (FileBits.file_url (FileBits.libpath context libname))
1527
            
1528
end
1529

    
1530
(* SubXml - A parser for a subset of XML
1531
   https://bitbucket.org/cannam/sml-subxml
1532
   Copyright 2018 Chris Cannam. BSD licence.
1533
*)
1534

    
1535
signature SUBXML = sig
1536

    
1537
    datatype node = ELEMENT of { name : string, children : node list }
1538
                  | ATTRIBUTE of { name : string, value : string }
1539
                  | TEXT of string
1540
                  | CDATA of string
1541
                  | COMMENT of string
1542

    
1543
    datatype document = DOCUMENT of { name : string, children : node list }
1544

    
1545
    datatype 'a result = OK of 'a
1546
                       | ERROR of string
1547

    
1548
    val parse : string -> document result
1549
    val serialise : document -> string
1550
                                  
1551
end
1552

    
1553
structure SubXml :> SUBXML = struct
1554

    
1555
    datatype node = ELEMENT of { name : string, children : node list }
1556
                  | ATTRIBUTE of { name : string, value : string }
1557
                  | TEXT of string
1558
                  | CDATA of string
1559
                  | COMMENT of string
1560

    
1561
    datatype document = DOCUMENT of { name : string, children : node list }
1562

    
1563
    datatype 'a result = OK of 'a
1564
                       | ERROR of string
1565

    
1566
    structure T = struct
1567
        datatype token = ANGLE_L
1568
                       | ANGLE_R
1569
                       | ANGLE_SLASH_L
1570
                       | SLASH_ANGLE_R
1571
                       | EQUAL
1572
                       | NAME of string
1573
                       | TEXT of string
1574
                       | CDATA of string
1575
                       | COMMENT of string
1576

    
1577
        fun name t =
1578
            case t of ANGLE_L => "<"
1579
                    | ANGLE_R => ">"
1580
                    | ANGLE_SLASH_L => "</"
1581
                    | SLASH_ANGLE_R => "/>"
1582
                    | EQUAL => "="
1583
                    | NAME s => "name \"" ^ s ^ "\""
1584
                    | TEXT s => "text"
1585
                    | CDATA _ => "CDATA section"
1586
                    | COMMENT _ => "comment"
1587
    end
1588

    
1589
    structure Lex :> sig
1590
                  val lex : string -> T.token list result
1591
              end = struct
1592
                      
1593
        fun error pos text =
1594
            ERROR (text ^ " at character position " ^ Int.toString (pos-1))
1595
        fun tokenError pos token =
1596
            error pos ("Unexpected token '" ^ Char.toString token ^ "'")
1597

    
1598
        val nameEnd = explode " \t\n\r\"'</>!=?"
1599
                              
1600
        fun quoted quote pos acc cc =
1601
            let fun quoted' pos text [] =
1602
                    error pos "Document ends during quoted string"
1603
                  | quoted' pos text (x::xs) =
1604
                    if x = quote
1605
                    then OK (rev text, xs, pos+1)
1606
                    else quoted' (pos+1) (x::text) xs
1607
            in
1608
                case quoted' pos [] cc of
1609
                    ERROR e => ERROR e
1610
                  | OK (text, rest, newpos) =>
1611
                    inside newpos (T.TEXT (implode text) :: acc) rest
1612
            end
1613

    
1614
        and name first pos acc cc =
1615
            let fun name' pos text [] =
1616
                    error pos "Document ends during name"
1617
                  | name' pos text (x::xs) =
1618
                    if List.find (fn c => c = x) nameEnd <> NONE
1619
                    then OK (rev text, (x::xs), pos)
1620
                    else name' (pos+1) (x::text) xs
1621
            in
1622
                case name' (pos-1) [] (first::cc) of
1623
                    ERROR e => ERROR e
1624
                  | OK ([], [], pos) => error pos "Document ends before name"
1625
                  | OK ([], (x::xs), pos) => tokenError pos x
1626
                  | OK (text, rest, pos) =>
1627
                    inside pos (T.NAME (implode text) :: acc) rest
1628
            end
1629

    
1630
        and comment pos acc cc =
1631
            let fun comment' pos text cc =
1632
                    case cc of
1633
                        #"-" :: #"-" :: #">" :: xs => OK (rev text, xs, pos+3)
1634
                      | x :: xs => comment' (pos+1) (x::text) xs
1635
                      | [] => error pos "Document ends during comment"
1636
            in
1637
                case comment' pos [] cc of
1638
                    ERROR e => ERROR e
1639
                  | OK (text, rest, pos) => 
1640
                    outside pos (T.COMMENT (implode text) :: acc) rest
1641
            end
1642

    
1643
        and instruction pos acc cc =
1644
            case cc of
1645
                #"?" :: #">" :: xs => outside (pos+2) acc xs
1646
              | #">" :: _ => tokenError pos #">"
1647
              | x :: xs => instruction (pos+1) acc xs
1648
              | [] => error pos "Document ends during processing instruction"
1649

    
1650
        and cdata pos acc cc =
1651
            let fun cdata' pos text cc =
1652
                    case cc of
1653
                        #"]" :: #"]" :: #">" :: xs => OK (rev text, xs, pos+3)
1654
                      | x :: xs => cdata' (pos+1) (x::text) xs
1655
                      | [] => error pos "Document ends during CDATA section"
1656
            in
1657
                case cdata' pos [] cc of
1658
                    ERROR e => ERROR e
1659
                  | OK (text, rest, pos) =>
1660
                    outside pos (T.CDATA (implode text) :: acc) rest
1661
            end
1662
                
1663
        and doctype pos acc cc =
1664
            case cc of
1665
                #">" :: xs => outside (pos+1) acc xs
1666
              | x :: xs => doctype (pos+1) acc xs
1667
              | [] => error pos "Document ends during DOCTYPE"
1668

    
1669
        and declaration pos acc cc =
1670
            case cc of
1671
                #"-" :: #"-" :: xs =>
1672
                comment (pos+2) acc xs
1673
              | #"[" :: #"C" :: #"D" :: #"A" :: #"T" :: #"A" :: #"[" :: xs =>
1674
                cdata (pos+7) acc xs
1675
              | #"D" :: #"O" :: #"C" :: #"T" :: #"Y" :: #"P" :: #"E" :: xs =>
1676
                doctype (pos+7) acc xs
1677
              | [] => error pos "Document ends during declaration"
1678
              | _ => error pos "Unsupported declaration type"
1679

    
1680
        and left pos acc cc =
1681
            case cc of
1682
                #"/" :: xs => inside (pos+1) (T.ANGLE_SLASH_L :: acc) xs
1683
              | #"!" :: xs => declaration (pos+1) acc xs
1684
              | #"?" :: xs => instruction (pos+1) acc xs
1685
              | xs => inside pos (T.ANGLE_L :: acc) xs
1686

    
1687
        and slash pos acc cc =
1688
            case cc of
1689
                #">" :: xs => outside (pos+1) (T.SLASH_ANGLE_R :: acc) xs
1690
              | x :: _ => tokenError pos x
1691
              | [] => error pos "Document ends before element closed"
1692

    
1693
        and close pos acc xs = outside pos (T.ANGLE_R :: acc) xs
1694

    
1695
        and equal pos acc xs = inside pos (T.EQUAL :: acc) xs
1696

    
1697
        and outside pos acc [] = OK acc
1698
          | outside pos acc cc =
1699
            let fun textOf text = T.TEXT (implode (rev text))
1700
                fun outside' pos [] acc [] = OK acc
1701
                  | outside' pos text acc [] = OK (textOf text :: acc)
1702
                  | outside' pos text acc (x::xs) =
1703
                    case x of
1704
                        #"<" => if text = []
1705
                                then left (pos+1) acc xs
1706
                                else left (pos+1) (textOf text :: acc) xs
1707
                      | x => outside' (pos+1) (x::text) acc xs
1708
            in
1709
                outside' pos [] acc cc
1710
            end
1711
                
1712
        and inside pos acc [] = error pos "Document ends within tag"
1713
          | inside pos acc (#"<"::_) = tokenError pos #"<"
1714
          | inside pos acc (x::xs) =
1715
            (case x of
1716
                 #" " => inside | #"\t" => inside
1717
               | #"\n" => inside | #"\r" => inside
1718
               | #"\"" => quoted x | #"'" => quoted x
1719
               | #"/" => slash | #">" => close | #"=" => equal
1720
               | x => name x) (pos+1) acc xs
1721

    
1722
        fun lex str =
1723
            case outside 1 [] (explode str) of
1724
                ERROR e => ERROR e
1725
              | OK tokens => OK (rev tokens)
1726
    end
1727

    
1728
    structure Parse :> sig
1729
                  val parse : string -> document result
1730
              end = struct                            
1731
                  
1732
        fun show [] = "end of input"
1733
          | show (tok :: _) = T.name tok
1734

    
1735
        fun error toks text = ERROR (text ^ " before " ^ show toks)
1736

    
1737
        fun attribute elt name toks =
1738
            case toks of
1739
                T.EQUAL :: T.TEXT value :: xs =>
1740
                namedElement {
1741
                    name = #name elt,
1742
                    children = ATTRIBUTE { name = name, value = value } ::
1743
                               #children elt
1744
                } xs
1745
              | T.EQUAL :: xs => error xs "Expected attribute value"
1746
              | toks => error toks "Expected attribute assignment"
1747

    
1748
        and content elt toks =
1749
            case toks of
1750
                T.ANGLE_SLASH_L :: T.NAME n :: T.ANGLE_R :: xs =>
1751
                if n = #name elt
1752
                then OK (elt, xs)
1753
                else ERROR ("Closing tag </" ^ n ^ "> " ^
1754
                            "does not match opening <" ^ #name elt ^ ">")
1755
              | T.TEXT text :: xs =>
1756
                content {
1757
                    name = #name elt,
1758
                    children = TEXT text :: #children elt
1759
                } xs
1760
              | T.CDATA text :: xs =>
1761
                content {
1762
                    name = #name elt,
1763
                    children = CDATA text :: #children elt
1764
                } xs
1765
              | T.COMMENT text :: xs =>
1766
                content {
1767
                    name = #name elt,
1768
                    children = COMMENT text :: #children elt
1769
                } xs
1770
              | T.ANGLE_L :: xs =>
1771
                (case element xs of
1772
                     ERROR e => ERROR e
1773
                   | OK (child, xs) =>
1774
                     content {
1775
                         name = #name elt,
1776
                         children = ELEMENT child :: #children elt
1777
                     } xs)
1778
              | tok :: xs =>
1779
                error xs ("Unexpected token " ^ T.name tok)
1780
              | [] =>
1781
                ERROR ("Document ends within element \"" ^ #name elt ^ "\"")
1782
                       
1783
        and namedElement elt toks =
1784
            case toks of
1785
                T.SLASH_ANGLE_R :: xs => OK (elt, xs)
1786
              | T.NAME name :: xs => attribute elt name xs
1787
              | T.ANGLE_R :: xs => content elt xs
1788
              | x :: xs => error xs ("Unexpected token " ^ T.name x)
1789
              | [] => ERROR "Document ends within opening tag"
1790
                       
1791
        and element toks =
1792
            case toks of
1793
                T.NAME name :: xs =>
1794
                (case namedElement { name = name, children = [] } xs of
1795
                     ERROR e => ERROR e 
1796
                   | OK ({ name, children }, xs) =>
1797
                     OK ({ name = name, children = rev children }, xs))
1798
              | toks => error toks "Expected element name"
1799

    
1800
        and document [] = ERROR "Empty document"
1801
          | document (tok :: xs) =
1802
            case tok of
1803
                T.TEXT _ => document xs
1804
              | T.COMMENT _ => document xs
1805
              | T.ANGLE_L =>
1806
                (case element xs of
1807
                     ERROR e => ERROR e
1808
                   | OK (elt, []) => OK (DOCUMENT elt)
1809
                   | OK (elt, (T.TEXT _ :: xs)) => OK (DOCUMENT elt)
1810
                   | OK (elt, xs) => error xs "Extra data after document")
1811
              | _ => error xs ("Unexpected token " ^ T.name tok)
1812

    
1813
        fun parse str =
1814
            case Lex.lex str of
1815
                ERROR e => ERROR e
1816
              | OK tokens => document tokens
1817
    end
1818

    
1819
    structure Serialise :> sig
1820
                  val serialise : document -> string
1821
              end = struct
1822

    
1823
        fun attributes nodes =
1824
            String.concatWith
1825
                " "
1826
                (map node (List.filter
1827
                               (fn ATTRIBUTE _ => true | _ => false)
1828
                               nodes))
1829

    
1830
        and nonAttributes nodes =
1831
            String.concat
1832
                (map node (List.filter
1833
                               (fn ATTRIBUTE _ => false | _ => true)
1834
                               nodes))
1835
                
1836
        and node n =
1837
            case n of
1838
                TEXT string =>
1839
                string
1840
              | CDATA string =>
1841
                "<![CDATA[" ^ string ^ "]]>"
1842
              | COMMENT string =>
1843
                "<!-- " ^ string ^ "-->"
1844
              | ATTRIBUTE { name, value } =>
1845
                name ^ "=" ^ "\"" ^ value ^ "\"" (*!!!*)
1846
              | ELEMENT { name, children } =>
1847
                "<" ^ name ^
1848
                (case (attributes children) of
1849
                     "" => ""
1850
                   | s => " " ^ s) ^
1851
                (case (nonAttributes children) of
1852
                     "" => "/>"
1853
                   | s => ">" ^ s ^ "</" ^ name ^ ">")
1854
                              
1855
        fun serialise (DOCUMENT { name, children }) =
1856
            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" ^
1857
            node (ELEMENT { name = name, children = children })
1858
    end
1859

    
1860
    val parse = Parse.parse
1861
    val serialise = Serialise.serialise
1862
                        
1863
end
1864

    
1865

    
1866
structure SvnControl :> VCS_CONTROL = struct
1867

    
1868
    val svn_program = "svn"
1869

    
1870
    fun svn_command context libname args =
1871
        FileBits.command context libname (svn_program :: args)
1872

    
1873
    fun svn_command_output context libname args =
1874
        FileBits.command_output context libname (svn_program :: args)
1875

    
1876
    fun svn_command_lines context libname args =
1877
        case svn_command_output context libname args of
1878
            ERROR e => ERROR e
1879
          | OK s => OK (String.tokens (fn c => c = #"\n" orelse c = #"\r") s)
1880

    
1881
    fun split_line_pair line =
1882
        let fun strip_leading_ws str = case explode str of
1883
                                           #" "::rest => implode rest
1884
                                         | _ => str
1885
        in
1886
            case String.tokens (fn c => c = #":") line of
1887
                [] => ("", "")
1888
              | first::rest =>
1889
                (first, strip_leading_ws (String.concatWith ":" rest))
1890
        end
1891

    
1892
    fun is_working context =
1893
        case svn_command_output context "" ["--version"] of
1894
            OK "" => OK false
1895
          | OK _ => OK true
1896
          | ERROR e => ERROR e
1897

    
1898
    structure X = SubXml
1899
                      
1900
    fun svn_info context libname route =
1901
        (* SVN 1.9 has info --show-item which is just what we need,
1902
           but at this point we still have 1.8 on the CI boxes so we
1903
           might as well aim to support it. For that we really have to
1904
           use the XML output format, since the default info output is
1905
           localised. This is the only thing our mini-XML parser is
1906
           used for though, so it would be good to trim it at some
1907
           point *)
1908
        let fun find elt [] = OK elt
1909
              | find { children, ... } (first :: rest) =
1910
                case List.find (fn (X.ELEMENT { name, ... }) => name = first
1911
                               | _ => false)
1912
                               children of
1913
                    NONE => ERROR ("No element \"" ^ first ^ "\" in SVN XML")
1914
                  | SOME (X.ELEMENT e) => find e rest
1915
                  | SOME _ => ERROR "Internal error"
1916
        in
1917
            case svn_command_output context libname ["info", "--xml"] of
1918
                ERROR e => ERROR e
1919
              | OK xml =>
1920
                case X.parse xml of
1921
                    X.ERROR e => ERROR e
1922
                  | X.OK (X.DOCUMENT doc) => find doc route
1923
        end
1924
            
1925
    fun exists context libname =
1926
        OK (OS.FileSys.isDir (FileBits.subpath context libname ".svn"))
1927
        handle _ => OK false
1928

    
1929
    fun remote_for context (libname, source) =
1930
        Provider.remote_url context SVN source libname
1931

    
1932
    (* Remote the checkout came from, not necessarily the one we want *)
1933
    fun actual_remote_for context libname =
1934
        case svn_info context libname ["entry", "url"] of
1935
            ERROR e => ERROR e
1936
          | OK { children, ... } =>
1937
            case List.find (fn (X.TEXT _) => true | _ => false) children of
1938
                NONE => ERROR "No content for URL in SVN info XML"
1939
              | SOME (X.TEXT url) => OK url
1940
              | SOME _ => ERROR "Internal error"
1941

    
1942
    fun id_of context libname =
1943
        case svn_info context libname ["entry"] of
1944
            ERROR e => ERROR e
1945
          | OK { children, ... } => 
1946
            case List.find
1947
                     (fn (X.ATTRIBUTE { name = "revision", ... }) => true
1948
                     | _ => false)
1949
                     children of
1950
                NONE => ERROR "No revision for entry in SVN info XML"
1951
              | SOME (X.ATTRIBUTE { value, ... }) => OK value
1952
              | SOME _ => ERROR "Internal error"
1953

    
1954
    fun is_at context (libname, id_or_tag) =
1955
        case id_of context libname of
1956
            ERROR e => ERROR e
1957
          | OK id => OK (id = id_or_tag)
1958

    
1959
    fun is_on_branch context (libname, b) =
1960
        OK (b = DEFAULT_BRANCH)
1961

    
1962
    fun check_remote context (libname, source) =
1963
      case (remote_for context (libname, source),
1964
            actual_remote_for context libname) of
1965
          (_, ERROR e) => ERROR e
1966
        | (url, OK actual) => 
1967
          if actual = url
1968
          then OK ()
1969
          else svn_command context libname ["relocate", url]
1970
               
1971
    fun is_newest context (libname, source, branch) =
1972
        case check_remote context (libname, source) of
1973
            ERROR e => ERROR e
1974
          | OK () => 
1975
            case svn_command_lines context libname
1976
                                   ["status", "--show-updates"] of
1977
                ERROR e => ERROR e
1978
              | OK lines =>
1979
                case rev lines of
1980
                    [] => ERROR "No result returned for server status"
1981
                  | last_line::_ =>
1982
                    case rev (String.tokens (fn c => c = #" ") last_line) of
1983
                        [] => ERROR "No revision field found in server status"
1984
                      | server_id::_ => is_at context (libname, server_id)
1985

    
1986
    fun is_newest_locally context (libname, branch) =
1987
        OK true (* no local history *)
1988

    
1989
    fun is_modified_locally context libname =
1990
        case svn_command_output context libname ["status"] of
1991
            ERROR e => ERROR e
1992
          | OK "" => OK false
1993
          | OK _ => OK true
1994

    
1995
    fun checkout context (libname, source, branch) =
1996
        let val url = remote_for context (libname, source)
1997
            val path = FileBits.libpath context libname
1998
        in
1999
            if FileBits.nonempty_dir_exists path
2000
            then (* Surprisingly, SVN itself has no problem with
2001
                    this. But for consistency with other VCSes we 
2002
                    don't allow it *)
2003
                ERROR ("Refusing checkout to nonempty dir \"" ^ path ^ "\"")
2004
            else 
2005
                (* make the lib dir rather than just the ext dir, since
2006
                   the lib dir might be nested and svn will happily check
2007
                   out into an existing empty dir anyway *)
2008
                case FileBits.mkpath (FileBits.libpath context libname) of
2009
                    ERROR e => ERROR e
2010
                  | _ => svn_command context "" ["checkout", url, libname]
2011
        end
2012
                                                    
2013
    fun update context (libname, source, branch) =
2014
        case check_remote context (libname, source) of
2015
            ERROR e => ERROR e
2016
          | OK () => 
2017
            case svn_command context libname
2018
                             ["update", "--accept", "postpone"] of
2019
                ERROR e => ERROR e
2020
              | _ => OK ()
2021

    
2022
    fun update_to context (libname, _, "") =
2023
        ERROR "Non-empty id (tag or revision id) required for update_to"
2024
      | update_to context (libname, source, id) = 
2025
        case check_remote context (libname, source) of
2026
            ERROR e => ERROR e
2027
          | OK () => 
2028
            case svn_command context libname
2029
                             ["update", "-r", id, "--accept", "postpone"] of
2030
                ERROR e => ERROR e
2031
              | OK _ => OK ()
2032

    
2033
    fun copy_url_for context libname =
2034
        actual_remote_for context libname
2035

    
2036
end
2037

    
2038
structure AnyLibControl :> LIB_CONTROL = struct
2039

    
2040
    structure H = LibControlFn(HgControl)
2041
    structure G = LibControlFn(GitControl)
2042
    structure S = LibControlFn(SvnControl)
2043

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

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

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

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

    
2056
    fun is_working context vcs =
2057
        (fn HG => H.is_working | GIT => G.is_working | SVN => S.is_working)
2058
            vcs context vcs
2059

    
2060
end
2061

    
2062

    
2063
type exclusions = string list
2064
              
2065
structure Archive :> sig
2066

    
2067
    val archive : string * exclusions -> project -> OS.Process.status
2068
        
2069
end = struct
2070

    
2071
    (* The idea of "archive" is to replace hg/git archive, which won't
2072
       include files, like the Repoint-introduced external libraries,
2073
       that are not under version control with the main repo.
2074

    
2075
       The process goes like this:
2076

    
2077
       - Make sure we have a target filename from the user, and take
2078
         its basename as our archive directory name
2079

    
2080
       - Make an "archive root" subdir of the project repo, named
2081
         typically .repoint-archive
2082
       
2083
       - Identify the VCS used for the project repo. Note that any
2084
         explicit references to VCS type in this structure are to
2085
         the VCS used for the project (something Repoint doesn't 
2086
         otherwise care about), not for an individual library
2087

    
2088
       - Synthesise a Repoint project with the archive root as its
2089
         root path, "." as its extdir, with one library whose
2090
         name is the user-supplied basename and whose explicit
2091
         source URL is the original project root; update that
2092
         project -- thus cloning the original project to a subdir
2093
         of the archive root
2094

    
2095
       - Synthesise a Repoint project identical to the original one for
2096
         this project, but with the newly-cloned copy as its root
2097
         path; update that project -- thus checking out clean copies
2098
         of the external library dirs
2099

    
2100
       - Call out to an archive program to archive up the new copy,
2101
         running e.g.
2102
         tar cvzf project-release.tar.gz \
2103
             --exclude=.hg --exclude=.git project-release
2104
         in the archive root dir
2105

    
2106
       - (We also omit the repoint-project.json file and any trace of
2107
         Repoint. It can't properly be run in a directory where the
2108
         external project folders already exist but their repo history
2109
         does not. End users shouldn't get to see Repoint)
2110

    
2111
       - Clean up by deleting the new copy
2112
    *)
2113

    
2114
    fun project_vcs_id_and_url dir =
2115
        let val context = {
2116
                rootpath = dir,
2117
                extdir = ".",
2118
                providers = [],
2119
                accounts = []
2120
            }
2121
            val vcs_maybe = 
2122
                case [HgControl.exists context ".",
2123
                      GitControl.exists context ".",
2124
                      SvnControl.exists context "."] of
2125
                    [OK true, OK false, OK false] => OK HG
2126
                  | [OK false, OK true, OK false] => OK GIT
2127
                  | [OK false, OK false, OK true] => OK SVN
2128
                  | _ => ERROR ("Unable to identify VCS for directory " ^ dir)
2129
        in
2130
            case vcs_maybe of
2131
                ERROR e => ERROR e
2132
              | OK vcs =>
2133
                case (fn HG => HgControl.id_of
2134
                       | GIT => GitControl.id_of 
2135
                       | SVN => SvnControl.id_of)
2136
                         vcs context "." of
2137
                    ERROR e => ERROR ("Unable to find id of project repo: " ^ e)
2138
                  | OK id =>
2139
                    case (fn HG => HgControl.copy_url_for
2140
                           | GIT => GitControl.copy_url_for
2141
                           | SVN => SvnControl.copy_url_for)
2142
                             vcs context "." of
2143
                        ERROR e => ERROR ("Unable to find URL of project repo: "
2144
                                          ^ e)
2145
                      | OK url => OK (vcs, id, url)
2146
        end
2147
            
2148
    fun make_archive_root (context : context) =
2149
        let val path = OS.Path.joinDirFile {
2150
                    dir = #rootpath context,
2151
                    file = RepointFilenames.archive_dir
2152
                }
2153
        in
2154
            case FileBits.mkpath path of
2155
                ERROR e => raise Fail ("Failed to create archive directory \""
2156
                                       ^ path ^ "\": " ^ e)
2157
              | OK () => path
2158
        end
2159

    
2160
    fun archive_path archive_dir target_name =
2161
        OS.Path.joinDirFile {
2162
            dir = archive_dir,
2163
            file = target_name
2164
        }
2165

    
2166
    fun check_nonexistent path =
2167
        case SOME (OS.FileSys.fileSize path) handle OS.SysErr _ => NONE of
2168
            NONE => ()
2169
          | _ => raise Fail ("Path " ^ path ^ " exists, not overwriting")
2170
            
2171
    fun make_archive_copy target_name (vcs, project_id, source_url)
2172
                          ({ context, ... } : project) =
2173
        let val archive_root = make_archive_root context
2174
            val synthetic_context = {
2175
                rootpath = archive_root,
2176
                extdir = ".",
2177
                providers = [],
2178
                accounts = []
2179
            }
2180
            val synthetic_library = {
2181
                libname = target_name,
2182
                vcs = vcs,
2183
                source = URL_SOURCE source_url,
2184
                branch = DEFAULT_BRANCH, (* overridden by pinned id below *)
2185
                project_pin = PINNED project_id,
2186
                lock_pin = PINNED project_id
2187
            }
2188
            val path = archive_path archive_root target_name
2189
            val _ = print ("Cloning original project to " ^ path
2190
                           ^ " at revision " ^ project_id ^ "...\n");
2191
            val _ = check_nonexistent path
2192
        in
2193
            case AnyLibControl.update synthetic_context synthetic_library of
2194
                ERROR e => ERROR ("Failed to clone original project to "
2195
                                  ^ path ^ ": " ^ e)
2196
              | OK _ => OK archive_root
2197
        end
2198

    
2199
    fun update_archive archive_root target_name
2200
                       (project as { context, ... } : project) =
2201
        let val synthetic_context = {
2202
                rootpath = archive_path archive_root target_name,
2203
                extdir = #extdir context,
2204
                providers = #providers context,
2205
                accounts = #accounts context
2206
            }
2207
        in
2208
            foldl (fn (lib, acc) =>
2209
                      case acc of
2210
                          ERROR e => ERROR e
2211
                        | OK () => AnyLibControl.update synthetic_context lib)
2212
                  (OK ())
2213
                  (#libs project)
2214
        end
2215

    
2216
    datatype packer = TAR
2217
                    | TAR_GZ
2218
                    | TAR_BZ2
2219
                    | TAR_XZ
2220
    (* could add other packers, e.g. zip, if we knew how to
2221
       handle the file omissions etc properly in pack_archive *)
2222
                          
2223
    fun packer_and_basename path =
2224
        let val extensions = [ (".tar", TAR),
2225
                               (".tar.gz", TAR_GZ),
2226
                               (".tar.bz2", TAR_BZ2),
2227
                               (".tar.xz", TAR_XZ)]
2228
            val filename = OS.Path.file path
2229
        in
2230
            foldl (fn ((ext, packer), acc) =>
2231
                      if String.isSuffix ext filename
2232
                      then SOME (packer,
2233
                                 String.substring (filename, 0,
2234
                                                   String.size filename -
2235
                                                   String.size ext))
2236
                      else acc)
2237
                  NONE
2238
                  extensions
2239
        end
2240
            
2241
    fun pack_archive archive_root target_name target_path packer exclusions =
2242
        case FileBits.command {
2243
                rootpath = archive_root,
2244
                extdir = ".",
2245
                providers = [],
2246
                accounts = []
2247
            } "" ([
2248
                     "tar",
2249
                     case packer of
2250
                         TAR => "cf"
2251
                       | TAR_GZ => "czf"
2252
                       | TAR_BZ2 => "cjf"
2253
                       | TAR_XZ => "cJf",
2254
                     target_path,
2255
                     "--exclude=.hg",
2256
                     "--exclude=.git",
2257
                     "--exclude=.svn",
2258
                     "--exclude=repoint",
2259
                     "--exclude=repoint.sml",
2260
                     "--exclude=repoint.ps1",
2261
                     "--exclude=repoint.bat",
2262
                     "--exclude=repoint-project.json",
2263
                     "--exclude=repoint-lock.json"
2264
                 ] @ (map (fn e => "--exclude=" ^ e) exclusions) @
2265
                  [ target_name ])
2266
         of
2267
            ERROR e => ERROR e
2268
          | OK _ => FileBits.rmpath (archive_path archive_root target_name)
2269
            
2270
    fun archive (target_path, exclusions) (project : project) =
2271
        let val _ = check_nonexistent target_path
2272
            val (packer, name) =
2273
                case packer_and_basename target_path of
2274
                    NONE => raise Fail ("Unsupported archive file extension in "
2275
                                        ^ target_path)
2276
                  | SOME pn => pn
2277
            val details =
2278
                case project_vcs_id_and_url (#rootpath (#context project)) of
2279
                    ERROR e => raise Fail e
2280
                  | OK details => details
2281
            val archive_root =
2282
                case make_archive_copy name details project of
2283
                    ERROR e => raise Fail e
2284
                  | OK archive_root => archive_root
2285
            val outcome = 
2286
                case update_archive archive_root name project of
2287
                    ERROR e => ERROR e
2288
                  | OK _ =>
2289
                    case pack_archive archive_root name
2290
                                      target_path packer exclusions of
2291
                        ERROR e => ERROR e
2292
                      | OK _ => OK ()
2293
        in
2294
            case outcome of
2295
                ERROR e => raise Fail e
2296
              | OK () => OS.Process.success
2297
        end
2298
            
2299
end
2300

    
2301
val libobjname = "libraries"
2302
                                             
2303
fun load_libspec spec_json lock_json libname : libspec =
2304
    let open JsonBits
2305
        val libobj   = lookup_mandatory spec_json [libobjname, libname]
2306
        val vcs      = lookup_mandatory_string libobj ["vcs"]
2307
        val retrieve = lookup_optional_string libobj
2308
        val service  = retrieve ["service"]
2309
        val owner    = retrieve ["owner"]
2310
        val repo     = retrieve ["repository"]
2311
        val url      = retrieve ["url"]
2312
        val branch   = retrieve ["branch"]
2313
        val project_pin = case retrieve ["pin"] of
2314
                              NONE => UNPINNED
2315
                            | SOME p => PINNED p
2316
        val lock_pin = case lookup_optional lock_json [libobjname, libname] of
2317
                           NONE => UNPINNED
2318
                         | SOME ll => case lookup_optional_string ll ["pin"] of
2319
                                          SOME p => PINNED p
2320
                                        | NONE => UNPINNED
2321
    in
2322
        {
2323
          libname = libname,
2324
          vcs = case vcs of
2325
                    "hg" => HG
2326
                  | "git" => GIT
2327
                  | "svn" => SVN
2328
                  | other => raise Fail ("Unknown version-control system \"" ^
2329
                                         other ^ "\""),
2330
          source = case (url, service, owner, repo) of
2331
                       (SOME u, NONE, _, _) => URL_SOURCE u
2332
                     | (NONE, SOME ss, owner, repo) =>
2333
                       SERVICE_SOURCE { service = ss, owner = owner, repo = repo }
2334
                     | _ => raise Fail ("Must have exactly one of service " ^
2335
                                        "or url string"),
2336
          project_pin = project_pin,
2337
          lock_pin = lock_pin,
2338
          branch = case branch of
2339
                       NONE => DEFAULT_BRANCH
2340
                     | SOME b => 
2341
                       case vcs of
2342
                           "svn" => raise Fail ("Branches not supported for " ^
2343
                                                "svn repositories; change " ^
2344
                                                "URL instead")
2345
                         | _ => BRANCH b
2346
        }
2347
    end  
2348

    
2349
fun load_userconfig () : userconfig =
2350
    let val home = FileBits.homedir ()
2351
        val conf_json = 
2352
            JsonBits.load_json_from
2353
                (OS.Path.joinDirFile {
2354
                      dir = home,
2355
                      file = RepointFilenames.user_config_file })
2356
            handle IO.Io _ => Json.OBJECT []
2357
    in
2358
        {
2359
          accounts = case JsonBits.lookup_optional conf_json ["accounts"] of
2360
                         NONE => []
2361
                       | SOME (Json.OBJECT aa) =>
2362
                         map (fn (k, (Json.STRING v)) =>
2363
                                 { service = k, login = v }
2364
                             | _ => raise Fail
2365
                                          "String expected for account name")
2366
                             aa
2367
                       | _ => raise Fail "Array expected for accounts",
2368
          providers = Provider.load_providers conf_json
2369
        }
2370
    end
2371

    
2372
datatype pintype =
2373
         NO_LOCKFILE |
2374
         USE_LOCKFILE
2375
        
2376
fun load_project (userconfig : userconfig) rootpath pintype : project =
2377
    let val spec_file = FileBits.project_spec_path rootpath
2378
        val lock_file = FileBits.project_lock_path rootpath
2379
        val _ = if OS.FileSys.access (spec_file, [OS.FileSys.A_READ])
2380
                   handle OS.SysErr _ => false
2381
                then ()
2382
                else raise Fail ("Failed to open project spec file " ^
2383
                                 (RepointFilenames.project_file) ^ " in " ^
2384
                                 rootpath ^
2385
                                 ".\nPlease ensure the spec file is in the " ^
2386
                                 "project root and run this from there.")
2387
        val spec_json = JsonBits.load_json_from spec_file
2388
        val lock_json = if pintype = USE_LOCKFILE
2389
                        then JsonBits.load_json_from lock_file
2390
                             handle IO.Io _ => Json.OBJECT []
2391
                        else Json.OBJECT []
2392
        val extdir = JsonBits.lookup_mandatory_string spec_json
2393
                                                      ["config", "extdir"]
2394
        val spec_libs = JsonBits.lookup_optional spec_json [libobjname]
2395
        val lock_libs = JsonBits.lookup_optional lock_json [libobjname]
2396
        val providers = Provider.load_more_providers
2397
                            (#providers userconfig) spec_json
2398
        val libnames = case spec_libs of
2399
                           NONE => []
2400
                         | SOME (Json.OBJECT ll) => map (fn (k, v) => k) ll
2401
                         | _ => raise Fail "Object expected for libs"
2402
    in
2403
        {
2404
          context = {
2405
            rootpath = rootpath,
2406
            extdir = extdir,
2407
            providers = providers,
2408
            accounts = #accounts userconfig
2409
          },
2410
          libs = map (load_libspec spec_json lock_json) libnames
2411
        }
2412
    end
2413

    
2414
fun save_lock_file rootpath locks =
2415
    let val lock_file = FileBits.project_lock_path rootpath
2416
        open Json
2417
        val lock_json =
2418
            OBJECT [
2419
                (libobjname,
2420
                 OBJECT (map (fn { libname, id_or_tag } =>
2421
                                 (libname,
2422
                                  OBJECT [ ("pin", STRING id_or_tag) ]))
2423
                             locks))
2424
            ]
2425
    in
2426
        JsonBits.save_json_to lock_file lock_json
2427
    end
2428

    
2429
fun checkpoint_completion_file rootpath =
2430
    let val completion_file = FileBits.project_completion_path rootpath
2431
        val stream = TextIO.openOut completion_file
2432
    in
2433
        TextIO.closeOut stream
2434
    end
2435
                                                               
2436
fun pad_to n str =
2437
    if n <= String.size str then str
2438
    else pad_to n (str ^ " ")
2439

    
2440
fun hline_to 0 = ""
2441
  | hline_to n = "-" ^ hline_to (n-1)
2442

    
2443
val libname_width = 28
2444
val libstate_width = 11
2445
val localstate_width = 17
2446
val notes_width = 5
2447
val divider = " | "
2448
val clear_line = "\r" ^ pad_to 80 "";
2449

    
2450
fun print_status_header () =
2451
    print (clear_line ^ "\n " ^
2452
           pad_to libname_width "Library" ^ divider ^
2453
           pad_to libstate_width "State" ^ divider ^
2454
           pad_to localstate_width "Local" ^ divider ^
2455
           "Notes" ^ "\n " ^
2456
           hline_to libname_width ^ "-+-" ^
2457
           hline_to libstate_width ^ "-+-" ^
2458
           hline_to localstate_width ^ "-+-" ^
2459
           hline_to notes_width ^ "\n")
2460

    
2461
fun print_outcome_header () =
2462
    print (clear_line ^ "\n " ^
2463
           pad_to libname_width "Library" ^ divider ^
2464
           pad_to libstate_width "Outcome" ^ divider ^
2465
           "Notes" ^ "\n " ^
2466
           hline_to libname_width ^ "-+-" ^
2467
           hline_to libstate_width ^ "-+-" ^
2468
           hline_to notes_width ^ "\n")
2469
                        
2470
fun print_status with_network (lib : libspec, status) =
2471
    let val libstate_str =
2472
            case status of
2473
                OK (ABSENT, _) => "Absent"
2474
              | OK (CORRECT, _) => if with_network then "Correct" else "Present"
2475
              | OK (SUPERSEDED, _) => "Superseded"
2476
              | OK (WRONG, _) => "Wrong"
2477
              | ERROR _ => "Error"
2478
        val localstate_str =
2479
            case status of
2480
                OK (_, MODIFIED) => "Modified"
2481
              | OK (_, LOCK_MISMATCHED) => "Differs from Lock"
2482
              | OK (_, CLEAN) => "Clean"
2483
              | ERROR _ => ""
2484
        val error_str =
2485
            case status of
2486
                ERROR e => e
2487
              | _ => ""
2488
    in
2489
        print (" " ^
2490
               pad_to libname_width (#libname lib) ^ divider ^
2491
               pad_to libstate_width libstate_str ^ divider ^
2492
               pad_to localstate_width localstate_str ^ divider ^
2493
               error_str ^ "\n")
2494
    end
2495

    
2496
fun print_update_outcome (lib : libspec, outcome) =
2497
    let val outcome_str =
2498
            case outcome of
2499
                OK id => "Ok"
2500
              | ERROR e => "Failed"
2501
        val error_str =
2502
            case outcome of
2503
                ERROR e => e
2504
              | _ => ""
2505
    in
2506
        print (" " ^
2507
               pad_to libname_width (#libname lib) ^ divider ^
2508
               pad_to libstate_width outcome_str ^ divider ^
2509
               error_str ^ "\n")
2510
    end
2511

    
2512
fun vcs_name HG = ("Mercurial", "hg")
2513
  | vcs_name GIT = ("Git", "git")
2514
  | vcs_name SVN = ("Subversion", "svn")
2515
        
2516
fun print_problem_summary context lines =
2517
    let val failed_vcs =
2518
            foldl (fn (({ vcs, ... } : libspec, ERROR _), acc) => vcs::acc
2519
                  | (_, acc) => acc) [] lines
2520
        fun report_nonworking vcs error =
2521
            print ((if error = "" then "" else error ^ "\n\n") ^
2522
                   "Error: The project uses the " ^ (#1 (vcs_name vcs)) ^
2523
                   " version control system, but its\n" ^
2524
                   "executable program (" ^ (#2 (vcs_name vcs)) ^
2525
                   ") does not appear to be installed in the program path\n\n")
2526
        fun check_working [] checked = ()
2527
          | check_working (vcs::rest) checked =
2528
            if List.exists (fn v => vcs = v) checked
2529
            then check_working rest checked
2530
            else
2531
                case AnyLibControl.is_working context vcs of
2532
                    OK true => check_working rest checked
2533
                  | OK false => (report_nonworking vcs "";
2534
                                 check_working rest (vcs::checked))
2535
                  | ERROR e => (report_nonworking vcs e;
2536
                                check_working rest (vcs::checked))
2537
    in
2538
        print "\nError: Some operations failed\n\n";
2539
        check_working failed_vcs []
2540
    end
2541
        
2542
fun act_and_print action print_header print_line context (libs : libspec list) =
2543
    let val lines = map (fn lib => (lib, action lib)) libs
2544
        val imperfect = List.exists (fn (_, ERROR _) => true | _ => false) lines
2545
        val _ = print_header ()
2546
    in
2547
        app print_line lines;
2548
        if imperfect then print_problem_summary context lines else ();
2549
        lines
2550
    end
2551

    
2552
fun return_code_for outcomes =
2553
    foldl (fn ((_, result), acc) =>
2554
              case result of
2555
                  ERROR _ => OS.Process.failure
2556
                | _ => acc)
2557
          OS.Process.success
2558
          outcomes
2559
        
2560
fun status_of_project ({ context, libs } : project) =
2561
    return_code_for (act_and_print (AnyLibControl.status context)
2562
                                   print_status_header (print_status false)
2563
                                   context libs)
2564
                                             
2565
fun review_project ({ context, libs } : project) =
2566
    return_code_for (act_and_print (AnyLibControl.review context)
2567
                                   print_status_header (print_status true)
2568
                                   context libs)
2569

    
2570
fun lock_project ({ context, libs } : project) =
2571
    let val _ = if FileBits.verbose ()
2572
                then print ("Scanning IDs for lock file...\n")
2573
                else ()
2574
        val outcomes = map (fn lib => (lib, AnyLibControl.id_of context lib))
2575
                           libs
2576
        val locks =
2577
            List.concat
2578
                (map (fn (lib : libspec, result) =>
2579
                         case result of
2580
                             ERROR _ => []
2581
                           | OK id => [{ libname = #libname lib,
2582
                                         id_or_tag = id }])
2583
                     outcomes)
2584
        val return_code = return_code_for outcomes
2585
        val _ = print clear_line
2586
    in
2587
        if OS.Process.isSuccess return_code
2588
        then save_lock_file (#rootpath context) locks
2589
        else ();
2590
        return_code
2591
    end
2592

    
2593
fun update_project (project as { context, libs }) =
2594
    let val outcomes = act_and_print
2595
                           (AnyLibControl.update context)
2596
                           print_outcome_header print_update_outcome
2597
                           context libs
2598
        val _ = if List.exists (fn (_, OK _) => true | _ => false) outcomes
2599
                then lock_project project
2600
                else OS.Process.success
2601
        val return_code = return_code_for outcomes
2602
    in
2603
        if OS.Process.isSuccess return_code
2604
        then checkpoint_completion_file (#rootpath context)
2605
        else ();
2606
        return_code
2607
    end
2608
    
2609
fun load_local_project pintype =
2610
    let val userconfig = load_userconfig ()
2611
        val rootpath = OS.FileSys.getDir ()
2612
    in
2613
        load_project userconfig rootpath pintype
2614
    end    
2615

    
2616
fun with_local_project pintype f =
2617
  let open OS.Process
2618
      val return_code =
2619
          f (load_local_project pintype)
2620
          handle Fail msg =>
2621
                 failure before print ("Error: " ^ msg)
2622
               | JsonBits.Config msg =>
2623
                 failure before print ("Error in configuration: " ^ msg)
2624
               | e =>
2625
                 failure before print ("Error: " ^ exnMessage e)
2626
        val _ = print "\n";
2627
    in
2628
        return_code
2629
    end
2630
        
2631
fun review () = with_local_project USE_LOCKFILE review_project
2632
fun status () = with_local_project USE_LOCKFILE status_of_project
2633
fun update () = with_local_project NO_LOCKFILE update_project
2634
fun lock () = with_local_project NO_LOCKFILE lock_project
2635
fun install () = with_local_project USE_LOCKFILE update_project
2636

    
2637
fun version () =
2638
    (print ("v" ^ repoint_version ^ "\n");
2639
     OS.Process.success)
2640
                      
2641
fun usage () =
2642
    (print "\nRepoint ";
2643
     version ();
2644
     print ("\n  A simple manager for third-party source code dependencies.\n"
2645
            ^ "  http://all-day-breakfast.com/repoint/\n\n"
2646
            ^ "Usage:\n\n"
2647
            ^ "  repoint <command> [<options>]\n\n"
2648
            ^ "where <command> is one of:\n\n"
2649
            ^ "  status   print quick report on local status only, without using network\n"
2650
            ^ "  review   check configured libraries against their providers, and report\n"
2651
            ^ "  install  update configured libraries according to project specs and lock file\n"
2652
            ^ "  update   update configured libraries and lock file according to project specs\n"
2653
            ^ "  lock     rewrite lock file to match local library status\n"
2654
            ^ "  archive  pack up project and all libraries into an archive file:\n"
2655
            ^ "           invoke as 'repoint archive targetfile.tar.gz --exclude unwanted.txt'\n"
2656
            ^ "  version  print the Repoint version number and exit\n\n"
2657
            ^ "and <options> may include:\n\n"
2658
            ^ "  --directory <dir>\n"
2659
            ^ "           change to directory <dir> before doing anything; in particular,\n"
2660
            ^ "           expect to find project spec file in that directory\n\n");
2661
    OS.Process.failure)
2662

    
2663
fun archive target args =
2664
    case args of
2665
        [] =>
2666
        with_local_project USE_LOCKFILE (Archive.archive (target, []))
2667
      | "--exclude"::xs =>
2668
        with_local_project USE_LOCKFILE (Archive.archive (target, xs))
2669
      | _ => usage ()
2670

    
2671
fun handleSystemArgs args =
2672
    let fun handleSystemArgs' leftover args =
2673
            case args of
2674
                "--directory"::dir::rest =>
2675
                (OS.FileSys.chDir dir;
2676
                 handleSystemArgs' leftover rest)
2677
              | arg::rest =>
2678
                handleSystemArgs' (leftover @ [arg]) rest
2679
              | [] => leftover
2680
    in
2681
        OK (handleSystemArgs' [] args)
2682
        handle e => ERROR (exnMessage e)
2683
    end
2684
                   
2685
fun repoint args =
2686
    case handleSystemArgs args of
2687
        ERROR e => (print ("Error: " ^ e ^ "\n");
2688
                    OS.Process.exit OS.Process.failure)
2689
      | OK args => 
2690
        let val return_code = 
2691
            case args of
2692
                ["review"] => review ()
2693
              | ["status"] => status ()
2694
              | ["install"] => install ()
2695
              | ["update"] => update ()
2696
              | ["lock"] => lock ()
2697
              | ["version"] => version ()
2698
              | "archive"::target::args => archive target args
2699
              | arg::_ => (print ("Error: unknown argument \"" ^ arg ^ "\"\n");
2700
                           usage ())
2701
              | _ => usage ()
2702
        in
2703
            OS.Process.exit return_code
2704
        end
2705
        
2706
fun main () =
2707
    repoint (CommandLine.arguments ())