annotate vext.sml @ 50:ec5b5a9adac2

Overhaul to use Vext for external repos
author Chris Cannam <c.cannam@qmul.ac.uk>
date Wed, 28 Jun 2017 10:49:26 +0100
parents
children 1e6940e42972
rev   line source
c@50 1 (* This file is automatically generated from the individual
c@50 2 source files in the Vext repository. *)
c@50 3
c@50 4 (*
c@50 5 Vext
c@50 6
c@50 7 A simple manager for third-party source code dependencies
c@50 8
c@50 9 Copyright 2017 Chris Cannam.
c@50 10
c@50 11 Permission is hereby granted, free of charge, to any person
c@50 12 obtaining a copy of this software and associated documentation
c@50 13 files (the "Software"), to deal in the Software without
c@50 14 restriction, including without limitation the rights to use, copy,
c@50 15 modify, merge, publish, distribute, sublicense, and/or sell copies
c@50 16 of the Software, and to permit persons to whom the Software is
c@50 17 furnished to do so, subject to the following conditions:
c@50 18
c@50 19 The above copyright notice and this permission notice shall be
c@50 20 included in all copies or substantial portions of the Software.
c@50 21
c@50 22 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
c@50 23 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
c@50 24 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
c@50 25 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
c@50 26 ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
c@50 27 CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
c@50 28 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
c@50 29
c@50 30 Except as contained in this notice, the names of Chris Cannam and
c@50 31 Particular Programs Ltd shall not be used in advertising or
c@50 32 otherwise to promote the sale, use or other dealings in this
c@50 33 Software without prior written authorization.
c@50 34 *)
c@50 35
c@50 36 val vext_version = "0.9.1"
c@50 37
c@50 38
c@50 39 datatype vcs =
c@50 40 HG |
c@50 41 GIT
c@50 42
c@50 43 datatype source =
c@50 44 URL of string |
c@50 45 PROVIDER of {
c@50 46 service : string,
c@50 47 owner : string option,
c@50 48 repo : string option
c@50 49 }
c@50 50
c@50 51 datatype pin =
c@50 52 UNPINNED |
c@50 53 PINNED of string
c@50 54
c@50 55 datatype libstate =
c@50 56 ABSENT |
c@50 57 CORRECT |
c@50 58 SUPERSEDED |
c@50 59 WRONG
c@50 60
c@50 61 datatype localstate =
c@50 62 MODIFIED |
c@50 63 UNMODIFIED
c@50 64
c@50 65 datatype branch =
c@50 66 BRANCH of string |
c@50 67 DEFAULT_BRANCH
c@50 68
c@50 69 (* If we can recover from an error, for example by reporting failure
c@50 70 for this one thing and going on to the next thing, then the error
c@50 71 should usually be returned through a result type rather than an
c@50 72 exception. *)
c@50 73
c@50 74 datatype 'a result =
c@50 75 OK of 'a |
c@50 76 ERROR of string
c@50 77
c@50 78 type libname = string
c@50 79
c@50 80 type id_or_tag = string
c@50 81
c@50 82 type libspec = {
c@50 83 libname : libname,
c@50 84 vcs : vcs,
c@50 85 source : source,
c@50 86 branch : branch,
c@50 87 pin : pin
c@50 88 }
c@50 89
c@50 90 type lock = {
c@50 91 libname : libname,
c@50 92 id_or_tag : id_or_tag
c@50 93 }
c@50 94
c@50 95 type remote_spec = {
c@50 96 anon : string option,
c@50 97 auth : string option
c@50 98 }
c@50 99
c@50 100 type provider = {
c@50 101 service : string,
c@50 102 supports : vcs list,
c@50 103 remote_spec : remote_spec
c@50 104 }
c@50 105
c@50 106 type account = {
c@50 107 service : string,
c@50 108 login : string
c@50 109 }
c@50 110
c@50 111 type context = {
c@50 112 rootpath : string,
c@50 113 extdir : string,
c@50 114 providers : provider list,
c@50 115 accounts : account list
c@50 116 }
c@50 117
c@50 118 type userconfig = {
c@50 119 providers : provider list,
c@50 120 accounts : account list
c@50 121 }
c@50 122
c@50 123 type project = {
c@50 124 context : context,
c@50 125 libs : libspec list
c@50 126 }
c@50 127
c@50 128 structure VextFilenames = struct
c@50 129 val project_file = "vext-project.json"
c@50 130 val project_lock_file = "vext-lock.json"
c@50 131 val user_config_file = ".vext.json"
c@50 132 end
c@50 133
c@50 134 signature VCS_CONTROL = sig
c@50 135
c@50 136 (** Test whether the library is present locally at all *)
c@50 137 val exists : context -> libname -> bool result
c@50 138
c@50 139 (** Return the id (hash) of the current revision for the library *)
c@50 140 val id_of : context -> libname -> id_or_tag result
c@50 141
c@50 142 (** Test whether the library is at the given id *)
c@50 143 val is_at : context -> libname * id_or_tag -> bool result
c@50 144
c@50 145 (** Test whether the library is on the given branch, i.e. is at
c@50 146 the branch tip or an ancestor of it *)
c@50 147 val is_on_branch : context -> libname * branch -> bool result
c@50 148
c@50 149 (** Test whether the library is at the newest revision for the
c@50 150 given branch. False may indicate that the branch has advanced
c@50 151 or that the library is not on the branch at all. This function
c@50 152 may use the network to check for new revisions *)
c@50 153 val is_newest : context -> libname * branch -> bool result
c@50 154
c@50 155 (** Test whether the library is at the newest revision available
c@50 156 locally for the given branch. False may indicate that the
c@50 157 branch has advanced or that the library is not on the branch
c@50 158 at all. This function must not use the network *)
c@50 159 val is_newest_locally : context -> libname * branch -> bool result
c@50 160
c@50 161 (** Test whether the library has been modified in the local
c@50 162 working copy *)
c@50 163 val is_modified_locally : context -> libname -> bool result
c@50 164
c@50 165 (** Check out, i.e. clone a fresh copy of, the repo for the given
c@50 166 library on the given branch *)
c@50 167 val checkout : context -> libname * source * branch -> unit result
c@50 168
c@50 169 (** Update the library to the given branch tip *)
c@50 170 val update : context -> libname * branch -> id_or_tag result
c@50 171
c@50 172 (** Update the library to the given specific id or tag *)
c@50 173 val update_to : context -> libname * id_or_tag -> id_or_tag result
c@50 174 end
c@50 175
c@50 176 signature LIB_CONTROL = sig
c@50 177 val review : context -> libspec -> (libstate * localstate) result
c@50 178 val status : context -> libspec -> (libstate * localstate) result
c@50 179 val update : context -> libspec -> id_or_tag result
c@50 180 end
c@50 181
c@50 182 structure FileBits :> sig
c@50 183 val extpath : context -> string
c@50 184 val libpath : context -> libname -> string
c@50 185 val subpath : context -> libname -> string -> string
c@50 186 val command_output : context -> libname -> string list -> string result
c@50 187 val command : context -> libname -> string list -> unit result
c@50 188 val file_contents : string -> string
c@50 189 val mydir : unit -> string
c@50 190 val homedir : unit -> string
c@50 191 val mkpath : string -> unit result
c@50 192 val project_spec_path : string -> string
c@50 193 val project_lock_path : string -> string
c@50 194 val verbose : unit -> bool
c@50 195 end = struct
c@50 196
c@50 197 fun verbose () =
c@50 198 case OS.Process.getEnv "VEXT_VERBOSE" of
c@50 199 SOME "0" => false
c@50 200 | SOME _ => true
c@50 201 | NONE => false
c@50 202
c@50 203 fun extpath ({ rootpath, extdir, ... } : context) =
c@50 204 let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
c@50 205 in OS.Path.toString {
c@50 206 isAbs = isAbs,
c@50 207 vol = vol,
c@50 208 arcs = arcs @ [ extdir ]
c@50 209 }
c@50 210 end
c@50 211
c@50 212 fun subpath ({ rootpath, extdir, ... } : context) libname remainder =
c@50 213 (* NB libname is allowed to be a path fragment, e.g. foo/bar *)
c@50 214 let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
c@50 215 val split = String.fields (fn c => c = #"/")
c@50 216 in OS.Path.toString {
c@50 217 isAbs = isAbs,
c@50 218 vol = vol,
c@50 219 arcs = arcs @ [ extdir ] @ split libname @ split remainder
c@50 220 }
c@50 221 end
c@50 222
c@50 223 fun libpath context "" =
c@50 224 extpath context
c@50 225 | libpath context libname =
c@50 226 subpath context libname ""
c@50 227
c@50 228 fun project_file_path rootpath filename =
c@50 229 let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
c@50 230 in OS.Path.toString {
c@50 231 isAbs = isAbs,
c@50 232 vol = vol,
c@50 233 arcs = arcs @ [ filename ]
c@50 234 }
c@50 235 end
c@50 236
c@50 237 fun project_spec_path rootpath =
c@50 238 project_file_path rootpath (VextFilenames.project_file)
c@50 239
c@50 240 fun project_lock_path rootpath =
c@50 241 project_file_path rootpath (VextFilenames.project_lock_file)
c@50 242
c@50 243 fun trim str =
c@50 244 hd (String.fields (fn x => x = #"\n" orelse x = #"\r") str)
c@50 245
c@50 246 fun file_contents filename =
c@50 247 let val stream = TextIO.openIn filename
c@50 248 fun read_all str acc =
c@50 249 case TextIO.inputLine str of
c@50 250 SOME line => read_all str (trim line :: acc)
c@50 251 | NONE => rev acc
c@50 252 val contents = read_all stream []
c@50 253 val _ = TextIO.closeIn stream
c@50 254 in
c@50 255 String.concatWith "\n" contents
c@50 256 end
c@50 257
c@50 258 fun expand_commandline cmdlist =
c@50 259 (* We are quite [too] strict about what we accept here, except
c@50 260 for the first element in cmdlist which is assumed to be a
c@50 261 known command location rather than arbitrary user input. NB
c@50 262 only ASCII accepted at this point. *)
c@50 263 let open Char
c@50 264 fun quote arg =
c@50 265 if List.all
c@50 266 (fn c => isAlphaNum c orelse c = #"-" orelse c = #"_")
c@50 267 (explode arg)
c@50 268 then arg
c@50 269 else "\"" ^ arg ^ "\""
c@50 270 fun check arg =
c@50 271 let val valid = explode " /#:;?,._-{}@="
c@50 272 in
c@50 273 app (fn c =>
c@50 274 if isAlphaNum c orelse
c@50 275 List.exists (fn v => v = c) valid
c@50 276 then ()
c@50 277 else raise Fail ("Invalid character '" ^
c@50 278 (Char.toString c) ^
c@50 279 "' in command list"))
c@50 280 (explode arg);
c@50 281 arg
c@50 282 end
c@50 283 in
c@50 284 String.concatWith " "
c@50 285 (map quote
c@50 286 (hd cmdlist :: map check (tl cmdlist)))
c@50 287 end
c@50 288
c@50 289 val tick_cycle = ref 0
c@50 290 val tick_chars = Vector.fromList (map String.str (explode "|/-\\"))
c@50 291
c@50 292 fun tick libname cmdlist =
c@50 293 let val n = Vector.length tick_chars
c@50 294 fun pad_to n str =
c@50 295 if n <= String.size str then str
c@50 296 else pad_to n (str ^ " ")
c@50 297 val name = if libname <> "" then libname
c@50 298 else if cmdlist = nil then ""
c@50 299 else hd (rev cmdlist)
c@50 300 in
c@50 301 print (" " ^
c@50 302 Vector.sub(tick_chars, !tick_cycle) ^ " " ^
c@50 303 pad_to 24 name ^
c@50 304 "\r");
c@50 305 tick_cycle := (if !tick_cycle = n - 1 then 0 else 1 + !tick_cycle)
c@50 306 end
c@50 307
c@50 308 fun run_command context libname cmdlist redirect =
c@50 309 let open OS
c@50 310 val dir = libpath context libname
c@50 311 val cmd = expand_commandline cmdlist
c@50 312 val _ = if verbose ()
c@50 313 then print ("Running: " ^ cmd ^
c@50 314 " (in dir " ^ dir ^ ")...\n")
c@50 315 else tick libname cmdlist
c@50 316 val _ = FileSys.chDir dir
c@50 317 val status = case redirect of
c@50 318 NONE => Process.system cmd
c@50 319 | SOME file => Process.system (cmd ^ ">" ^ file)
c@50 320 in
c@50 321 if Process.isSuccess status
c@50 322 then OK ()
c@50 323 else ERROR ("Command failed: " ^ cmd ^ " (in dir " ^ dir ^ ")")
c@50 324 end
c@50 325 handle ex => ERROR ("Unable to run command: " ^ exnMessage ex)
c@50 326
c@50 327 fun command context libname cmdlist =
c@50 328 run_command context libname cmdlist NONE
c@50 329
c@50 330 fun command_output context libname cmdlist =
c@50 331 let open OS
c@50 332 val tmpFile = FileSys.tmpName ()
c@50 333 val result = run_command context libname cmdlist (SOME tmpFile)
c@50 334 val contents = file_contents tmpFile
c@50 335 in
c@50 336 FileSys.remove tmpFile handle _ => ();
c@50 337 case result of
c@50 338 OK () => OK contents
c@50 339 | ERROR e => ERROR e
c@50 340 end
c@50 341
c@50 342 fun mydir () =
c@50 343 let open OS
c@50 344 val { dir, file } = Path.splitDirFile (CommandLine.name ())
c@50 345 in
c@50 346 FileSys.realPath
c@50 347 (if Path.isAbsolute dir
c@50 348 then dir
c@50 349 else Path.concat (FileSys.getDir (), dir))
c@50 350 end
c@50 351
c@50 352 fun homedir () =
c@50 353 (* Failure is not routine, so we use an exception here *)
c@50 354 case (OS.Process.getEnv "HOME",
c@50 355 OS.Process.getEnv "HOMEPATH") of
c@50 356 (SOME home, _) => home
c@50 357 | (NONE, SOME home) => home
c@50 358 | (NONE, NONE) =>
c@50 359 raise Fail "Failed to look up home directory from environment"
c@50 360
c@50 361 fun mkpath path =
c@50 362 if OS.FileSys.isDir path handle _ => false
c@50 363 then OK ()
c@50 364 else case OS.Path.fromString path of
c@50 365 { arcs = nil, ... } => OK ()
c@50 366 | { isAbs = false, ... } => ERROR "mkpath requires absolute path"
c@50 367 | { isAbs, vol, arcs } =>
c@50 368 case mkpath (OS.Path.toString { (* parent *)
c@50 369 isAbs = isAbs,
c@50 370 vol = vol,
c@50 371 arcs = rev (tl (rev arcs)) }) of
c@50 372 ERROR e => ERROR e
c@50 373 | OK () => ((OS.FileSys.mkDir path; OK ())
c@50 374 handle OS.SysErr (e, _) =>
c@50 375 ERROR ("Directory creation failed: " ^ e))
c@50 376 end
c@50 377
c@50 378 functor LibControlFn (V: VCS_CONTROL) :> LIB_CONTROL = struct
c@50 379
c@50 380 (* Valid states for unpinned libraries:
c@50 381
c@50 382 - CORRECT: We are on the right branch and are up-to-date with
c@50 383 it as far as we can tell. (If not using the network, this
c@50 384 should be reported to user as "Present" rather than "Correct"
c@50 385 as the remote repo may have advanced without us knowing.)
c@50 386
c@50 387 - SUPERSEDED: We are on the right branch but we can see that
c@50 388 there is a newer revision either locally or on the remote (in
c@50 389 Git terms, we are at an ancestor of the desired branch tip).
c@50 390
c@50 391 - WRONG: We are on the wrong branch (in Git terms, we are not
c@50 392 at the desired branch tip or any ancestor of it).
c@50 393
c@50 394 - ABSENT: Repo doesn't exist here at all.
c@50 395
c@50 396 Valid states for pinned libraries:
c@50 397
c@50 398 - CORRECT: We are at the pinned revision.
c@50 399
c@50 400 - WRONG: We are at any revision other than the pinned one.
c@50 401
c@50 402 - ABSENT: Repo doesn't exist here at all.
c@50 403 *)
c@50 404
c@50 405 fun check with_network context ({ libname, branch, pin, ... } : libspec) =
c@50 406 let fun check_unpinned () =
c@50 407 let val is_newest = if with_network
c@50 408 then V.is_newest
c@50 409 else V.is_newest_locally
c@50 410 in
c@50 411 case is_newest context (libname, branch) of
c@50 412 ERROR e => ERROR e
c@50 413 | OK true => OK CORRECT
c@50 414 | OK false =>
c@50 415 case V.is_on_branch context (libname, branch) of
c@50 416 ERROR e => ERROR e
c@50 417 | OK true => OK SUPERSEDED
c@50 418 | OK false => OK WRONG
c@50 419 end
c@50 420 fun check_pinned target =
c@50 421 case V.is_at context (libname, target) of
c@50 422 ERROR e => ERROR e
c@50 423 | OK true => OK CORRECT
c@50 424 | OK false => OK WRONG
c@50 425 fun check' () =
c@50 426 case pin of
c@50 427 UNPINNED => check_unpinned ()
c@50 428 | PINNED target => check_pinned target
c@50 429 in
c@50 430 case V.exists context libname of
c@50 431 ERROR e => ERROR e
c@50 432 | OK false => OK (ABSENT, UNMODIFIED)
c@50 433 | OK true =>
c@50 434 case (check' (), V.is_modified_locally context libname) of
c@50 435 (ERROR e, _) => ERROR e
c@50 436 | (_, ERROR e) => ERROR e
c@50 437 | (OK state, OK true) => OK (state, MODIFIED)
c@50 438 | (OK state, OK false) => OK (state, UNMODIFIED)
c@50 439 end
c@50 440
c@50 441 val review = check true
c@50 442 val status = check false
c@50 443
c@50 444 fun update context ({ libname, source, branch, pin, ... } : libspec) =
c@50 445 let fun update_unpinned () =
c@50 446 case V.is_newest context (libname, branch) of
c@50 447 ERROR e => ERROR e
c@50 448 | OK true => V.id_of context libname
c@50 449 | OK false => V.update context (libname, branch)
c@50 450 fun update_pinned target =
c@50 451 case V.is_at context (libname, target) of
c@50 452 ERROR e => ERROR e
c@50 453 | OK true => OK target
c@50 454 | OK false => V.update_to context (libname, target)
c@50 455 fun update' () =
c@50 456 case pin of
c@50 457 UNPINNED => update_unpinned ()
c@50 458 | PINNED target => update_pinned target
c@50 459 in
c@50 460 case V.exists context libname of
c@50 461 ERROR e => ERROR e
c@50 462 | OK true => update' ()
c@50 463 | OK false =>
c@50 464 case V.checkout context (libname, source, branch) of
c@50 465 ERROR e => ERROR e
c@50 466 | OK () => update' ()
c@50 467 end
c@50 468 end
c@50 469
c@50 470 (* Simple Standard ML JSON parser
c@50 471 ==============================
c@50 472
c@50 473 https://bitbucket.org/cannam/sml-simplejson
c@50 474
c@50 475 An RFC-compliant JSON parser in one SML file with no dependency
c@50 476 on anything outside the Basis library. Also includes a simple
c@50 477 serialiser.
c@50 478
c@50 479 Tested with MLton, Poly/ML, and SML/NJ compilers.
c@50 480
c@50 481 Parser notes:
c@50 482
c@50 483 * Complies with RFC 7159, The JavaScript Object Notation (JSON)
c@50 484 Data Interchange Format
c@50 485
c@50 486 * Passes all of the JSONTestSuite parser accept/reject tests that
c@50 487 exist at the time of writing, as listed in "Parsing JSON is a
c@50 488 Minefield" (http://seriot.ch/parsing_json.php)
c@50 489
c@50 490 * Two-pass parser using naive exploded strings, therefore not
c@50 491 particularly fast and not suitable for large input files
c@50 492
c@50 493 * Only supports UTF-8 input, not UTF-16 or UTF-32. Doesn't check
c@50 494 that JSON strings are valid UTF-8 -- the caller must do that --
c@50 495 but does handle \u escapes
c@50 496
c@50 497 * Converts all numbers to type "real". If that is a 64-bit IEEE
c@50 498 float type (common but not guaranteed in SML) then we're pretty
c@50 499 standard for a JSON parser
c@50 500
c@50 501 Copyright 2017 Chris Cannam.
c@50 502 Parts based on the JSON parser in the Ponyo library by Phil Eaton.
c@50 503
c@50 504 Permission is hereby granted, free of charge, to any person
c@50 505 obtaining a copy of this software and associated documentation
c@50 506 files (the "Software"), to deal in the Software without
c@50 507 restriction, including without limitation the rights to use, copy,
c@50 508 modify, merge, publish, distribute, sublicense, and/or sell copies
c@50 509 of the Software, and to permit persons to whom the Software is
c@50 510 furnished to do so, subject to the following conditions:
c@50 511
c@50 512 The above copyright notice and this permission notice shall be
c@50 513 included in all copies or substantial portions of the Software.
c@50 514
c@50 515 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
c@50 516 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
c@50 517 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
c@50 518 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
c@50 519 ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
c@50 520 CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
c@50 521 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
c@50 522
c@50 523 Except as contained in this notice, the names of Chris Cannam and
c@50 524 Particular Programs Ltd shall not be used in advertising or
c@50 525 otherwise to promote the sale, use or other dealings in this
c@50 526 Software without prior written authorization.
c@50 527 *)
c@50 528
c@50 529 signature JSON = sig
c@50 530
c@50 531 datatype json = OBJECT of (string * json) list
c@50 532 | ARRAY of json list
c@50 533 | NUMBER of real
c@50 534 | STRING of string
c@50 535 | BOOL of bool
c@50 536 | NULL
c@50 537
c@50 538 datatype 'a result = OK of 'a
c@50 539 | ERROR of string
c@50 540
c@50 541 val parse : string -> json result
c@50 542 val serialise : json -> string
c@50 543 val serialiseIndented : json -> string
c@50 544
c@50 545 end
c@50 546
c@50 547 structure Json :> JSON = struct
c@50 548
c@50 549 datatype json = OBJECT of (string * json) list
c@50 550 | ARRAY of json list
c@50 551 | NUMBER of real
c@50 552 | STRING of string
c@50 553 | BOOL of bool
c@50 554 | NULL
c@50 555
c@50 556 datatype 'a result = OK of 'a
c@50 557 | ERROR of string
c@50 558
c@50 559 structure T = struct
c@50 560 datatype token = NUMBER of char list
c@50 561 | STRING of string
c@50 562 | BOOL of bool
c@50 563 | NULL
c@50 564 | CURLY_L
c@50 565 | CURLY_R
c@50 566 | SQUARE_L
c@50 567 | SQUARE_R
c@50 568 | COLON
c@50 569 | COMMA
c@50 570
c@50 571 fun toString t =
c@50 572 case t of NUMBER digits => implode digits
c@50 573 | STRING s => s
c@50 574 | BOOL b => Bool.toString b
c@50 575 | NULL => "null"
c@50 576 | CURLY_L => "{"
c@50 577 | CURLY_R => "}"
c@50 578 | SQUARE_L => "["
c@50 579 | SQUARE_R => "]"
c@50 580 | COLON => ":"
c@50 581 | COMMA => ","
c@50 582 end
c@50 583
c@50 584 fun bmpToUtf8 cp = (* convert a codepoint in Unicode BMP to utf8 bytes *)
c@50 585 let open Word
c@50 586 infix 6 orb andb >>
c@50 587 in
c@50 588 map (Char.chr o toInt)
c@50 589 (if cp < 0wx80 then
c@50 590 [cp]
c@50 591 else if cp < 0wx800 then
c@50 592 [0wxc0 orb (cp >> 0w6), 0wx80 orb (cp andb 0wx3f)]
c@50 593 else if cp < 0wx10000 then
c@50 594 [0wxe0 orb (cp >> 0w12),
c@50 595 0wx80 orb ((cp >> 0w6) andb 0wx3f),
c@50 596 0wx80 orb (cp andb 0wx3f)]
c@50 597 else raise Fail ("Invalid BMP point " ^ (Word.toString cp)))
c@50 598 end
c@50 599
c@50 600 fun error pos text = ERROR (text ^ " at character position " ^
c@50 601 Int.toString (pos - 1))
c@50 602 fun token_error pos = error pos ("Unexpected token")
c@50 603
c@50 604 fun lexNull pos acc (#"u" :: #"l" :: #"l" :: xs) =
c@50 605 lex (pos + 3) (T.NULL :: acc) xs
c@50 606 | lexNull pos acc _ = token_error pos
c@50 607
c@50 608 and lexTrue pos acc (#"r" :: #"u" :: #"e" :: xs) =
c@50 609 lex (pos + 3) (T.BOOL true :: acc) xs
c@50 610 | lexTrue pos acc _ = token_error pos
c@50 611
c@50 612 and lexFalse pos acc (#"a" :: #"l" :: #"s" :: #"e" :: xs) =
c@50 613 lex (pos + 4) (T.BOOL false :: acc) xs
c@50 614 | lexFalse pos acc _ = token_error pos
c@50 615
c@50 616 and lexChar tok pos acc xs =
c@50 617 lex pos (tok :: acc) xs
c@50 618
c@50 619 and lexString pos acc cc =
c@50 620 let datatype escaped = ESCAPED | NORMAL
c@50 621 fun lexString' pos text ESCAPED [] =
c@50 622 error pos "End of input during escape sequence"
c@50 623 | lexString' pos text NORMAL [] =
c@50 624 error pos "End of input during string"
c@50 625 | lexString' pos text ESCAPED (x :: xs) =
c@50 626 let fun esc c = lexString' (pos + 1) (c :: text) NORMAL xs
c@50 627 in case x of
c@50 628 #"\"" => esc x
c@50 629 | #"\\" => esc x
c@50 630 | #"/" => esc x
c@50 631 | #"b" => esc #"\b"
c@50 632 | #"f" => esc #"\f"
c@50 633 | #"n" => esc #"\n"
c@50 634 | #"r" => esc #"\r"
c@50 635 | #"t" => esc #"\t"
c@50 636 | _ => error pos ("Invalid escape \\" ^
c@50 637 Char.toString x)
c@50 638 end
c@50 639 | lexString' pos text NORMAL (#"\\" :: #"u" ::a::b::c::d:: xs) =
c@50 640 if List.all Char.isHexDigit [a,b,c,d]
c@50 641 then case Word.fromString ("0wx" ^ (implode [a,b,c,d])) of
c@50 642 SOME w => (let val utf = rev (bmpToUtf8 w) in
c@50 643 lexString' (pos + 6) (utf @ text)
c@50 644 NORMAL xs
c@50 645 end
c@50 646 handle Fail err => error pos err)
c@50 647 | NONE => error pos "Invalid Unicode BMP escape sequence"
c@50 648 else error pos "Invalid Unicode BMP escape sequence"
c@50 649 | lexString' pos text NORMAL (x :: xs) =
c@50 650 if Char.ord x < 0x20
c@50 651 then error pos "Invalid unescaped control character"
c@50 652 else
c@50 653 case x of
c@50 654 #"\"" => OK (rev text, xs, pos + 1)
c@50 655 | #"\\" => lexString' (pos + 1) text ESCAPED xs
c@50 656 | _ => lexString' (pos + 1) (x :: text) NORMAL xs
c@50 657 in
c@50 658 case lexString' pos [] NORMAL cc of
c@50 659 OK (text, rest, newpos) =>
c@50 660 lex newpos (T.STRING (implode text) :: acc) rest
c@50 661 | ERROR e => ERROR e
c@50 662 end
c@50 663
c@50 664 and lexNumber firstChar pos acc cc =
c@50 665 let val valid = explode ".+-e"
c@50 666 fun lexNumber' pos digits [] = (rev digits, [], pos)
c@50 667 | lexNumber' pos digits (x :: xs) =
c@50 668 if x = #"E" then lexNumber' (pos + 1) (#"e" :: digits) xs
c@50 669 else if Char.isDigit x orelse List.exists (fn c => x = c) valid
c@50 670 then lexNumber' (pos + 1) (x :: digits) xs
c@50 671 else (rev digits, x :: xs, pos)
c@50 672 val (digits, rest, newpos) =
c@50 673 lexNumber' (pos - 1) [] (firstChar :: cc)
c@50 674 in
c@50 675 case digits of
c@50 676 [] => token_error pos
c@50 677 | _ => lex newpos (T.NUMBER digits :: acc) rest
c@50 678 end
c@50 679
c@50 680 and lex pos acc [] = OK (rev acc)
c@50 681 | lex pos acc (x::xs) =
c@50 682 (case x of
c@50 683 #" " => lex
c@50 684 | #"\t" => lex
c@50 685 | #"\n" => lex
c@50 686 | #"\r" => lex
c@50 687 | #"{" => lexChar T.CURLY_L
c@50 688 | #"}" => lexChar T.CURLY_R
c@50 689 | #"[" => lexChar T.SQUARE_L
c@50 690 | #"]" => lexChar T.SQUARE_R
c@50 691 | #":" => lexChar T.COLON
c@50 692 | #"," => lexChar T.COMMA
c@50 693 | #"\"" => lexString
c@50 694 | #"t" => lexTrue
c@50 695 | #"f" => lexFalse
c@50 696 | #"n" => lexNull
c@50 697 | x => lexNumber x) (pos + 1) acc xs
c@50 698
c@50 699 fun show [] = "end of input"
c@50 700 | show (tok :: _) = T.toString tok
c@50 701
c@50 702 fun parseNumber digits =
c@50 703 (* Note lexNumber already case-insensitised the E for us *)
c@50 704 let open Char
c@50 705
c@50 706 fun okExpDigits [] = false
c@50 707 | okExpDigits (c :: []) = isDigit c
c@50 708 | okExpDigits (c :: cs) = isDigit c andalso okExpDigits cs
c@50 709
c@50 710 fun okExponent [] = false
c@50 711 | okExponent (#"+" :: cs) = okExpDigits cs
c@50 712 | okExponent (#"-" :: cs) = okExpDigits cs
c@50 713 | okExponent cc = okExpDigits cc
c@50 714
c@50 715 fun okFracTrailing [] = true
c@50 716 | okFracTrailing (c :: cs) =
c@50 717 (isDigit c andalso okFracTrailing cs) orelse
c@50 718 (c = #"e" andalso okExponent cs)
c@50 719
c@50 720 fun okFraction [] = false
c@50 721 | okFraction (c :: cs) =
c@50 722 isDigit c andalso okFracTrailing cs
c@50 723
c@50 724 fun okPosTrailing [] = true
c@50 725 | okPosTrailing (#"." :: cs) = okFraction cs
c@50 726 | okPosTrailing (#"e" :: cs) = okExponent cs
c@50 727 | okPosTrailing (c :: cs) =
c@50 728 isDigit c andalso okPosTrailing cs
c@50 729
c@50 730 fun okPositive [] = false
c@50 731 | okPositive (#"0" :: []) = true
c@50 732 | okPositive (#"0" :: #"." :: cs) = okFraction cs
c@50 733 | okPositive (#"0" :: #"e" :: cs) = okExponent cs
c@50 734 | okPositive (#"0" :: cs) = false
c@50 735 | okPositive (c :: cs) = isDigit c andalso okPosTrailing cs
c@50 736
c@50 737 fun okNumber (#"-" :: cs) = okPositive cs
c@50 738 | okNumber cc = okPositive cc
c@50 739 in
c@50 740 if okNumber digits
c@50 741 then case Real.fromString (implode digits) of
c@50 742 NONE => ERROR "Number out of range"
c@50 743 | SOME r => OK r
c@50 744 else ERROR ("Invalid number \"" ^ (implode digits) ^ "\"")
c@50 745 end
c@50 746
c@50 747 fun parseObject (T.CURLY_R :: xs) = OK (OBJECT [], xs)
c@50 748 | parseObject tokens =
c@50 749 let fun parsePair (T.STRING key :: T.COLON :: xs) =
c@50 750 (case parseTokens xs of
c@50 751 ERROR e => ERROR e
c@50 752 | OK (j, xs) => OK ((key, j), xs))
c@50 753 | parsePair other =
c@50 754 ERROR ("Object key/value pair expected around \"" ^
c@50 755 show other ^ "\"")
c@50 756 fun parseObject' acc [] = ERROR "End of input during object"
c@50 757 | parseObject' acc tokens =
c@50 758 case parsePair tokens of
c@50 759 ERROR e => ERROR e
c@50 760 | OK (pair, T.COMMA :: xs) =>
c@50 761 parseObject' (pair :: acc) xs
c@50 762 | OK (pair, T.CURLY_R :: xs) =>
c@50 763 OK (OBJECT (rev (pair :: acc)), xs)
c@50 764 | OK (_, _) => ERROR "Expected , or } after object element"
c@50 765 in
c@50 766 parseObject' [] tokens
c@50 767 end
c@50 768
c@50 769 and parseArray (T.SQUARE_R :: xs) = OK (ARRAY [], xs)
c@50 770 | parseArray tokens =
c@50 771 let fun parseArray' acc [] = ERROR "End of input during array"
c@50 772 | parseArray' acc tokens =
c@50 773 case parseTokens tokens of
c@50 774 ERROR e => ERROR e
c@50 775 | OK (j, T.COMMA :: xs) => parseArray' (j :: acc) xs
c@50 776 | OK (j, T.SQUARE_R :: xs) => OK (ARRAY (rev (j :: acc)), xs)
c@50 777 | OK (_, _) => ERROR "Expected , or ] after array element"
c@50 778 in
c@50 779 parseArray' [] tokens
c@50 780 end
c@50 781
c@50 782 and parseTokens [] = ERROR "Value expected"
c@50 783 | parseTokens (tok :: xs) =
c@50 784 (case tok of
c@50 785 T.NUMBER d => (case parseNumber d of
c@50 786 OK r => OK (NUMBER r, xs)
c@50 787 | ERROR e => ERROR e)
c@50 788 | T.STRING s => OK (STRING s, xs)
c@50 789 | T.BOOL b => OK (BOOL b, xs)
c@50 790 | T.NULL => OK (NULL, xs)
c@50 791 | T.CURLY_L => parseObject xs
c@50 792 | T.SQUARE_L => parseArray xs
c@50 793 | _ => ERROR ("Unexpected token " ^ T.toString tok ^
c@50 794 " before " ^ show xs))
c@50 795
c@50 796 fun parse str =
c@50 797 case lex 1 [] (explode str) of
c@50 798 ERROR e => ERROR e
c@50 799 | OK tokens => case parseTokens tokens of
c@50 800 OK (value, []) => OK value
c@50 801 | OK (_, _) => ERROR "Extra data after input"
c@50 802 | ERROR e => ERROR e
c@50 803
c@50 804 fun stringEscape s =
c@50 805 let fun esc x = [x, #"\\"]
c@50 806 fun escape' acc [] = rev acc
c@50 807 | escape' acc (x :: xs) =
c@50 808 escape' (case x of
c@50 809 #"\"" => esc x @ acc
c@50 810 | #"\\" => esc x @ acc
c@50 811 | #"\b" => esc #"b" @ acc
c@50 812 | #"\f" => esc #"f" @ acc
c@50 813 | #"\n" => esc #"n" @ acc
c@50 814 | #"\r" => esc #"r" @ acc
c@50 815 | #"\t" => esc #"t" @ acc
c@50 816 | _ =>
c@50 817 let val c = Char.ord x
c@50 818 in
c@50 819 if c < 0x20
c@50 820 then let val hex = Word.toString (Word.fromInt c)
c@50 821 in (rev o explode) (if c < 0x10
c@50 822 then ("\\u000" ^ hex)
c@50 823 else ("\\u00" ^ hex))
c@50 824 end @ acc
c@50 825 else
c@50 826 x :: acc
c@50 827 end)
c@50 828 xs
c@50 829 in
c@50 830 implode (escape' [] (explode s))
c@50 831 end
c@50 832
c@50 833 fun serialise json =
c@50 834 case json of
c@50 835 OBJECT pp => "{" ^ String.concatWith
c@50 836 "," (map (fn (key, value) =>
c@50 837 serialise (STRING key) ^ ":" ^
c@50 838 serialise value) pp) ^
c@50 839 "}"
c@50 840 | ARRAY arr => "[" ^ String.concatWith "," (map serialise arr) ^ "]"
c@50 841 | NUMBER n => implode (map (fn #"~" => #"-" | c => c)
c@50 842 (explode (Real.toString n)))
c@50 843 | STRING s => "\"" ^ stringEscape s ^ "\""
c@50 844 | BOOL b => Bool.toString b
c@50 845 | NULL => "null"
c@50 846
c@50 847 fun serialiseIndented json =
c@50 848 let fun indent 0 = ""
c@50 849 | indent i = " " ^ indent (i - 1)
c@50 850 fun serialiseIndented' i json =
c@50 851 let val ser = serialiseIndented' (i + 1)
c@50 852 in
c@50 853 case json of
c@50 854 OBJECT [] => "{}"
c@50 855 | ARRAY [] => "[]"
c@50 856 | OBJECT pp => "{\n" ^ indent (i + 1) ^
c@50 857 String.concatWith
c@50 858 (",\n" ^ indent (i + 1))
c@50 859 (map (fn (key, value) =>
c@50 860 ser (STRING key) ^ ": " ^
c@50 861 ser value) pp) ^
c@50 862 "\n" ^ indent i ^ "}"
c@50 863 | ARRAY arr => "[\n" ^ indent (i + 1) ^
c@50 864 String.concatWith
c@50 865 (",\n" ^ indent (i + 1))
c@50 866 (map ser arr) ^
c@50 867 "\n" ^ indent i ^ "]"
c@50 868 | other => serialise other
c@50 869 end
c@50 870 in
c@50 871 serialiseIndented' 0 json ^ "\n"
c@50 872 end
c@50 873
c@50 874 end
c@50 875
c@50 876
c@50 877 structure JsonBits :> sig
c@50 878 val load_json_from : string -> Json.json (* filename -> json *)
c@50 879 val save_json_to : string -> Json.json -> unit
c@50 880 val lookup_optional : Json.json -> string list -> Json.json option
c@50 881 val lookup_optional_string : Json.json -> string list -> string option
c@50 882 val lookup_mandatory : Json.json -> string list -> Json.json
c@50 883 val lookup_mandatory_string : Json.json -> string list -> string
c@50 884 end = struct
c@50 885
c@50 886 fun load_json_from filename =
c@50 887 case Json.parse (FileBits.file_contents filename) of
c@50 888 Json.OK json => json
c@50 889 | Json.ERROR e => raise Fail ("Failed to parse file: " ^ e)
c@50 890
c@50 891 fun save_json_to filename json =
c@50 892 let val jstr = Json.serialiseIndented json
c@50 893 val stream = TextIO.openOut filename
c@50 894 in
c@50 895 TextIO.output (stream, jstr);
c@50 896 TextIO.closeOut stream
c@50 897 end
c@50 898
c@50 899 fun lookup_optional json kk =
c@50 900 let fun lookup key =
c@50 901 case json of
c@50 902 Json.OBJECT kvs =>
c@50 903 (case List.find (fn (k, v) => k = key) kvs of
c@50 904 SOME (k, v) => SOME v
c@50 905 | NONE => NONE)
c@50 906 | _ => raise Fail "Object expected"
c@50 907 in
c@50 908 case kk of
c@50 909 [] => NONE
c@50 910 | key::[] => lookup key
c@50 911 | key::kk => case lookup key of
c@50 912 NONE => NONE
c@50 913 | SOME j => lookup_optional j kk
c@50 914 end
c@50 915
c@50 916 fun lookup_optional_string json kk =
c@50 917 case lookup_optional json kk of
c@50 918 SOME (Json.STRING s) => SOME s
c@50 919 | SOME _ => raise Fail ("Value (if present) must be string: " ^
c@50 920 (String.concatWith " -> " kk))
c@50 921 | NONE => NONE
c@50 922
c@50 923 fun lookup_mandatory json kk =
c@50 924 case lookup_optional json kk of
c@50 925 SOME v => v
c@50 926 | NONE => raise Fail ("Value is mandatory: " ^
c@50 927 (String.concatWith " -> " kk) ^ " in json: " ^
c@50 928 (Json.serialise json))
c@50 929
c@50 930 fun lookup_mandatory_string json kk =
c@50 931 case lookup_optional json kk of
c@50 932 SOME (Json.STRING s) => s
c@50 933 | _ => raise Fail ("Value must be string: " ^
c@50 934 (String.concatWith " -> " kk))
c@50 935 end
c@50 936
c@50 937 structure Provider :> sig
c@50 938 val load_providers : Json.json -> provider list
c@50 939 val load_more_providers : provider list -> Json.json -> provider list
c@50 940 val remote_url : context -> vcs -> source -> libname -> string
c@50 941 end = struct
c@50 942
c@50 943 val known_providers : provider list =
c@50 944 [ {
c@50 945 service = "bitbucket",
c@50 946 supports = [HG, GIT],
c@50 947 remote_spec = {
c@50 948 anon = SOME "https://bitbucket.org/{owner}/{repo}",
c@50 949 auth = SOME "ssh://{vcs}@bitbucket.org/{owner}/{repo}"
c@50 950 }
c@50 951 },
c@50 952 {
c@50 953 service = "github",
c@50 954 supports = [GIT],
c@50 955 remote_spec = {
c@50 956 anon = SOME "https://github.com/{owner}/{repo}",
c@50 957 auth = SOME "ssh://{vcs}@github.com/{owner}/{repo}"
c@50 958 }
c@50 959 }
c@50 960 ]
c@50 961
c@50 962 fun vcs_name vcs =
c@50 963 case vcs of GIT => "git" |
c@50 964 HG => "hg"
c@50 965
c@50 966 fun vcs_from_name name =
c@50 967 case name of "git" => GIT
c@50 968 | "hg" => HG
c@50 969 | other => raise Fail ("Unknown vcs name \"" ^ name ^ "\"")
c@50 970
c@50 971 fun load_more_providers previously_loaded json =
c@50 972 let open JsonBits
c@50 973 fun load pjson pname : provider =
c@50 974 {
c@50 975 service = pname,
c@50 976 supports =
c@50 977 case lookup_mandatory pjson ["vcs"] of
c@50 978 Json.ARRAY vv =>
c@50 979 map (fn (Json.STRING v) => vcs_from_name v
c@50 980 | _ => raise Fail "Strings expected in vcs array")
c@50 981 vv
c@50 982 | _ => raise Fail "Array expected for vcs",
c@50 983 remote_spec = {
c@50 984 anon = lookup_optional_string pjson ["anon"],
c@50 985 auth = lookup_optional_string pjson ["auth"]
c@50 986 }
c@50 987 }
c@50 988 val loaded =
c@50 989 case lookup_optional json ["providers"] of
c@50 990 NONE => []
c@50 991 | SOME (Json.OBJECT pl) => map (fn (k, v) => load v k) pl
c@50 992 | _ => raise Fail "Object expected for providers in config"
c@50 993 val newly_loaded =
c@50 994 List.filter (fn p => not (List.exists (fn pp => #service p =
c@50 995 #service pp)
c@50 996 previously_loaded))
c@50 997 loaded
c@50 998 in
c@50 999 previously_loaded @ newly_loaded
c@50 1000 end
c@50 1001
c@50 1002 fun load_providers json =
c@50 1003 load_more_providers known_providers json
c@50 1004
c@50 1005 fun expand_spec spec { vcs, service, owner, repo } login =
c@50 1006 (* ugly *)
c@50 1007 let fun replace str =
c@50 1008 case str of
c@50 1009 "vcs" => vcs_name vcs
c@50 1010 | "service" => service
c@50 1011 | "owner" =>
c@50 1012 (case owner of
c@50 1013 SOME ostr => ostr
c@50 1014 | NONE => raise Fail ("Owner not specified for service " ^
c@50 1015 service))
c@50 1016 | "repo" => repo
c@50 1017 | "account" =>
c@50 1018 (case login of
c@50 1019 SOME acc => acc
c@50 1020 | NONE => raise Fail ("Account not given for service " ^
c@50 1021 service))
c@50 1022 | other => raise Fail ("Unknown variable \"" ^ other ^
c@50 1023 "\" in spec for service " ^ service)
c@50 1024 fun expand' acc sstr =
c@50 1025 case Substring.splitl (fn c => c <> #"{") sstr of
c@50 1026 (pfx, sfx) =>
c@50 1027 if Substring.isEmpty sfx
c@50 1028 then rev (pfx :: acc)
c@50 1029 else
c@50 1030 case Substring.splitl (fn c => c <> #"}") sfx of
c@50 1031 (tok, remainder) =>
c@50 1032 if Substring.isEmpty remainder
c@50 1033 then rev (tok :: pfx :: acc)
c@50 1034 else let val replacement =
c@50 1035 replace
c@50 1036 (* tok begins with "{": *)
c@50 1037 (Substring.string
c@50 1038 (Substring.triml 1 tok))
c@50 1039 in
c@50 1040 expand' (Substring.full replacement ::
c@50 1041 pfx :: acc)
c@50 1042 (* remainder begins with "}": *)
c@50 1043 (Substring.triml 1 remainder)
c@50 1044 end
c@50 1045 in
c@50 1046 Substring.concat (expand' [] (Substring.full spec))
c@50 1047 end
c@50 1048
c@50 1049 fun provider_url req login providers =
c@50 1050 case providers of
c@50 1051 [] => raise Fail ("Unknown service \"" ^ (#service req) ^
c@50 1052 "\" for vcs \"" ^ (vcs_name (#vcs req)) ^ "\"")
c@50 1053 | ({ service, supports, remote_spec : remote_spec } :: rest) =>
c@50 1054 if service <> (#service req) orelse
c@50 1055 not (List.exists (fn v => v = (#vcs req)) supports)
c@50 1056 then provider_url req login rest
c@50 1057 else
c@50 1058 case (login, #auth remote_spec, #anon remote_spec) of
c@50 1059 (SOME _, SOME auth, _) => expand_spec auth req login
c@50 1060 | (SOME _, _, SOME anon) => expand_spec anon req NONE
c@50 1061 | (NONE, _, SOME anon) => expand_spec anon req NONE
c@50 1062 | _ => raise Fail ("No suitable anon/auth URL spec " ^
c@50 1063 "provided for service \"" ^ service ^ "\"")
c@50 1064
c@50 1065 fun login_for ({ accounts, ... } : context) service =
c@50 1066 case List.find (fn a => service = #service a) accounts of
c@50 1067 SOME { login, ... } => SOME login
c@50 1068 | NONE => NONE
c@50 1069
c@50 1070 fun remote_url (context : context) vcs source libname =
c@50 1071 case source of
c@50 1072 URL u => u
c@50 1073 | PROVIDER { service, owner, repo } =>
c@50 1074 provider_url { vcs = vcs,
c@50 1075 service = service,
c@50 1076 owner = owner,
c@50 1077 repo = case repo of
c@50 1078 SOME r => r
c@50 1079 | NONE => libname }
c@50 1080 (login_for context service)
c@50 1081 (#providers context)
c@50 1082 end
c@50 1083
c@50 1084 structure HgControl :> VCS_CONTROL = struct
c@50 1085
c@50 1086 type vcsstate = { id: string, modified: bool,
c@50 1087 branch: string, tags: string list }
c@50 1088
c@50 1089 val hg_args = [ "--config", "ui.interactive=true" ]
c@50 1090
c@50 1091 fun hg_command context libname args =
c@50 1092 FileBits.command context libname ("hg" :: hg_args @ args)
c@50 1093
c@50 1094 fun hg_command_output context libname args =
c@50 1095 FileBits.command_output context libname ("hg" :: hg_args @ args)
c@50 1096
c@50 1097 fun exists context libname =
c@50 1098 OK (OS.FileSys.isDir (FileBits.subpath context libname ".hg"))
c@50 1099 handle _ => OK false
c@50 1100
c@50 1101 fun remote_for context (libname, source) =
c@50 1102 Provider.remote_url context HG source libname
c@50 1103
c@50 1104 fun current_state context libname : vcsstate result =
c@50 1105 let fun is_branch text = text <> "" andalso #"(" = hd (explode text)
c@50 1106 and extract_branch b =
c@50 1107 if is_branch b (* need to remove enclosing parens *)
c@50 1108 then (implode o rev o tl o rev o tl o explode) b
c@50 1109 else "default"
c@50 1110 and is_modified id = id <> "" andalso #"+" = hd (rev (explode id))
c@50 1111 and extract_id id =
c@50 1112 if is_modified id (* need to remove trailing "+" *)
c@50 1113 then (implode o rev o tl o rev o explode) id
c@50 1114 else id
c@50 1115 and split_tags tags = String.tokens (fn c => c = #"/") tags
c@50 1116 and state_for (id, branch, tags) =
c@50 1117 OK { id = extract_id id,
c@50 1118 modified = is_modified id,
c@50 1119 branch = extract_branch branch,
c@50 1120 tags = split_tags tags }
c@50 1121 in
c@50 1122 case hg_command_output context libname ["id"] of
c@50 1123 ERROR e => ERROR e
c@50 1124 | OK out =>
c@50 1125 case String.tokens (fn x => x = #" ") out of
c@50 1126 [id, branch, tags] => state_for (id, branch, tags)
c@50 1127 | [id, other] => if is_branch other
c@50 1128 then state_for (id, other, "")
c@50 1129 else state_for (id, "", other)
c@50 1130 | [id] => state_for (id, "", "")
c@50 1131 | _ => ERROR ("Unexpected output from hg id: " ^ out)
c@50 1132 end
c@50 1133
c@50 1134 fun branch_name branch = case branch of
c@50 1135 DEFAULT_BRANCH => "default"
c@50 1136 | BRANCH "" => "default"
c@50 1137 | BRANCH b => b
c@50 1138
c@50 1139 fun id_of context libname =
c@50 1140 case current_state context libname of
c@50 1141 ERROR e => ERROR e
c@50 1142 | OK { id, ... } => OK id
c@50 1143
c@50 1144 fun is_at context (libname, id_or_tag) =
c@50 1145 case current_state context libname of
c@50 1146 ERROR e => ERROR e
c@50 1147 | OK { id, tags, ... } =>
c@50 1148 OK (String.isPrefix id_or_tag id orelse
c@50 1149 String.isPrefix id id_or_tag orelse
c@50 1150 List.exists (fn t => t = id_or_tag) tags)
c@50 1151
c@50 1152 fun is_on_branch context (libname, b) =
c@50 1153 case current_state context libname of
c@50 1154 ERROR e => ERROR e
c@50 1155 | OK { branch, ... } => OK (branch = branch_name b)
c@50 1156
c@50 1157 fun is_newest_locally context (libname, branch) =
c@50 1158 case hg_command_output context libname
c@50 1159 ["log", "-l1",
c@50 1160 "-b", branch_name branch,
c@50 1161 "--template", "{node}"] of
c@50 1162 ERROR e => ERROR e
c@50 1163 | OK newest_in_repo => is_at context (libname, newest_in_repo)
c@50 1164
c@50 1165 fun pull context libname =
c@50 1166 hg_command context libname
c@50 1167 (if FileBits.verbose ()
c@50 1168 then ["pull"]
c@50 1169 else ["pull", "-q"])
c@50 1170
c@50 1171 fun is_newest context (libname, branch) =
c@50 1172 case is_newest_locally context (libname, branch) of
c@50 1173 ERROR e => ERROR e
c@50 1174 | OK false => OK false
c@50 1175 | OK true =>
c@50 1176 case pull context libname of
c@50 1177 ERROR e => ERROR e
c@50 1178 | _ => is_newest_locally context (libname, branch)
c@50 1179
c@50 1180 fun is_modified_locally context libname =
c@50 1181 case current_state context libname of
c@50 1182 ERROR e => ERROR e
c@50 1183 | OK { modified, ... } => OK modified
c@50 1184
c@50 1185 fun checkout context (libname, source, branch) =
c@50 1186 let val url = remote_for context (libname, source)
c@50 1187 in
c@50 1188 case FileBits.mkpath (FileBits.extpath context) of
c@50 1189 ERROR e => ERROR e
c@50 1190 | _ => hg_command context ""
c@50 1191 ["clone", "-u", branch_name branch,
c@50 1192 url, libname]
c@50 1193 end
c@50 1194
c@50 1195 fun update context (libname, branch) =
c@50 1196 let val pull_result = pull context libname
c@50 1197 in
c@50 1198 case hg_command context libname ["update", branch_name branch] of
c@50 1199 ERROR e => ERROR e
c@50 1200 | _ =>
c@50 1201 case pull_result of
c@50 1202 ERROR e => ERROR e
c@50 1203 | _ => id_of context libname
c@50 1204 end
c@50 1205
c@50 1206 fun update_to context (libname, "") =
c@50 1207 ERROR "Non-empty id (tag or revision id) required for update_to"
c@50 1208 | update_to context (libname, id) =
c@50 1209 case hg_command context libname ["update", "-r" ^ id] of
c@50 1210 OK () => id_of context libname
c@50 1211 | ERROR _ =>
c@50 1212 case pull context libname of
c@50 1213 ERROR e => ERROR e
c@50 1214 | _ =>
c@50 1215 case hg_command context libname ["update", "-r" ^ id] of
c@50 1216 ERROR e => ERROR e
c@50 1217 | _ => id_of context libname
c@50 1218
c@50 1219 end
c@50 1220
c@50 1221 structure GitControl :> VCS_CONTROL = struct
c@50 1222
c@50 1223 (* With Git repos we always operate in detached HEAD state. Even
c@50 1224 the master branch is checked out using the remote reference,
c@50 1225 origin/master. *)
c@50 1226
c@50 1227 fun git_command context libname args =
c@50 1228 FileBits.command context libname ("git" :: args)
c@50 1229
c@50 1230 fun git_command_output context libname args =
c@50 1231 FileBits.command_output context libname ("git" :: args)
c@50 1232
c@50 1233 fun exists context libname =
c@50 1234 OK (OS.FileSys.isDir (FileBits.subpath context libname ".git"))
c@50 1235 handle _ => OK false
c@50 1236
c@50 1237 fun remote_for context (libname, source) =
c@50 1238 Provider.remote_url context GIT source libname
c@50 1239
c@50 1240 fun branch_name branch = case branch of
c@50 1241 DEFAULT_BRANCH => "master"
c@50 1242 | BRANCH "" => "master"
c@50 1243 | BRANCH b => b
c@50 1244
c@50 1245 fun remote_branch_name branch = "origin/" ^ branch_name branch
c@50 1246
c@50 1247 fun checkout context (libname, source, branch) =
c@50 1248 let val url = remote_for context (libname, source)
c@50 1249 in
c@50 1250 case FileBits.mkpath (FileBits.extpath context) of
c@50 1251 OK () => git_command context ""
c@50 1252 ["clone", "-b",
c@50 1253 branch_name branch,
c@50 1254 url, libname]
c@50 1255 | ERROR e => ERROR e
c@50 1256 end
c@50 1257
c@50 1258 (* NB git rev-parse HEAD shows revision id of current checkout;
c@50 1259 git rev-list -1 <tag> shows revision id of revision with that tag *)
c@50 1260
c@50 1261 fun id_of context libname =
c@50 1262 git_command_output context libname ["rev-parse", "HEAD"]
c@50 1263
c@50 1264 fun is_at context (libname, id_or_tag) =
c@50 1265 case id_of context libname of
c@50 1266 ERROR e => ERROR e
c@50 1267 | OK id =>
c@50 1268 if String.isPrefix id_or_tag id orelse
c@50 1269 String.isPrefix id id_or_tag
c@50 1270 then OK true
c@50 1271 else
c@50 1272 case git_command_output context libname
c@50 1273 ["rev-list", "-1", id_or_tag] of
c@50 1274 ERROR e => OK false (* id_or_tag is not an id or tag, but
c@50 1275 that could just mean it hasn't been
c@50 1276 fetched *)
c@50 1277 | OK tid => OK (tid = id)
c@50 1278
c@50 1279 fun branch_tip context (libname, branch) =
c@50 1280 git_command_output context libname
c@50 1281 ["rev-list", "-1",
c@50 1282 remote_branch_name branch]
c@50 1283
c@50 1284 fun is_newest_locally context (libname, branch) =
c@50 1285 case branch_tip context (libname, branch) of
c@50 1286 ERROR e => ERROR e
c@50 1287 | OK rev => is_at context (libname, rev)
c@50 1288
c@50 1289 fun is_on_branch context (libname, branch) =
c@50 1290 case branch_tip context (libname, branch) of
c@50 1291 ERROR e => ERROR e
c@50 1292 | OK rev =>
c@50 1293 case is_at context (libname, rev) of
c@50 1294 ERROR e => ERROR e
c@50 1295 | OK true => OK true
c@50 1296 | OK false =>
c@50 1297 case git_command context libname
c@50 1298 ["merge-base", "--is-ancestor",
c@50 1299 "HEAD", remote_branch_name branch] of
c@50 1300 ERROR e => OK false (* cmd returns non-zero for no *)
c@50 1301 | _ => OK true
c@50 1302
c@50 1303 fun is_newest context (libname, branch) =
c@50 1304 case is_newest_locally context (libname, branch) of
c@50 1305 ERROR e => ERROR e
c@50 1306 | OK false => OK false
c@50 1307 | OK true =>
c@50 1308 case git_command context libname ["fetch"] of
c@50 1309 ERROR e => ERROR e
c@50 1310 | _ => is_newest_locally context (libname, branch)
c@50 1311
c@50 1312 fun is_modified_locally context libname =
c@50 1313 case git_command_output context libname ["status", "--porcelain"] of
c@50 1314 ERROR e => ERROR e
c@50 1315 | OK "" => OK false
c@50 1316 | OK _ => OK true
c@50 1317
c@50 1318 (* This function updates to the latest revision on a branch rather
c@50 1319 than to a specific id or tag. We can't just checkout the given
c@50 1320 branch, as that will succeed even if the branch isn't up to
c@50 1321 date. We could checkout the branch and then fetch and merge,
c@50 1322 but it's perhaps cleaner not to maintain a local branch at all,
c@50 1323 but instead checkout the remote branch as a detached head. *)
c@50 1324
c@50 1325 fun update context (libname, branch) =
c@50 1326 case git_command context libname ["fetch"] of
c@50 1327 ERROR e => ERROR e
c@50 1328 | _ =>
c@50 1329 case git_command context libname ["checkout", "--detach",
c@50 1330 remote_branch_name branch] of
c@50 1331 ERROR e => ERROR e
c@50 1332 | _ => id_of context libname
c@50 1333
c@50 1334 (* This function is dealing with a specific id or tag, so if we
c@50 1335 can successfully check it out (detached) then that's all we need
c@50 1336 to do. Otherwise we need to fetch and try again *)
c@50 1337
c@50 1338 fun update_to context (libname, "") =
c@50 1339 ERROR "Non-empty id (tag or revision id) required for update_to"
c@50 1340 | update_to context (libname, id) =
c@50 1341 case git_command context libname ["checkout", "--detach", id] of
c@50 1342 OK () => id_of context libname
c@50 1343 | ERROR _ =>
c@50 1344 case git_command context libname ["fetch"] of
c@50 1345 ERROR e => ERROR e
c@50 1346 | _ =>
c@50 1347 case git_command context libname ["checkout", "--detach", id] of
c@50 1348 ERROR e => ERROR e
c@50 1349 | _ => id_of context libname
c@50 1350 end
c@50 1351
c@50 1352 structure AnyLibControl :> LIB_CONTROL = struct
c@50 1353
c@50 1354 structure H = LibControlFn(HgControl)
c@50 1355 structure G = LibControlFn(GitControl)
c@50 1356
c@50 1357 fun review context (spec as { vcs, ... } : libspec) =
c@50 1358 (fn HG => H.review | GIT => G.review) vcs context spec
c@50 1359
c@50 1360 fun status context (spec as { vcs, ... } : libspec) =
c@50 1361 (fn HG => H.status | GIT => G.status) vcs context spec
c@50 1362
c@50 1363 fun update context (spec as { vcs, ... } : libspec) =
c@50 1364 (fn HG => H.update | GIT => G.update) vcs context spec
c@50 1365 end
c@50 1366
c@50 1367 fun load_libspec spec_json lock_json libname : libspec =
c@50 1368 let open JsonBits
c@50 1369 val libobj = lookup_mandatory spec_json ["libs", libname]
c@50 1370 val vcs = lookup_mandatory_string libobj ["vcs"]
c@50 1371 val retrieve = lookup_optional_string libobj
c@50 1372 val service = retrieve ["service"]
c@50 1373 val owner = retrieve ["owner"]
c@50 1374 val repo = retrieve ["repository"]
c@50 1375 val url = retrieve ["url"]
c@50 1376 val branch = retrieve ["branch"]
c@50 1377 val user_pin = retrieve ["pin"]
c@50 1378 val lock_pin = case lookup_optional lock_json ["libs", libname] of
c@50 1379 SOME ll => lookup_optional_string ll ["pin"]
c@50 1380 | NONE => NONE
c@50 1381 in
c@50 1382 {
c@50 1383 libname = libname,
c@50 1384 vcs = case vcs of
c@50 1385 "hg" => HG
c@50 1386 | "git" => GIT
c@50 1387 | other => raise Fail ("Unknown version-control system \"" ^
c@50 1388 other ^ "\""),
c@50 1389 source = case (url, service, owner, repo) of
c@50 1390 (SOME u, NONE, _, _) => URL u
c@50 1391 | (NONE, SOME ss, owner, repo) =>
c@50 1392 PROVIDER { service = ss, owner = owner, repo = repo }
c@50 1393 | _ => raise Fail ("Must have exactly one of service " ^
c@50 1394 "or url string"),
c@50 1395 pin = case lock_pin of
c@50 1396 SOME p => PINNED p
c@50 1397 | NONE =>
c@50 1398 case user_pin of
c@50 1399 SOME p => PINNED p
c@50 1400 | NONE => UNPINNED,
c@50 1401 branch = case branch of
c@50 1402 SOME b => BRANCH b
c@50 1403 | NONE => DEFAULT_BRANCH
c@50 1404 }
c@50 1405 end
c@50 1406
c@50 1407 fun load_userconfig () : userconfig =
c@50 1408 let val home = FileBits.homedir ()
c@50 1409 val conf_json =
c@50 1410 JsonBits.load_json_from
c@50 1411 (OS.Path.joinDirFile {
c@50 1412 dir = home,
c@50 1413 file = VextFilenames.user_config_file })
c@50 1414 handle IO.Io _ => Json.OBJECT []
c@50 1415 in
c@50 1416 {
c@50 1417 accounts = case JsonBits.lookup_optional conf_json ["accounts"] of
c@50 1418 NONE => []
c@50 1419 | SOME (Json.OBJECT aa) =>
c@50 1420 map (fn (k, (Json.STRING v)) =>
c@50 1421 { service = k, login = v }
c@50 1422 | _ => raise Fail
c@50 1423 "String expected for account name")
c@50 1424 aa
c@50 1425 | _ => raise Fail "Array expected for accounts",
c@50 1426 providers = Provider.load_providers conf_json
c@50 1427 }
c@50 1428 end
c@50 1429
c@50 1430 fun load_project (userconfig : userconfig) rootpath use_locks : project =
c@50 1431 let val spec_file = FileBits.project_spec_path rootpath
c@50 1432 val lock_file = FileBits.project_lock_path rootpath
c@50 1433 val _ = if OS.FileSys.access (spec_file, [OS.FileSys.A_READ])
c@50 1434 handle OS.SysErr _ => false
c@50 1435 then ()
c@50 1436 else raise Fail ("Failed to open project spec file " ^
c@50 1437 (VextFilenames.project_file) ^ " in " ^
c@50 1438 rootpath ^
c@50 1439 ".\nPlease ensure the spec file is in the " ^
c@50 1440 "project root and run this from there.")
c@50 1441 val spec_json = JsonBits.load_json_from spec_file
c@50 1442 val lock_json = if use_locks
c@50 1443 then JsonBits.load_json_from lock_file
c@50 1444 handle IO.Io _ => Json.OBJECT []
c@50 1445 else Json.OBJECT []
c@50 1446 val extdir = JsonBits.lookup_mandatory_string spec_json
c@50 1447 ["config", "extdir"]
c@50 1448 val spec_libs = JsonBits.lookup_optional spec_json ["libs"]
c@50 1449 val lock_libs = JsonBits.lookup_optional lock_json ["libs"]
c@50 1450 val providers = Provider.load_more_providers
c@50 1451 (#providers userconfig) spec_json
c@50 1452 val libnames = case spec_libs of
c@50 1453 NONE => []
c@50 1454 | SOME (Json.OBJECT ll) => map (fn (k, v) => k) ll
c@50 1455 | _ => raise Fail "Object expected for libs"
c@50 1456 in
c@50 1457 {
c@50 1458 context = {
c@50 1459 rootpath = rootpath,
c@50 1460 extdir = extdir,
c@50 1461 providers = providers,
c@50 1462 accounts = #accounts userconfig
c@50 1463 },
c@50 1464 libs = map (load_libspec spec_json lock_json) libnames
c@50 1465 }
c@50 1466 end
c@50 1467
c@50 1468 fun save_lock_file rootpath locks =
c@50 1469 let val lock_file = FileBits.project_lock_path rootpath
c@50 1470 open Json
c@50 1471 val lock_json =
c@50 1472 OBJECT [
c@50 1473 ("libs", OBJECT
c@50 1474 (map (fn { libname, id_or_tag } =>
c@50 1475 (libname,
c@50 1476 OBJECT [ ("pin", STRING id_or_tag) ]))
c@50 1477 locks))
c@50 1478 ]
c@50 1479 in
c@50 1480 JsonBits.save_json_to lock_file lock_json
c@50 1481 end
c@50 1482
c@50 1483 fun pad_to n str =
c@50 1484 if n <= String.size str then str
c@50 1485 else pad_to n (str ^ " ")
c@50 1486
c@50 1487 fun hline_to 0 = ""
c@50 1488 | hline_to n = "-" ^ hline_to (n-1)
c@50 1489
c@50 1490 val libname_width = 25
c@50 1491 val libstate_width = 11
c@50 1492 val localstate_width = 9
c@50 1493 val notes_width = 5
c@50 1494 val divider = " | "
c@50 1495
c@50 1496 fun print_status_header () =
c@50 1497 print ("\r" ^ pad_to 80 "" ^ "\n " ^
c@50 1498 pad_to libname_width "Library" ^ divider ^
c@50 1499 pad_to libstate_width "State" ^ divider ^
c@50 1500 pad_to localstate_width "Local" ^ divider ^
c@50 1501 "Notes" ^ "\n " ^
c@50 1502 hline_to libname_width ^ "-+-" ^
c@50 1503 hline_to libstate_width ^ "-+-" ^
c@50 1504 hline_to localstate_width ^ "-+-" ^
c@50 1505 hline_to notes_width ^ "\n")
c@50 1506
c@50 1507 fun print_outcome_header () =
c@50 1508 print ("\r" ^ pad_to 80 "" ^ "\n " ^
c@50 1509 pad_to libname_width "Library" ^ divider ^
c@50 1510 pad_to libstate_width "Outcome" ^ divider ^
c@50 1511 "Notes" ^ "\n " ^
c@50 1512 hline_to libname_width ^ "-+-" ^
c@50 1513 hline_to libstate_width ^ "-+-" ^
c@50 1514 hline_to notes_width ^ "\n")
c@50 1515
c@50 1516 fun print_status with_network (libname, status) =
c@50 1517 let val libstate_str =
c@50 1518 case status of
c@50 1519 OK (ABSENT, _) => "Absent"
c@50 1520 | OK (CORRECT, _) => if with_network then "Correct" else "Present"
c@50 1521 | OK (SUPERSEDED, _) => "Superseded"
c@50 1522 | OK (WRONG, _) => "Wrong"
c@50 1523 | ERROR _ => "Error"
c@50 1524 val localstate_str =
c@50 1525 case status of
c@50 1526 OK (_, MODIFIED) => "Modified"
c@50 1527 | OK (_, UNMODIFIED) => "Clean"
c@50 1528 | _ => ""
c@50 1529 val error_str =
c@50 1530 case status of
c@50 1531 ERROR e => e
c@50 1532 | _ => ""
c@50 1533 in
c@50 1534 print (" " ^
c@50 1535 pad_to libname_width libname ^ divider ^
c@50 1536 pad_to libstate_width libstate_str ^ divider ^
c@50 1537 pad_to localstate_width localstate_str ^ divider ^
c@50 1538 error_str ^ "\n")
c@50 1539 end
c@50 1540
c@50 1541 fun print_update_outcome (libname, outcome) =
c@50 1542 let val outcome_str =
c@50 1543 case outcome of
c@50 1544 OK id => "Ok"
c@50 1545 | ERROR e => "Failed"
c@50 1546 val error_str =
c@50 1547 case outcome of
c@50 1548 ERROR e => e
c@50 1549 | _ => ""
c@50 1550 in
c@50 1551 print (" " ^
c@50 1552 pad_to libname_width libname ^ divider ^
c@50 1553 pad_to libstate_width outcome_str ^ divider ^
c@50 1554 error_str ^ "\n")
c@50 1555 end
c@50 1556
c@50 1557 fun act_and_print action print_header print_line (libs : libspec list) =
c@50 1558 let val lines = map (fn lib => (#libname lib, action lib)) libs
c@50 1559 val _ = print_header ()
c@50 1560 in
c@50 1561 app print_line lines;
c@50 1562 lines
c@50 1563 end
c@50 1564
c@50 1565 fun return_code_for outcomes =
c@50 1566 foldl (fn ((_, result), acc) =>
c@50 1567 case result of
c@50 1568 ERROR _ => OS.Process.failure
c@50 1569 | _ => acc)
c@50 1570 OS.Process.success
c@50 1571 outcomes
c@50 1572
c@50 1573 fun status_of_project ({ context, libs } : project) =
c@50 1574 return_code_for (act_and_print (AnyLibControl.status context)
c@50 1575 print_status_header (print_status false)
c@50 1576 libs)
c@50 1577
c@50 1578 fun review_project ({ context, libs } : project) =
c@50 1579 return_code_for (act_and_print (AnyLibControl.review context)
c@50 1580 print_status_header (print_status true)
c@50 1581 libs)
c@50 1582
c@50 1583 fun update_project ({ context, libs } : project) =
c@50 1584 let val outcomes = act_and_print
c@50 1585 (AnyLibControl.update context)
c@50 1586 print_outcome_header print_update_outcome libs
c@50 1587 val locks =
c@50 1588 List.concat
c@50 1589 (map (fn (libname, result) =>
c@50 1590 case result of
c@50 1591 ERROR _ => []
c@50 1592 | OK id => [{ libname = libname, id_or_tag = id }])
c@50 1593 outcomes)
c@50 1594 val return_code = return_code_for outcomes
c@50 1595 in
c@50 1596 if OS.Process.isSuccess return_code
c@50 1597 then save_lock_file (#rootpath context) locks
c@50 1598 else ();
c@50 1599 return_code
c@50 1600 end
c@50 1601
c@50 1602 fun load_local_project use_locks =
c@50 1603 let val userconfig = load_userconfig ()
c@50 1604 val rootpath = OS.FileSys.getDir ()
c@50 1605 in
c@50 1606 load_project userconfig rootpath use_locks
c@50 1607 end
c@50 1608
c@50 1609 fun with_local_project use_locks f =
c@50 1610 let val return_code = f (load_local_project use_locks)
c@50 1611 handle e =>
c@50 1612 (print ("Failed with exception: " ^
c@50 1613 (exnMessage e) ^ "\n");
c@50 1614 OS.Process.failure)
c@50 1615 val _ = print "\n";
c@50 1616 in
c@50 1617 return_code
c@50 1618 end
c@50 1619
c@50 1620 fun review () = with_local_project false review_project
c@50 1621 fun status () = with_local_project false status_of_project
c@50 1622 fun update () = with_local_project false update_project
c@50 1623 fun install () = with_local_project true update_project
c@50 1624
c@50 1625 fun version () =
c@50 1626 (print ("v" ^ vext_version ^ "\n");
c@50 1627 OS.Process.success)
c@50 1628
c@50 1629 fun usage () =
c@50 1630 (print "\nVext ";
c@50 1631 version ();
c@50 1632 print ("\nA simple manager for third-party source code dependencies.\n\n"
c@50 1633 ^ "Usage:\n\n"
c@50 1634 ^ " vext <command>\n\n"
c@50 1635 ^ "where <command> is one of:\n\n"
c@50 1636 ^ " status print quick report on local status only, without using network\n"
c@50 1637 ^ " review check configured libraries against their providers, and report\n"
c@50 1638 ^ " install update configured libraries according to project specs and lock file\n"
c@50 1639 ^ " update update configured libraries and lock file according to project specs\n"
c@50 1640 ^ " version print the Vext version number and exit\n\n");
c@50 1641 OS.Process.failure)
c@50 1642
c@50 1643 fun vext args =
c@50 1644 let val return_code =
c@50 1645 case args of
c@50 1646 ["review"] => review ()
c@50 1647 | ["status"] => status ()
c@50 1648 | ["install"] => install ()
c@50 1649 | ["update"] => update ()
c@50 1650 | ["version"] => version ()
c@50 1651 | _ => usage ()
c@50 1652 in
c@50 1653 OS.Process.exit return_code;
c@50 1654 ()
c@50 1655 end
c@50 1656
c@50 1657 fun main () =
c@50 1658 vext (CommandLine.arguments ())