annotate vext.sml @ 1752:716e13004b19

Update Vext and subrepos
author Chris Cannam
date Thu, 09 Nov 2017 15:59:18 +0000
parents ffe59b457557
children edaa018a3731
rev   line source
Chris@1746 1 (*
Chris@1746 2 DO NOT EDIT THIS FILE.
Chris@1746 3 This file is automatically generated from the individual
Chris@1746 4 source files in the Vext repository.
Chris@1746 5 *)
Chris@1706 6
Chris@1706 7 (*
Chris@1706 8 Vext
Chris@1706 9
Chris@1706 10 A simple manager for third-party source code dependencies
Chris@1706 11
Chris@1746 12 Copyright 2017 Chris Cannam, Particular Programs Ltd,
Chris@1746 13 and Queen Mary, University of London
Chris@1706 14
Chris@1706 15 Permission is hereby granted, free of charge, to any person
Chris@1706 16 obtaining a copy of this software and associated documentation
Chris@1706 17 files (the "Software"), to deal in the Software without
Chris@1706 18 restriction, including without limitation the rights to use, copy,
Chris@1706 19 modify, merge, publish, distribute, sublicense, and/or sell copies
Chris@1706 20 of the Software, and to permit persons to whom the Software is
Chris@1706 21 furnished to do so, subject to the following conditions:
Chris@1706 22
Chris@1706 23 The above copyright notice and this permission notice shall be
Chris@1706 24 included in all copies or substantial portions of the Software.
Chris@1706 25
Chris@1706 26 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
Chris@1706 27 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
Chris@1706 28 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
Chris@1706 29 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
Chris@1706 30 ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
Chris@1706 31 CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
Chris@1706 32 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Chris@1706 33
Chris@1746 34 Except as contained in this notice, the names of Chris Cannam,
Chris@1746 35 Particular Programs Ltd, and Queen Mary, University of London
Chris@1746 36 shall not be used in advertising or otherwise to promote the sale,
Chris@1746 37 use or other dealings in this Software without prior written
Chris@1746 38 authorization.
Chris@1706 39 *)
Chris@1706 40
Chris@1752 41 val vext_version = "0.9.91"
Chris@1706 42
Chris@1706 43
Chris@1706 44 datatype vcs =
Chris@1706 45 HG |
Chris@1706 46 GIT
Chris@1706 47
Chris@1706 48 datatype source =
Chris@1721 49 URL_SOURCE of string |
Chris@1721 50 SERVICE_SOURCE of {
Chris@1706 51 service : string,
Chris@1706 52 owner : string option,
Chris@1706 53 repo : string option
Chris@1706 54 }
Chris@1706 55
Chris@1740 56 type id_or_tag = string
Chris@1740 57
Chris@1706 58 datatype pin =
Chris@1706 59 UNPINNED |
Chris@1740 60 PINNED of id_or_tag
Chris@1706 61
Chris@1706 62 datatype libstate =
Chris@1706 63 ABSENT |
Chris@1706 64 CORRECT |
Chris@1706 65 SUPERSEDED |
Chris@1706 66 WRONG
Chris@1706 67
Chris@1706 68 datatype localstate =
Chris@1706 69 MODIFIED |
Chris@1740 70 LOCK_MISMATCHED |
Chris@1740 71 CLEAN
Chris@1706 72
Chris@1706 73 datatype branch =
Chris@1706 74 BRANCH of string |
Chris@1706 75 DEFAULT_BRANCH
Chris@1706 76
Chris@1706 77 (* If we can recover from an error, for example by reporting failure
Chris@1706 78 for this one thing and going on to the next thing, then the error
Chris@1706 79 should usually be returned through a result type rather than an
Chris@1706 80 exception. *)
Chris@1706 81
Chris@1706 82 datatype 'a result =
Chris@1706 83 OK of 'a |
Chris@1706 84 ERROR of string
Chris@1706 85
Chris@1706 86 type libname = string
Chris@1706 87
Chris@1706 88 type libspec = {
Chris@1706 89 libname : libname,
Chris@1706 90 vcs : vcs,
Chris@1706 91 source : source,
Chris@1706 92 branch : branch,
Chris@1740 93 project_pin : pin,
Chris@1740 94 lock_pin : pin
Chris@1706 95 }
Chris@1706 96
Chris@1706 97 type lock = {
Chris@1706 98 libname : libname,
Chris@1706 99 id_or_tag : id_or_tag
Chris@1706 100 }
Chris@1740 101
Chris@1706 102 type remote_spec = {
Chris@1706 103 anon : string option,
Chris@1706 104 auth : string option
Chris@1706 105 }
Chris@1706 106
Chris@1706 107 type provider = {
Chris@1706 108 service : string,
Chris@1706 109 supports : vcs list,
Chris@1706 110 remote_spec : remote_spec
Chris@1706 111 }
Chris@1706 112
Chris@1706 113 type account = {
Chris@1706 114 service : string,
Chris@1706 115 login : string
Chris@1706 116 }
Chris@1706 117
Chris@1706 118 type context = {
Chris@1706 119 rootpath : string,
Chris@1706 120 extdir : string,
Chris@1706 121 providers : provider list,
Chris@1706 122 accounts : account list
Chris@1706 123 }
Chris@1706 124
Chris@1706 125 type userconfig = {
Chris@1706 126 providers : provider list,
Chris@1706 127 accounts : account list
Chris@1706 128 }
Chris@1706 129
Chris@1706 130 type project = {
Chris@1706 131 context : context,
Chris@1706 132 libs : libspec list
Chris@1706 133 }
Chris@1706 134
Chris@1706 135 structure VextFilenames = struct
Chris@1706 136 val project_file = "vext-project.json"
Chris@1706 137 val project_lock_file = "vext-lock.json"
Chris@1706 138 val user_config_file = ".vext.json"
Chris@1746 139 val archive_dir = ".vext-archive"
Chris@1706 140 end
Chris@1706 141
Chris@1706 142 signature VCS_CONTROL = sig
Chris@1706 143
Chris@1706 144 (** Test whether the library is present locally at all *)
Chris@1706 145 val exists : context -> libname -> bool result
Chris@1706 146
Chris@1706 147 (** Return the id (hash) of the current revision for the library *)
Chris@1706 148 val id_of : context -> libname -> id_or_tag result
Chris@1706 149
Chris@1706 150 (** Test whether the library is at the given id *)
Chris@1706 151 val is_at : context -> libname * id_or_tag -> bool result
Chris@1706 152
Chris@1706 153 (** Test whether the library is on the given branch, i.e. is at
Chris@1706 154 the branch tip or an ancestor of it *)
Chris@1706 155 val is_on_branch : context -> libname * branch -> bool result
Chris@1706 156
Chris@1706 157 (** Test whether the library is at the newest revision for the
Chris@1706 158 given branch. False may indicate that the branch has advanced
Chris@1706 159 or that the library is not on the branch at all. This function
Chris@1706 160 may use the network to check for new revisions *)
Chris@1752 161 val is_newest : context -> libname * source * branch -> bool result
Chris@1706 162
Chris@1706 163 (** Test whether the library is at the newest revision available
Chris@1706 164 locally for the given branch. False may indicate that the
Chris@1706 165 branch has advanced or that the library is not on the branch
Chris@1706 166 at all. This function must not use the network *)
Chris@1706 167 val is_newest_locally : context -> libname * branch -> bool result
Chris@1706 168
Chris@1706 169 (** Test whether the library has been modified in the local
Chris@1706 170 working copy *)
Chris@1706 171 val is_modified_locally : context -> libname -> bool result
Chris@1706 172
Chris@1706 173 (** Check out, i.e. clone a fresh copy of, the repo for the given
Chris@1706 174 library on the given branch *)
Chris@1706 175 val checkout : context -> libname * source * branch -> unit result
Chris@1706 176
Chris@1706 177 (** Update the library to the given branch tip *)
Chris@1752 178 val update : context -> libname * source * branch -> id_or_tag result
Chris@1706 179
Chris@1706 180 (** Update the library to the given specific id or tag *)
Chris@1752 181 val update_to : context -> libname * source * id_or_tag -> id_or_tag result
Chris@1706 182 end
Chris@1706 183
Chris@1706 184 signature LIB_CONTROL = sig
Chris@1706 185 val review : context -> libspec -> (libstate * localstate) result
Chris@1706 186 val status : context -> libspec -> (libstate * localstate) result
Chris@1706 187 val update : context -> libspec -> id_or_tag result
Chris@1740 188 val id_of : context -> libspec -> id_or_tag result
Chris@1706 189 end
Chris@1706 190
Chris@1706 191 structure FileBits :> sig
Chris@1706 192 val extpath : context -> string
Chris@1706 193 val libpath : context -> libname -> string
Chris@1706 194 val subpath : context -> libname -> string -> string
Chris@1706 195 val command_output : context -> libname -> string list -> string result
Chris@1706 196 val command : context -> libname -> string list -> unit result
Chris@1706 197 val file_contents : string -> string
Chris@1706 198 val mydir : unit -> string
Chris@1706 199 val homedir : unit -> string
Chris@1706 200 val mkpath : string -> unit result
Chris@1746 201 val rmpath : string -> unit result
Chris@1706 202 val project_spec_path : string -> string
Chris@1706 203 val project_lock_path : string -> string
Chris@1706 204 val verbose : unit -> bool
Chris@1706 205 end = struct
Chris@1706 206
Chris@1706 207 fun verbose () =
Chris@1706 208 case OS.Process.getEnv "VEXT_VERBOSE" of
Chris@1706 209 SOME "0" => false
Chris@1706 210 | SOME _ => true
Chris@1706 211 | NONE => false
Chris@1706 212
Chris@1752 213 fun split_relative path desc =
Chris@1752 214 case OS.Path.fromString path of
Chris@1752 215 { isAbs = true, ... } => raise Fail (desc ^ " may not be absolute")
Chris@1752 216 | { arcs, ... } => arcs
Chris@1752 217
Chris@1706 218 fun extpath ({ rootpath, extdir, ... } : context) =
Chris@1706 219 let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
Chris@1706 220 in OS.Path.toString {
Chris@1706 221 isAbs = isAbs,
Chris@1706 222 vol = vol,
Chris@1752 223 arcs = arcs @
Chris@1752 224 split_relative extdir "extdir"
Chris@1706 225 }
Chris@1706 226 end
Chris@1706 227
Chris@1706 228 fun subpath ({ rootpath, extdir, ... } : context) libname remainder =
Chris@1706 229 (* NB libname is allowed to be a path fragment, e.g. foo/bar *)
Chris@1706 230 let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
Chris@1706 231 in OS.Path.toString {
Chris@1706 232 isAbs = isAbs,
Chris@1706 233 vol = vol,
Chris@1752 234 arcs = arcs @
Chris@1752 235 split_relative extdir "extdir" @
Chris@1752 236 split_relative libname "library path" @
Chris@1752 237 split_relative remainder "subpath"
Chris@1706 238 }
Chris@1706 239 end
Chris@1706 240
Chris@1706 241 fun libpath context "" =
Chris@1706 242 extpath context
Chris@1706 243 | libpath context libname =
Chris@1706 244 subpath context libname ""
Chris@1706 245
Chris@1706 246 fun project_file_path rootpath filename =
Chris@1706 247 let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
Chris@1706 248 in OS.Path.toString {
Chris@1706 249 isAbs = isAbs,
Chris@1706 250 vol = vol,
Chris@1706 251 arcs = arcs @ [ filename ]
Chris@1706 252 }
Chris@1706 253 end
Chris@1706 254
Chris@1706 255 fun project_spec_path rootpath =
Chris@1706 256 project_file_path rootpath (VextFilenames.project_file)
Chris@1706 257
Chris@1706 258 fun project_lock_path rootpath =
Chris@1706 259 project_file_path rootpath (VextFilenames.project_lock_file)
Chris@1706 260
Chris@1706 261 fun trim str =
Chris@1706 262 hd (String.fields (fn x => x = #"\n" orelse x = #"\r") str)
Chris@1706 263
Chris@1706 264 fun file_contents filename =
Chris@1706 265 let val stream = TextIO.openIn filename
Chris@1706 266 fun read_all str acc =
Chris@1706 267 case TextIO.inputLine str of
Chris@1706 268 SOME line => read_all str (trim line :: acc)
Chris@1706 269 | NONE => rev acc
Chris@1706 270 val contents = read_all stream []
Chris@1706 271 val _ = TextIO.closeIn stream
Chris@1706 272 in
Chris@1706 273 String.concatWith "\n" contents
Chris@1706 274 end
Chris@1706 275
Chris@1706 276 fun expand_commandline cmdlist =
Chris@1706 277 (* We are quite [too] strict about what we accept here, except
Chris@1706 278 for the first element in cmdlist which is assumed to be a
Chris@1706 279 known command location rather than arbitrary user input. NB
Chris@1706 280 only ASCII accepted at this point. *)
Chris@1706 281 let open Char
Chris@1706 282 fun quote arg =
Chris@1706 283 if List.all
Chris@1706 284 (fn c => isAlphaNum c orelse c = #"-" orelse c = #"_")
Chris@1706 285 (explode arg)
Chris@1706 286 then arg
Chris@1706 287 else "\"" ^ arg ^ "\""
Chris@1706 288 fun check arg =
Chris@1706 289 let val valid = explode " /#:;?,._-{}@="
Chris@1706 290 in
Chris@1706 291 app (fn c =>
Chris@1706 292 if isAlphaNum c orelse
Chris@1706 293 List.exists (fn v => v = c) valid
Chris@1706 294 then ()
Chris@1706 295 else raise Fail ("Invalid character '" ^
Chris@1706 296 (Char.toString c) ^
Chris@1706 297 "' in command list"))
Chris@1706 298 (explode arg);
Chris@1706 299 arg
Chris@1706 300 end
Chris@1706 301 in
Chris@1706 302 String.concatWith " "
Chris@1706 303 (map quote
Chris@1706 304 (hd cmdlist :: map check (tl cmdlist)))
Chris@1706 305 end
Chris@1706 306
Chris@1706 307 val tick_cycle = ref 0
Chris@1706 308 val tick_chars = Vector.fromList (map String.str (explode "|/-\\"))
Chris@1706 309
Chris@1716 310 fun tick libname cmdlist =
Chris@1706 311 let val n = Vector.length tick_chars
Chris@1706 312 fun pad_to n str =
Chris@1716 313 if n <= String.size str then str
Chris@1716 314 else pad_to n (str ^ " ")
Chris@1716 315 val name = if libname <> "" then libname
Chris@1716 316 else if cmdlist = nil then ""
Chris@1716 317 else hd (rev cmdlist)
Chris@1706 318 in
Chris@1716 319 print (" " ^
Chris@1706 320 Vector.sub(tick_chars, !tick_cycle) ^ " " ^
Chris@1716 321 pad_to 24 name ^
Chris@1716 322 "\r");
Chris@1706 323 tick_cycle := (if !tick_cycle = n - 1 then 0 else 1 + !tick_cycle)
Chris@1706 324 end
Chris@1706 325
Chris@1706 326 fun run_command context libname cmdlist redirect =
Chris@1706 327 let open OS
Chris@1706 328 val dir = libpath context libname
Chris@1706 329 val cmd = expand_commandline cmdlist
Chris@1706 330 val _ = if verbose ()
Chris@1706 331 then print ("Running: " ^ cmd ^
Chris@1706 332 " (in dir " ^ dir ^ ")...\n")
Chris@1716 333 else tick libname cmdlist
Chris@1706 334 val _ = FileSys.chDir dir
Chris@1706 335 val status = case redirect of
Chris@1706 336 NONE => Process.system cmd
Chris@1706 337 | SOME file => Process.system (cmd ^ ">" ^ file)
Chris@1706 338 in
Chris@1706 339 if Process.isSuccess status
Chris@1706 340 then OK ()
Chris@1706 341 else ERROR ("Command failed: " ^ cmd ^ " (in dir " ^ dir ^ ")")
Chris@1706 342 end
Chris@1706 343 handle ex => ERROR ("Unable to run command: " ^ exnMessage ex)
Chris@1706 344
Chris@1706 345 fun command context libname cmdlist =
Chris@1706 346 run_command context libname cmdlist NONE
Chris@1706 347
Chris@1706 348 fun command_output context libname cmdlist =
Chris@1706 349 let open OS
Chris@1706 350 val tmpFile = FileSys.tmpName ()
Chris@1706 351 val result = run_command context libname cmdlist (SOME tmpFile)
Chris@1706 352 val contents = file_contents tmpFile
Chris@1706 353 in
Chris@1706 354 FileSys.remove tmpFile handle _ => ();
Chris@1706 355 case result of
Chris@1706 356 OK () => OK contents
Chris@1706 357 | ERROR e => ERROR e
Chris@1706 358 end
Chris@1706 359
Chris@1706 360 fun mydir () =
Chris@1706 361 let open OS
Chris@1706 362 val { dir, file } = Path.splitDirFile (CommandLine.name ())
Chris@1706 363 in
Chris@1706 364 FileSys.realPath
Chris@1706 365 (if Path.isAbsolute dir
Chris@1706 366 then dir
Chris@1706 367 else Path.concat (FileSys.getDir (), dir))
Chris@1706 368 end
Chris@1706 369
Chris@1706 370 fun homedir () =
Chris@1706 371 (* Failure is not routine, so we use an exception here *)
Chris@1706 372 case (OS.Process.getEnv "HOME",
Chris@1706 373 OS.Process.getEnv "HOMEPATH") of
Chris@1706 374 (SOME home, _) => home
Chris@1706 375 | (NONE, SOME home) => home
Chris@1706 376 | (NONE, NONE) =>
Chris@1706 377 raise Fail "Failed to look up home directory from environment"
Chris@1706 378
Chris@1747 379 fun mkpath' path =
Chris@1706 380 if OS.FileSys.isDir path handle _ => false
Chris@1706 381 then OK ()
Chris@1706 382 else case OS.Path.fromString path of
Chris@1706 383 { arcs = nil, ... } => OK ()
Chris@1706 384 | { isAbs = false, ... } => ERROR "mkpath requires absolute path"
Chris@1706 385 | { isAbs, vol, arcs } =>
Chris@1747 386 case mkpath' (OS.Path.toString { (* parent *)
Chris@1747 387 isAbs = isAbs,
Chris@1747 388 vol = vol,
Chris@1747 389 arcs = rev (tl (rev arcs)) }) of
Chris@1706 390 ERROR e => ERROR e
Chris@1706 391 | OK () => ((OS.FileSys.mkDir path; OK ())
Chris@1706 392 handle OS.SysErr (e, _) =>
Chris@1706 393 ERROR ("Directory creation failed: " ^ e))
Chris@1746 394
Chris@1747 395 fun mkpath path =
Chris@1752 396 mkpath' (OS.Path.mkCanonical path)
Chris@1747 397
Chris@1752 398 fun rmpath' path =
Chris@1746 399 let open OS
Chris@1746 400 fun files_from dirstream =
Chris@1746 401 case FileSys.readDir dirstream of
Chris@1746 402 NONE => []
Chris@1746 403 | SOME file =>
Chris@1746 404 (* readDir is supposed to filter these,
Chris@1746 405 but let's be extra cautious: *)
Chris@1746 406 if file = Path.parentArc orelse file = Path.currentArc
Chris@1746 407 then files_from dirstream
Chris@1746 408 else file :: files_from dirstream
Chris@1746 409 fun contents dir =
Chris@1746 410 let val stream = FileSys.openDir dir
Chris@1746 411 val files = map (fn f => Path.joinDirFile
Chris@1746 412 { dir = dir, file = f })
Chris@1746 413 (files_from stream)
Chris@1746 414 val _ = FileSys.closeDir stream
Chris@1746 415 in files
Chris@1746 416 end
Chris@1746 417 fun remove path =
Chris@1746 418 if FileSys.isLink path (* dangling links bother isDir *)
Chris@1746 419 then FileSys.remove path
Chris@1746 420 else if FileSys.isDir path
Chris@1746 421 then (app remove (contents path); FileSys.rmDir path)
Chris@1746 422 else FileSys.remove path
Chris@1746 423 in
Chris@1746 424 (remove path; OK ())
Chris@1746 425 handle SysErr (e, _) => ERROR ("Path removal failed: " ^ e)
Chris@1746 426 end
Chris@1752 427
Chris@1752 428 fun rmpath path =
Chris@1752 429 rmpath' (OS.Path.mkCanonical path)
Chris@1752 430
Chris@1706 431 end
Chris@1706 432
Chris@1706 433 functor LibControlFn (V: VCS_CONTROL) :> LIB_CONTROL = struct
Chris@1706 434
Chris@1706 435 (* Valid states for unpinned libraries:
Chris@1706 436
Chris@1706 437 - CORRECT: We are on the right branch and are up-to-date with
Chris@1706 438 it as far as we can tell. (If not using the network, this
Chris@1706 439 should be reported to user as "Present" rather than "Correct"
Chris@1706 440 as the remote repo may have advanced without us knowing.)
Chris@1706 441
Chris@1706 442 - SUPERSEDED: We are on the right branch but we can see that
Chris@1706 443 there is a newer revision either locally or on the remote (in
Chris@1706 444 Git terms, we are at an ancestor of the desired branch tip).
Chris@1706 445
Chris@1706 446 - WRONG: We are on the wrong branch (in Git terms, we are not
Chris@1706 447 at the desired branch tip or any ancestor of it).
Chris@1706 448
Chris@1706 449 - ABSENT: Repo doesn't exist here at all.
Chris@1706 450
Chris@1706 451 Valid states for pinned libraries:
Chris@1706 452
Chris@1706 453 - CORRECT: We are at the pinned revision.
Chris@1706 454
Chris@1706 455 - WRONG: We are at any revision other than the pinned one.
Chris@1706 456
Chris@1706 457 - ABSENT: Repo doesn't exist here at all.
Chris@1706 458 *)
Chris@1706 459
Chris@1740 460 fun check with_network context
Chris@1752 461 ({ libname, source, branch,
Chris@1752 462 project_pin, lock_pin, ... } : libspec) =
Chris@1706 463 let fun check_unpinned () =
Chris@1752 464 let val newest =
Chris@1752 465 if with_network
Chris@1752 466 then V.is_newest context (libname, source, branch)
Chris@1752 467 else V.is_newest_locally context (libname, branch)
Chris@1706 468 in
Chris@1752 469 case newest of
Chris@1706 470 ERROR e => ERROR e
Chris@1706 471 | OK true => OK CORRECT
Chris@1706 472 | OK false =>
Chris@1706 473 case V.is_on_branch context (libname, branch) of
Chris@1706 474 ERROR e => ERROR e
Chris@1706 475 | OK true => OK SUPERSEDED
Chris@1706 476 | OK false => OK WRONG
Chris@1706 477 end
Chris@1706 478 fun check_pinned target =
Chris@1706 479 case V.is_at context (libname, target) of
Chris@1706 480 ERROR e => ERROR e
Chris@1706 481 | OK true => OK CORRECT
Chris@1706 482 | OK false => OK WRONG
Chris@1740 483 fun check_remote () =
Chris@1740 484 case project_pin of
Chris@1706 485 UNPINNED => check_unpinned ()
Chris@1706 486 | PINNED target => check_pinned target
Chris@1740 487 fun check_local () =
Chris@1740 488 case V.is_modified_locally context libname of
Chris@1740 489 ERROR e => ERROR e
Chris@1740 490 | OK true => OK MODIFIED
Chris@1740 491 | OK false =>
Chris@1740 492 case lock_pin of
Chris@1740 493 UNPINNED => OK CLEAN
Chris@1740 494 | PINNED target =>
Chris@1740 495 case V.is_at context (libname, target) of
Chris@1740 496 ERROR e => ERROR e
Chris@1740 497 | OK true => OK CLEAN
Chris@1740 498 | OK false => OK LOCK_MISMATCHED
Chris@1706 499 in
Chris@1706 500 case V.exists context libname of
Chris@1706 501 ERROR e => ERROR e
Chris@1740 502 | OK false => OK (ABSENT, CLEAN)
Chris@1706 503 | OK true =>
Chris@1740 504 case (check_remote (), check_local ()) of
Chris@1706 505 (ERROR e, _) => ERROR e
Chris@1706 506 | (_, ERROR e) => ERROR e
Chris@1740 507 | (OK r, OK l) => OK (r, l)
Chris@1706 508 end
Chris@1706 509
Chris@1706 510 val review = check true
Chris@1706 511 val status = check false
Chris@1740 512
Chris@1740 513 fun update context
Chris@1740 514 ({ libname, source, branch,
Chris@1740 515 project_pin, lock_pin, ... } : libspec) =
Chris@1706 516 let fun update_unpinned () =
Chris@1752 517 case V.is_newest context (libname, source, branch) of
Chris@1706 518 ERROR e => ERROR e
Chris@1706 519 | OK true => V.id_of context libname
Chris@1752 520 | OK false => V.update context (libname, source, branch)
Chris@1706 521 fun update_pinned target =
Chris@1706 522 case V.is_at context (libname, target) of
Chris@1706 523 ERROR e => ERROR e
Chris@1706 524 | OK true => OK target
Chris@1752 525 | OK false => V.update_to context (libname, source, target)
Chris@1706 526 fun update' () =
Chris@1740 527 case lock_pin of
Chris@1740 528 PINNED target => update_pinned target
Chris@1740 529 | UNPINNED =>
Chris@1740 530 case project_pin of
Chris@1740 531 PINNED target => update_pinned target
Chris@1740 532 | UNPINNED => update_unpinned ()
Chris@1706 533 in
Chris@1706 534 case V.exists context libname of
Chris@1706 535 ERROR e => ERROR e
Chris@1706 536 | OK true => update' ()
Chris@1706 537 | OK false =>
Chris@1706 538 case V.checkout context (libname, source, branch) of
Chris@1706 539 ERROR e => ERROR e
Chris@1706 540 | OK () => update' ()
Chris@1706 541 end
Chris@1740 542
Chris@1740 543 fun id_of context ({ libname, ... } : libspec) =
Chris@1740 544 V.id_of context libname
Chris@1740 545
Chris@1706 546 end
Chris@1706 547
Chris@1706 548 (* Simple Standard ML JSON parser
Chris@1706 549 ==============================
Chris@1706 550
Chris@1706 551 https://bitbucket.org/cannam/sml-simplejson
Chris@1706 552
Chris@1706 553 An RFC-compliant JSON parser in one SML file with no dependency
Chris@1706 554 on anything outside the Basis library. Also includes a simple
Chris@1706 555 serialiser.
Chris@1706 556
Chris@1706 557 Tested with MLton, Poly/ML, and SML/NJ compilers.
Chris@1706 558
Chris@1706 559 Parser notes:
Chris@1706 560
Chris@1706 561 * Complies with RFC 7159, The JavaScript Object Notation (JSON)
Chris@1706 562 Data Interchange Format
Chris@1706 563
Chris@1706 564 * Passes all of the JSONTestSuite parser accept/reject tests that
Chris@1706 565 exist at the time of writing, as listed in "Parsing JSON is a
Chris@1706 566 Minefield" (http://seriot.ch/parsing_json.php)
Chris@1706 567
Chris@1706 568 * Two-pass parser using naive exploded strings, therefore not
Chris@1706 569 particularly fast and not suitable for large input files
Chris@1706 570
Chris@1706 571 * Only supports UTF-8 input, not UTF-16 or UTF-32. Doesn't check
Chris@1706 572 that JSON strings are valid UTF-8 -- the caller must do that --
Chris@1706 573 but does handle \u escapes
Chris@1706 574
Chris@1706 575 * Converts all numbers to type "real". If that is a 64-bit IEEE
Chris@1706 576 float type (common but not guaranteed in SML) then we're pretty
Chris@1706 577 standard for a JSON parser
Chris@1706 578
Chris@1706 579 Copyright 2017 Chris Cannam.
Chris@1706 580 Parts based on the JSON parser in the Ponyo library by Phil Eaton.
Chris@1706 581
Chris@1706 582 Permission is hereby granted, free of charge, to any person
Chris@1706 583 obtaining a copy of this software and associated documentation
Chris@1706 584 files (the "Software"), to deal in the Software without
Chris@1706 585 restriction, including without limitation the rights to use, copy,
Chris@1706 586 modify, merge, publish, distribute, sublicense, and/or sell copies
Chris@1706 587 of the Software, and to permit persons to whom the Software is
Chris@1706 588 furnished to do so, subject to the following conditions:
Chris@1706 589
Chris@1706 590 The above copyright notice and this permission notice shall be
Chris@1706 591 included in all copies or substantial portions of the Software.
Chris@1706 592
Chris@1706 593 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
Chris@1706 594 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
Chris@1706 595 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
Chris@1706 596 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
Chris@1706 597 ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
Chris@1706 598 CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
Chris@1706 599 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Chris@1706 600
Chris@1706 601 Except as contained in this notice, the names of Chris Cannam and
Chris@1706 602 Particular Programs Ltd shall not be used in advertising or
Chris@1706 603 otherwise to promote the sale, use or other dealings in this
Chris@1706 604 Software without prior written authorization.
Chris@1706 605 *)
Chris@1706 606
Chris@1706 607 signature JSON = sig
Chris@1706 608
Chris@1706 609 datatype json = OBJECT of (string * json) list
Chris@1706 610 | ARRAY of json list
Chris@1706 611 | NUMBER of real
Chris@1706 612 | STRING of string
Chris@1706 613 | BOOL of bool
Chris@1706 614 | NULL
Chris@1706 615
Chris@1706 616 datatype 'a result = OK of 'a
Chris@1706 617 | ERROR of string
Chris@1706 618
Chris@1706 619 val parse : string -> json result
Chris@1706 620 val serialise : json -> string
Chris@1706 621 val serialiseIndented : json -> string
Chris@1706 622
Chris@1706 623 end
Chris@1706 624
Chris@1706 625 structure Json :> JSON = struct
Chris@1706 626
Chris@1706 627 datatype json = OBJECT of (string * json) list
Chris@1706 628 | ARRAY of json list
Chris@1706 629 | NUMBER of real
Chris@1706 630 | STRING of string
Chris@1706 631 | BOOL of bool
Chris@1706 632 | NULL
Chris@1706 633
Chris@1706 634 datatype 'a result = OK of 'a
Chris@1706 635 | ERROR of string
Chris@1706 636
Chris@1706 637 structure T = struct
Chris@1706 638 datatype token = NUMBER of char list
Chris@1706 639 | STRING of string
Chris@1706 640 | BOOL of bool
Chris@1706 641 | NULL
Chris@1706 642 | CURLY_L
Chris@1706 643 | CURLY_R
Chris@1706 644 | SQUARE_L
Chris@1706 645 | SQUARE_R
Chris@1706 646 | COLON
Chris@1706 647 | COMMA
Chris@1706 648
Chris@1706 649 fun toString t =
Chris@1706 650 case t of NUMBER digits => implode digits
Chris@1706 651 | STRING s => s
Chris@1706 652 | BOOL b => Bool.toString b
Chris@1706 653 | NULL => "null"
Chris@1706 654 | CURLY_L => "{"
Chris@1706 655 | CURLY_R => "}"
Chris@1706 656 | SQUARE_L => "["
Chris@1706 657 | SQUARE_R => "]"
Chris@1706 658 | COLON => ":"
Chris@1706 659 | COMMA => ","
Chris@1706 660 end
Chris@1706 661
Chris@1706 662 fun bmpToUtf8 cp = (* convert a codepoint in Unicode BMP to utf8 bytes *)
Chris@1706 663 let open Word
Chris@1706 664 infix 6 orb andb >>
Chris@1706 665 in
Chris@1706 666 map (Char.chr o toInt)
Chris@1706 667 (if cp < 0wx80 then
Chris@1706 668 [cp]
Chris@1706 669 else if cp < 0wx800 then
Chris@1706 670 [0wxc0 orb (cp >> 0w6), 0wx80 orb (cp andb 0wx3f)]
Chris@1706 671 else if cp < 0wx10000 then
Chris@1706 672 [0wxe0 orb (cp >> 0w12),
Chris@1706 673 0wx80 orb ((cp >> 0w6) andb 0wx3f),
Chris@1706 674 0wx80 orb (cp andb 0wx3f)]
Chris@1706 675 else raise Fail ("Invalid BMP point " ^ (Word.toString cp)))
Chris@1706 676 end
Chris@1706 677
Chris@1706 678 fun error pos text = ERROR (text ^ " at character position " ^
Chris@1706 679 Int.toString (pos - 1))
Chris@1706 680 fun token_error pos = error pos ("Unexpected token")
Chris@1706 681
Chris@1706 682 fun lexNull pos acc (#"u" :: #"l" :: #"l" :: xs) =
Chris@1706 683 lex (pos + 3) (T.NULL :: acc) xs
Chris@1706 684 | lexNull pos acc _ = token_error pos
Chris@1706 685
Chris@1706 686 and lexTrue pos acc (#"r" :: #"u" :: #"e" :: xs) =
Chris@1706 687 lex (pos + 3) (T.BOOL true :: acc) xs
Chris@1706 688 | lexTrue pos acc _ = token_error pos
Chris@1706 689
Chris@1706 690 and lexFalse pos acc (#"a" :: #"l" :: #"s" :: #"e" :: xs) =
Chris@1706 691 lex (pos + 4) (T.BOOL false :: acc) xs
Chris@1706 692 | lexFalse pos acc _ = token_error pos
Chris@1706 693
Chris@1706 694 and lexChar tok pos acc xs =
Chris@1706 695 lex pos (tok :: acc) xs
Chris@1706 696
Chris@1706 697 and lexString pos acc cc =
Chris@1706 698 let datatype escaped = ESCAPED | NORMAL
Chris@1706 699 fun lexString' pos text ESCAPED [] =
Chris@1706 700 error pos "End of input during escape sequence"
Chris@1706 701 | lexString' pos text NORMAL [] =
Chris@1706 702 error pos "End of input during string"
Chris@1706 703 | lexString' pos text ESCAPED (x :: xs) =
Chris@1706 704 let fun esc c = lexString' (pos + 1) (c :: text) NORMAL xs
Chris@1706 705 in case x of
Chris@1706 706 #"\"" => esc x
Chris@1706 707 | #"\\" => esc x
Chris@1706 708 | #"/" => esc x
Chris@1706 709 | #"b" => esc #"\b"
Chris@1706 710 | #"f" => esc #"\f"
Chris@1706 711 | #"n" => esc #"\n"
Chris@1706 712 | #"r" => esc #"\r"
Chris@1706 713 | #"t" => esc #"\t"
Chris@1706 714 | _ => error pos ("Invalid escape \\" ^
Chris@1706 715 Char.toString x)
Chris@1706 716 end
Chris@1706 717 | lexString' pos text NORMAL (#"\\" :: #"u" ::a::b::c::d:: xs) =
Chris@1706 718 if List.all Char.isHexDigit [a,b,c,d]
Chris@1706 719 then case Word.fromString ("0wx" ^ (implode [a,b,c,d])) of
Chris@1706 720 SOME w => (let val utf = rev (bmpToUtf8 w) in
Chris@1706 721 lexString' (pos + 6) (utf @ text)
Chris@1706 722 NORMAL xs
Chris@1706 723 end
Chris@1706 724 handle Fail err => error pos err)
Chris@1706 725 | NONE => error pos "Invalid Unicode BMP escape sequence"
Chris@1706 726 else error pos "Invalid Unicode BMP escape sequence"
Chris@1706 727 | lexString' pos text NORMAL (x :: xs) =
Chris@1706 728 if Char.ord x < 0x20
Chris@1706 729 then error pos "Invalid unescaped control character"
Chris@1706 730 else
Chris@1706 731 case x of
Chris@1706 732 #"\"" => OK (rev text, xs, pos + 1)
Chris@1706 733 | #"\\" => lexString' (pos + 1) text ESCAPED xs
Chris@1706 734 | _ => lexString' (pos + 1) (x :: text) NORMAL xs
Chris@1706 735 in
Chris@1706 736 case lexString' pos [] NORMAL cc of
Chris@1706 737 OK (text, rest, newpos) =>
Chris@1706 738 lex newpos (T.STRING (implode text) :: acc) rest
Chris@1706 739 | ERROR e => ERROR e
Chris@1706 740 end
Chris@1706 741
Chris@1706 742 and lexNumber firstChar pos acc cc =
Chris@1706 743 let val valid = explode ".+-e"
Chris@1706 744 fun lexNumber' pos digits [] = (rev digits, [], pos)
Chris@1706 745 | lexNumber' pos digits (x :: xs) =
Chris@1706 746 if x = #"E" then lexNumber' (pos + 1) (#"e" :: digits) xs
Chris@1706 747 else if Char.isDigit x orelse List.exists (fn c => x = c) valid
Chris@1706 748 then lexNumber' (pos + 1) (x :: digits) xs
Chris@1706 749 else (rev digits, x :: xs, pos)
Chris@1706 750 val (digits, rest, newpos) =
Chris@1706 751 lexNumber' (pos - 1) [] (firstChar :: cc)
Chris@1706 752 in
Chris@1706 753 case digits of
Chris@1706 754 [] => token_error pos
Chris@1706 755 | _ => lex newpos (T.NUMBER digits :: acc) rest
Chris@1706 756 end
Chris@1706 757
Chris@1706 758 and lex pos acc [] = OK (rev acc)
Chris@1706 759 | lex pos acc (x::xs) =
Chris@1706 760 (case x of
Chris@1706 761 #" " => lex
Chris@1706 762 | #"\t" => lex
Chris@1706 763 | #"\n" => lex
Chris@1706 764 | #"\r" => lex
Chris@1706 765 | #"{" => lexChar T.CURLY_L
Chris@1706 766 | #"}" => lexChar T.CURLY_R
Chris@1706 767 | #"[" => lexChar T.SQUARE_L
Chris@1706 768 | #"]" => lexChar T.SQUARE_R
Chris@1706 769 | #":" => lexChar T.COLON
Chris@1706 770 | #"," => lexChar T.COMMA
Chris@1706 771 | #"\"" => lexString
Chris@1706 772 | #"t" => lexTrue
Chris@1706 773 | #"f" => lexFalse
Chris@1706 774 | #"n" => lexNull
Chris@1706 775 | x => lexNumber x) (pos + 1) acc xs
Chris@1706 776
Chris@1706 777 fun show [] = "end of input"
Chris@1706 778 | show (tok :: _) = T.toString tok
Chris@1706 779
Chris@1706 780 fun parseNumber digits =
Chris@1706 781 (* Note lexNumber already case-insensitised the E for us *)
Chris@1706 782 let open Char
Chris@1706 783
Chris@1706 784 fun okExpDigits [] = false
Chris@1706 785 | okExpDigits (c :: []) = isDigit c
Chris@1706 786 | okExpDigits (c :: cs) = isDigit c andalso okExpDigits cs
Chris@1706 787
Chris@1706 788 fun okExponent [] = false
Chris@1706 789 | okExponent (#"+" :: cs) = okExpDigits cs
Chris@1706 790 | okExponent (#"-" :: cs) = okExpDigits cs
Chris@1706 791 | okExponent cc = okExpDigits cc
Chris@1706 792
Chris@1706 793 fun okFracTrailing [] = true
Chris@1706 794 | okFracTrailing (c :: cs) =
Chris@1706 795 (isDigit c andalso okFracTrailing cs) orelse
Chris@1706 796 (c = #"e" andalso okExponent cs)
Chris@1706 797
Chris@1706 798 fun okFraction [] = false
Chris@1706 799 | okFraction (c :: cs) =
Chris@1706 800 isDigit c andalso okFracTrailing cs
Chris@1706 801
Chris@1706 802 fun okPosTrailing [] = true
Chris@1706 803 | okPosTrailing (#"." :: cs) = okFraction cs
Chris@1706 804 | okPosTrailing (#"e" :: cs) = okExponent cs
Chris@1706 805 | okPosTrailing (c :: cs) =
Chris@1706 806 isDigit c andalso okPosTrailing cs
Chris@1706 807
Chris@1706 808 fun okPositive [] = false
Chris@1706 809 | okPositive (#"0" :: []) = true
Chris@1706 810 | okPositive (#"0" :: #"." :: cs) = okFraction cs
Chris@1706 811 | okPositive (#"0" :: #"e" :: cs) = okExponent cs
Chris@1706 812 | okPositive (#"0" :: cs) = false
Chris@1706 813 | okPositive (c :: cs) = isDigit c andalso okPosTrailing cs
Chris@1706 814
Chris@1706 815 fun okNumber (#"-" :: cs) = okPositive cs
Chris@1706 816 | okNumber cc = okPositive cc
Chris@1706 817 in
Chris@1706 818 if okNumber digits
Chris@1706 819 then case Real.fromString (implode digits) of
Chris@1706 820 NONE => ERROR "Number out of range"
Chris@1706 821 | SOME r => OK r
Chris@1706 822 else ERROR ("Invalid number \"" ^ (implode digits) ^ "\"")
Chris@1706 823 end
Chris@1706 824
Chris@1706 825 fun parseObject (T.CURLY_R :: xs) = OK (OBJECT [], xs)
Chris@1706 826 | parseObject tokens =
Chris@1706 827 let fun parsePair (T.STRING key :: T.COLON :: xs) =
Chris@1706 828 (case parseTokens xs of
Chris@1706 829 ERROR e => ERROR e
Chris@1706 830 | OK (j, xs) => OK ((key, j), xs))
Chris@1706 831 | parsePair other =
Chris@1706 832 ERROR ("Object key/value pair expected around \"" ^
Chris@1706 833 show other ^ "\"")
Chris@1706 834 fun parseObject' acc [] = ERROR "End of input during object"
Chris@1706 835 | parseObject' acc tokens =
Chris@1706 836 case parsePair tokens of
Chris@1706 837 ERROR e => ERROR e
Chris@1706 838 | OK (pair, T.COMMA :: xs) =>
Chris@1706 839 parseObject' (pair :: acc) xs
Chris@1706 840 | OK (pair, T.CURLY_R :: xs) =>
Chris@1706 841 OK (OBJECT (rev (pair :: acc)), xs)
Chris@1706 842 | OK (_, _) => ERROR "Expected , or } after object element"
Chris@1706 843 in
Chris@1706 844 parseObject' [] tokens
Chris@1706 845 end
Chris@1706 846
Chris@1706 847 and parseArray (T.SQUARE_R :: xs) = OK (ARRAY [], xs)
Chris@1706 848 | parseArray tokens =
Chris@1706 849 let fun parseArray' acc [] = ERROR "End of input during array"
Chris@1706 850 | parseArray' acc tokens =
Chris@1706 851 case parseTokens tokens of
Chris@1706 852 ERROR e => ERROR e
Chris@1706 853 | OK (j, T.COMMA :: xs) => parseArray' (j :: acc) xs
Chris@1706 854 | OK (j, T.SQUARE_R :: xs) => OK (ARRAY (rev (j :: acc)), xs)
Chris@1706 855 | OK (_, _) => ERROR "Expected , or ] after array element"
Chris@1706 856 in
Chris@1706 857 parseArray' [] tokens
Chris@1706 858 end
Chris@1706 859
Chris@1706 860 and parseTokens [] = ERROR "Value expected"
Chris@1706 861 | parseTokens (tok :: xs) =
Chris@1706 862 (case tok of
Chris@1706 863 T.NUMBER d => (case parseNumber d of
Chris@1706 864 OK r => OK (NUMBER r, xs)
Chris@1706 865 | ERROR e => ERROR e)
Chris@1706 866 | T.STRING s => OK (STRING s, xs)
Chris@1706 867 | T.BOOL b => OK (BOOL b, xs)
Chris@1706 868 | T.NULL => OK (NULL, xs)
Chris@1706 869 | T.CURLY_L => parseObject xs
Chris@1706 870 | T.SQUARE_L => parseArray xs
Chris@1706 871 | _ => ERROR ("Unexpected token " ^ T.toString tok ^
Chris@1706 872 " before " ^ show xs))
Chris@1706 873
Chris@1706 874 fun parse str =
Chris@1706 875 case lex 1 [] (explode str) of
Chris@1706 876 ERROR e => ERROR e
Chris@1706 877 | OK tokens => case parseTokens tokens of
Chris@1706 878 OK (value, []) => OK value
Chris@1706 879 | OK (_, _) => ERROR "Extra data after input"
Chris@1706 880 | ERROR e => ERROR e
Chris@1706 881
Chris@1706 882 fun stringEscape s =
Chris@1706 883 let fun esc x = [x, #"\\"]
Chris@1706 884 fun escape' acc [] = rev acc
Chris@1706 885 | escape' acc (x :: xs) =
Chris@1706 886 escape' (case x of
Chris@1706 887 #"\"" => esc x @ acc
Chris@1706 888 | #"\\" => esc x @ acc
Chris@1706 889 | #"\b" => esc #"b" @ acc
Chris@1706 890 | #"\f" => esc #"f" @ acc
Chris@1706 891 | #"\n" => esc #"n" @ acc
Chris@1706 892 | #"\r" => esc #"r" @ acc
Chris@1706 893 | #"\t" => esc #"t" @ acc
Chris@1706 894 | _ =>
Chris@1706 895 let val c = Char.ord x
Chris@1706 896 in
Chris@1706 897 if c < 0x20
Chris@1706 898 then let val hex = Word.toString (Word.fromInt c)
Chris@1706 899 in (rev o explode) (if c < 0x10
Chris@1706 900 then ("\\u000" ^ hex)
Chris@1706 901 else ("\\u00" ^ hex))
Chris@1706 902 end @ acc
Chris@1706 903 else
Chris@1706 904 x :: acc
Chris@1706 905 end)
Chris@1706 906 xs
Chris@1706 907 in
Chris@1706 908 implode (escape' [] (explode s))
Chris@1706 909 end
Chris@1706 910
Chris@1706 911 fun serialise json =
Chris@1706 912 case json of
Chris@1706 913 OBJECT pp => "{" ^ String.concatWith
Chris@1706 914 "," (map (fn (key, value) =>
Chris@1706 915 serialise (STRING key) ^ ":" ^
Chris@1706 916 serialise value) pp) ^
Chris@1706 917 "}"
Chris@1706 918 | ARRAY arr => "[" ^ String.concatWith "," (map serialise arr) ^ "]"
Chris@1706 919 | NUMBER n => implode (map (fn #"~" => #"-" | c => c)
Chris@1706 920 (explode (Real.toString n)))
Chris@1706 921 | STRING s => "\"" ^ stringEscape s ^ "\""
Chris@1706 922 | BOOL b => Bool.toString b
Chris@1706 923 | NULL => "null"
Chris@1706 924
Chris@1706 925 fun serialiseIndented json =
Chris@1706 926 let fun indent 0 = ""
Chris@1706 927 | indent i = " " ^ indent (i - 1)
Chris@1706 928 fun serialiseIndented' i json =
Chris@1706 929 let val ser = serialiseIndented' (i + 1)
Chris@1706 930 in
Chris@1706 931 case json of
Chris@1706 932 OBJECT [] => "{}"
Chris@1706 933 | ARRAY [] => "[]"
Chris@1706 934 | OBJECT pp => "{\n" ^ indent (i + 1) ^
Chris@1706 935 String.concatWith
Chris@1706 936 (",\n" ^ indent (i + 1))
Chris@1706 937 (map (fn (key, value) =>
Chris@1706 938 ser (STRING key) ^ ": " ^
Chris@1706 939 ser value) pp) ^
Chris@1706 940 "\n" ^ indent i ^ "}"
Chris@1706 941 | ARRAY arr => "[\n" ^ indent (i + 1) ^
Chris@1706 942 String.concatWith
Chris@1706 943 (",\n" ^ indent (i + 1))
Chris@1706 944 (map ser arr) ^
Chris@1706 945 "\n" ^ indent i ^ "]"
Chris@1706 946 | other => serialise other
Chris@1706 947 end
Chris@1706 948 in
Chris@1706 949 serialiseIndented' 0 json ^ "\n"
Chris@1706 950 end
Chris@1706 951
Chris@1706 952 end
Chris@1706 953
Chris@1706 954
Chris@1706 955 structure JsonBits :> sig
Chris@1706 956 val load_json_from : string -> Json.json (* filename -> json *)
Chris@1706 957 val save_json_to : string -> Json.json -> unit
Chris@1706 958 val lookup_optional : Json.json -> string list -> Json.json option
Chris@1706 959 val lookup_optional_string : Json.json -> string list -> string option
Chris@1706 960 val lookup_mandatory : Json.json -> string list -> Json.json
Chris@1706 961 val lookup_mandatory_string : Json.json -> string list -> string
Chris@1706 962 end = struct
Chris@1706 963
Chris@1706 964 fun load_json_from filename =
Chris@1706 965 case Json.parse (FileBits.file_contents filename) of
Chris@1706 966 Json.OK json => json
Chris@1706 967 | Json.ERROR e => raise Fail ("Failed to parse file: " ^ e)
Chris@1706 968
Chris@1706 969 fun save_json_to filename json =
Chris@1732 970 (* using binary I/O to avoid ever writing CR/LF line endings *)
Chris@1706 971 let val jstr = Json.serialiseIndented json
Chris@1732 972 val stream = BinIO.openOut filename
Chris@1706 973 in
Chris@1732 974 BinIO.output (stream, Byte.stringToBytes jstr);
Chris@1732 975 BinIO.closeOut stream
Chris@1706 976 end
Chris@1706 977
Chris@1706 978 fun lookup_optional json kk =
Chris@1706 979 let fun lookup key =
Chris@1706 980 case json of
Chris@1706 981 Json.OBJECT kvs =>
Chris@1706 982 (case List.find (fn (k, v) => k = key) kvs of
Chris@1706 983 SOME (k, v) => SOME v
Chris@1706 984 | NONE => NONE)
Chris@1706 985 | _ => raise Fail "Object expected"
Chris@1706 986 in
Chris@1706 987 case kk of
Chris@1706 988 [] => NONE
Chris@1706 989 | key::[] => lookup key
Chris@1706 990 | key::kk => case lookup key of
Chris@1706 991 NONE => NONE
Chris@1706 992 | SOME j => lookup_optional j kk
Chris@1706 993 end
Chris@1706 994
Chris@1706 995 fun lookup_optional_string json kk =
Chris@1706 996 case lookup_optional json kk of
Chris@1706 997 SOME (Json.STRING s) => SOME s
Chris@1706 998 | SOME _ => raise Fail ("Value (if present) must be string: " ^
Chris@1706 999 (String.concatWith " -> " kk))
Chris@1706 1000 | NONE => NONE
Chris@1706 1001
Chris@1706 1002 fun lookup_mandatory json kk =
Chris@1706 1003 case lookup_optional json kk of
Chris@1706 1004 SOME v => v
Chris@1706 1005 | NONE => raise Fail ("Value is mandatory: " ^
Chris@1706 1006 (String.concatWith " -> " kk) ^ " in json: " ^
Chris@1706 1007 (Json.serialise json))
Chris@1706 1008
Chris@1706 1009 fun lookup_mandatory_string json kk =
Chris@1706 1010 case lookup_optional json kk of
Chris@1706 1011 SOME (Json.STRING s) => s
Chris@1706 1012 | _ => raise Fail ("Value must be string: " ^
Chris@1706 1013 (String.concatWith " -> " kk))
Chris@1706 1014 end
Chris@1706 1015
Chris@1706 1016 structure Provider :> sig
Chris@1706 1017 val load_providers : Json.json -> provider list
Chris@1706 1018 val load_more_providers : provider list -> Json.json -> provider list
Chris@1706 1019 val remote_url : context -> vcs -> source -> libname -> string
Chris@1706 1020 end = struct
Chris@1706 1021
Chris@1706 1022 val known_providers : provider list =
Chris@1706 1023 [ {
Chris@1706 1024 service = "bitbucket",
Chris@1706 1025 supports = [HG, GIT],
Chris@1706 1026 remote_spec = {
Chris@1724 1027 anon = SOME "https://bitbucket.org/{owner}/{repository}",
Chris@1724 1028 auth = SOME "ssh://{vcs}@bitbucket.org/{owner}/{repository}"
Chris@1706 1029 }
Chris@1706 1030 },
Chris@1706 1031 {
Chris@1706 1032 service = "github",
Chris@1706 1033 supports = [GIT],
Chris@1706 1034 remote_spec = {
Chris@1724 1035 anon = SOME "https://github.com/{owner}/{repository}",
Chris@1724 1036 auth = SOME "ssh://{vcs}@github.com/{owner}/{repository}"
Chris@1706 1037 }
Chris@1706 1038 }
Chris@1706 1039 ]
Chris@1706 1040
Chris@1706 1041 fun vcs_name vcs =
Chris@1706 1042 case vcs of GIT => "git" |
Chris@1706 1043 HG => "hg"
Chris@1706 1044
Chris@1706 1045 fun vcs_from_name name =
Chris@1706 1046 case name of "git" => GIT
Chris@1706 1047 | "hg" => HG
Chris@1706 1048 | other => raise Fail ("Unknown vcs name \"" ^ name ^ "\"")
Chris@1706 1049
Chris@1706 1050 fun load_more_providers previously_loaded json =
Chris@1706 1051 let open JsonBits
Chris@1706 1052 fun load pjson pname : provider =
Chris@1706 1053 {
Chris@1706 1054 service = pname,
Chris@1706 1055 supports =
Chris@1706 1056 case lookup_mandatory pjson ["vcs"] of
Chris@1706 1057 Json.ARRAY vv =>
Chris@1706 1058 map (fn (Json.STRING v) => vcs_from_name v
Chris@1706 1059 | _ => raise Fail "Strings expected in vcs array")
Chris@1706 1060 vv
Chris@1706 1061 | _ => raise Fail "Array expected for vcs",
Chris@1706 1062 remote_spec = {
Chris@1724 1063 anon = lookup_optional_string pjson ["anonymous"],
Chris@1724 1064 auth = lookup_optional_string pjson ["authenticated"]
Chris@1706 1065 }
Chris@1706 1066 }
Chris@1706 1067 val loaded =
Chris@1721 1068 case lookup_optional json ["services"] of
Chris@1706 1069 NONE => []
Chris@1706 1070 | SOME (Json.OBJECT pl) => map (fn (k, v) => load v k) pl
Chris@1721 1071 | _ => raise Fail "Object expected for services in config"
Chris@1706 1072 val newly_loaded =
Chris@1706 1073 List.filter (fn p => not (List.exists (fn pp => #service p =
Chris@1706 1074 #service pp)
Chris@1706 1075 previously_loaded))
Chris@1706 1076 loaded
Chris@1706 1077 in
Chris@1706 1078 previously_loaded @ newly_loaded
Chris@1706 1079 end
Chris@1706 1080
Chris@1706 1081 fun load_providers json =
Chris@1706 1082 load_more_providers known_providers json
Chris@1706 1083
Chris@1706 1084 fun expand_spec spec { vcs, service, owner, repo } login =
Chris@1706 1085 (* ugly *)
Chris@1706 1086 let fun replace str =
Chris@1706 1087 case str of
Chris@1706 1088 "vcs" => vcs_name vcs
Chris@1706 1089 | "service" => service
Chris@1706 1090 | "owner" =>
Chris@1706 1091 (case owner of
Chris@1706 1092 SOME ostr => ostr
Chris@1706 1093 | NONE => raise Fail ("Owner not specified for service " ^
Chris@1706 1094 service))
Chris@1724 1095 | "repository" => repo
Chris@1706 1096 | "account" =>
Chris@1706 1097 (case login of
Chris@1706 1098 SOME acc => acc
Chris@1706 1099 | NONE => raise Fail ("Account not given for service " ^
Chris@1706 1100 service))
Chris@1706 1101 | other => raise Fail ("Unknown variable \"" ^ other ^
Chris@1706 1102 "\" in spec for service " ^ service)
Chris@1706 1103 fun expand' acc sstr =
Chris@1706 1104 case Substring.splitl (fn c => c <> #"{") sstr of
Chris@1706 1105 (pfx, sfx) =>
Chris@1706 1106 if Substring.isEmpty sfx
Chris@1706 1107 then rev (pfx :: acc)
Chris@1706 1108 else
Chris@1706 1109 case Substring.splitl (fn c => c <> #"}") sfx of
Chris@1706 1110 (tok, remainder) =>
Chris@1706 1111 if Substring.isEmpty remainder
Chris@1706 1112 then rev (tok :: pfx :: acc)
Chris@1706 1113 else let val replacement =
Chris@1706 1114 replace
Chris@1706 1115 (* tok begins with "{": *)
Chris@1706 1116 (Substring.string
Chris@1706 1117 (Substring.triml 1 tok))
Chris@1706 1118 in
Chris@1706 1119 expand' (Substring.full replacement ::
Chris@1706 1120 pfx :: acc)
Chris@1706 1121 (* remainder begins with "}": *)
Chris@1706 1122 (Substring.triml 1 remainder)
Chris@1706 1123 end
Chris@1706 1124 in
Chris@1706 1125 Substring.concat (expand' [] (Substring.full spec))
Chris@1706 1126 end
Chris@1706 1127
Chris@1706 1128 fun provider_url req login providers =
Chris@1706 1129 case providers of
Chris@1706 1130 [] => raise Fail ("Unknown service \"" ^ (#service req) ^
Chris@1706 1131 "\" for vcs \"" ^ (vcs_name (#vcs req)) ^ "\"")
Chris@1706 1132 | ({ service, supports, remote_spec : remote_spec } :: rest) =>
Chris@1706 1133 if service <> (#service req) orelse
Chris@1706 1134 not (List.exists (fn v => v = (#vcs req)) supports)
Chris@1706 1135 then provider_url req login rest
Chris@1706 1136 else
Chris@1706 1137 case (login, #auth remote_spec, #anon remote_spec) of
Chris@1706 1138 (SOME _, SOME auth, _) => expand_spec auth req login
Chris@1706 1139 | (SOME _, _, SOME anon) => expand_spec anon req NONE
Chris@1706 1140 | (NONE, _, SOME anon) => expand_spec anon req NONE
Chris@1724 1141 | _ => raise Fail ("No suitable anonymous or authenticated " ^
Chris@1724 1142 "URL spec provided for service \"" ^
Chris@1724 1143 service ^ "\"")
Chris@1706 1144
Chris@1706 1145 fun login_for ({ accounts, ... } : context) service =
Chris@1706 1146 case List.find (fn a => service = #service a) accounts of
Chris@1706 1147 SOME { login, ... } => SOME login
Chris@1706 1148 | NONE => NONE
Chris@1747 1149
Chris@1747 1150 fun reponame_for path =
Chris@1747 1151 case String.tokens (fn c => c = #"/") path of
Chris@1747 1152 [] => raise Fail "Non-empty library path required"
Chris@1747 1153 | toks => hd (rev toks)
Chris@1747 1154
Chris@1706 1155 fun remote_url (context : context) vcs source libname =
Chris@1706 1156 case source of
Chris@1721 1157 URL_SOURCE u => u
Chris@1721 1158 | SERVICE_SOURCE { service, owner, repo } =>
Chris@1706 1159 provider_url { vcs = vcs,
Chris@1706 1160 service = service,
Chris@1706 1161 owner = owner,
Chris@1706 1162 repo = case repo of
Chris@1706 1163 SOME r => r
Chris@1747 1164 | NONE => reponame_for libname }
Chris@1706 1165 (login_for context service)
Chris@1706 1166 (#providers context)
Chris@1706 1167 end
Chris@1706 1168
Chris@1706 1169 structure HgControl :> VCS_CONTROL = struct
Chris@1752 1170
Chris@1752 1171 (* Pulls always use an explicit URL, never just the default
Chris@1752 1172 remote, in order to ensure we update properly if the location
Chris@1752 1173 given in the project file changes. *)
Chris@1752 1174
Chris@1706 1175 type vcsstate = { id: string, modified: bool,
Chris@1706 1176 branch: string, tags: string list }
Chris@1706 1177
Chris@1706 1178 val hg_args = [ "--config", "ui.interactive=true" ]
Chris@1706 1179
Chris@1706 1180 fun hg_command context libname args =
Chris@1706 1181 FileBits.command context libname ("hg" :: hg_args @ args)
Chris@1706 1182
Chris@1706 1183 fun hg_command_output context libname args =
Chris@1706 1184 FileBits.command_output context libname ("hg" :: hg_args @ args)
Chris@1706 1185
Chris@1706 1186 fun exists context libname =
Chris@1706 1187 OK (OS.FileSys.isDir (FileBits.subpath context libname ".hg"))
Chris@1706 1188 handle _ => OK false
Chris@1706 1189
Chris@1706 1190 fun remote_for context (libname, source) =
Chris@1706 1191 Provider.remote_url context HG source libname
Chris@1706 1192
Chris@1706 1193 fun current_state context libname : vcsstate result =
Chris@1706 1194 let fun is_branch text = text <> "" andalso #"(" = hd (explode text)
Chris@1706 1195 and extract_branch b =
Chris@1706 1196 if is_branch b (* need to remove enclosing parens *)
Chris@1706 1197 then (implode o rev o tl o rev o tl o explode) b
Chris@1706 1198 else "default"
Chris@1706 1199 and is_modified id = id <> "" andalso #"+" = hd (rev (explode id))
Chris@1706 1200 and extract_id id =
Chris@1706 1201 if is_modified id (* need to remove trailing "+" *)
Chris@1706 1202 then (implode o rev o tl o rev o explode) id
Chris@1706 1203 else id
Chris@1706 1204 and split_tags tags = String.tokens (fn c => c = #"/") tags
Chris@1706 1205 and state_for (id, branch, tags) =
Chris@1706 1206 OK { id = extract_id id,
Chris@1706 1207 modified = is_modified id,
Chris@1706 1208 branch = extract_branch branch,
Chris@1706 1209 tags = split_tags tags }
Chris@1706 1210 in
Chris@1706 1211 case hg_command_output context libname ["id"] of
Chris@1706 1212 ERROR e => ERROR e
Chris@1706 1213 | OK out =>
Chris@1706 1214 case String.tokens (fn x => x = #" ") out of
Chris@1706 1215 [id, branch, tags] => state_for (id, branch, tags)
Chris@1706 1216 | [id, other] => if is_branch other
Chris@1706 1217 then state_for (id, other, "")
Chris@1706 1218 else state_for (id, "", other)
Chris@1706 1219 | [id] => state_for (id, "", "")
Chris@1706 1220 | _ => ERROR ("Unexpected output from hg id: " ^ out)
Chris@1706 1221 end
Chris@1706 1222
Chris@1706 1223 fun branch_name branch = case branch of
Chris@1706 1224 DEFAULT_BRANCH => "default"
Chris@1706 1225 | BRANCH "" => "default"
Chris@1706 1226 | BRANCH b => b
Chris@1706 1227
Chris@1706 1228 fun id_of context libname =
Chris@1706 1229 case current_state context libname of
Chris@1706 1230 ERROR e => ERROR e
Chris@1706 1231 | OK { id, ... } => OK id
Chris@1706 1232
Chris@1706 1233 fun is_at context (libname, id_or_tag) =
Chris@1706 1234 case current_state context libname of
Chris@1706 1235 ERROR e => ERROR e
Chris@1706 1236 | OK { id, tags, ... } =>
Chris@1706 1237 OK (String.isPrefix id_or_tag id orelse
Chris@1706 1238 String.isPrefix id id_or_tag orelse
Chris@1706 1239 List.exists (fn t => t = id_or_tag) tags)
Chris@1706 1240
Chris@1706 1241 fun is_on_branch context (libname, b) =
Chris@1706 1242 case current_state context libname of
Chris@1706 1243 ERROR e => ERROR e
Chris@1706 1244 | OK { branch, ... } => OK (branch = branch_name b)
Chris@1706 1245
Chris@1706 1246 fun is_newest_locally context (libname, branch) =
Chris@1706 1247 case hg_command_output context libname
Chris@1706 1248 ["log", "-l1",
Chris@1706 1249 "-b", branch_name branch,
Chris@1706 1250 "--template", "{node}"] of
Chris@1752 1251 ERROR e => OK false (* desired branch does not exist *)
Chris@1706 1252 | OK newest_in_repo => is_at context (libname, newest_in_repo)
Chris@1706 1253
Chris@1752 1254 fun pull context (libname, source) =
Chris@1752 1255 let val url = remote_for context (libname, source)
Chris@1752 1256 in
Chris@1752 1257 hg_command context libname
Chris@1752 1258 (if FileBits.verbose ()
Chris@1752 1259 then ["pull", url]
Chris@1752 1260 else ["pull", "-q", url])
Chris@1752 1261 end
Chris@1706 1262
Chris@1752 1263 fun is_newest context (libname, source, branch) =
Chris@1706 1264 case is_newest_locally context (libname, branch) of
Chris@1706 1265 ERROR e => ERROR e
Chris@1706 1266 | OK false => OK false
Chris@1706 1267 | OK true =>
Chris@1752 1268 case pull context (libname, source) of
Chris@1706 1269 ERROR e => ERROR e
Chris@1706 1270 | _ => is_newest_locally context (libname, branch)
Chris@1706 1271
Chris@1706 1272 fun is_modified_locally context libname =
Chris@1706 1273 case current_state context libname of
Chris@1706 1274 ERROR e => ERROR e
Chris@1706 1275 | OK { modified, ... } => OK modified
Chris@1706 1276
Chris@1706 1277 fun checkout context (libname, source, branch) =
Chris@1706 1278 let val url = remote_for context (libname, source)
Chris@1706 1279 in
Chris@1747 1280 (* make the lib dir rather than just the ext dir, since
Chris@1747 1281 the lib dir might be nested and hg will happily check
Chris@1747 1282 out into an existing empty dir anyway *)
Chris@1747 1283 case FileBits.mkpath (FileBits.libpath context libname) of
Chris@1706 1284 ERROR e => ERROR e
Chris@1706 1285 | _ => hg_command context ""
Chris@1706 1286 ["clone", "-u", branch_name branch,
Chris@1706 1287 url, libname]
Chris@1706 1288 end
Chris@1706 1289
Chris@1752 1290 fun update context (libname, source, branch) =
Chris@1752 1291 let val pull_result = pull context (libname, source)
Chris@1706 1292 in
Chris@1706 1293 case hg_command context libname ["update", branch_name branch] of
Chris@1706 1294 ERROR e => ERROR e
Chris@1706 1295 | _ =>
Chris@1706 1296 case pull_result of
Chris@1706 1297 ERROR e => ERROR e
Chris@1706 1298 | _ => id_of context libname
Chris@1706 1299 end
Chris@1706 1300
Chris@1752 1301 fun update_to context (libname, _, "") =
Chris@1706 1302 ERROR "Non-empty id (tag or revision id) required for update_to"
Chris@1752 1303 | update_to context (libname, source, id) =
Chris@1752 1304 let val pull_result = pull context (libname, source)
Chris@1723 1305 in
Chris@1723 1306 case hg_command context libname ["update", "-r", id] of
Chris@1723 1307 OK _ => id_of context libname
Chris@1723 1308 | ERROR e =>
Chris@1723 1309 case pull_result of
Chris@1723 1310 ERROR e' => ERROR e' (* this was the ur-error *)
Chris@1723 1311 | _ => ERROR e
Chris@1723 1312 end
Chris@1706 1313
Chris@1706 1314 end
Chris@1706 1315
Chris@1706 1316 structure GitControl :> VCS_CONTROL = struct
Chris@1706 1317
Chris@1706 1318 (* With Git repos we always operate in detached HEAD state. Even
Chris@1752 1319 the master branch is checked out using a remote reference
Chris@1752 1320 (vext/master). The remote we use is always named vext, and we
Chris@1752 1321 update it to the expected URL each time we fetch, in order to
Chris@1752 1322 ensure we update properly if the location given in the project
Chris@1752 1323 file changes. The origin remote is unused. *)
Chris@1706 1324
Chris@1706 1325 fun git_command context libname args =
Chris@1706 1326 FileBits.command context libname ("git" :: args)
Chris@1706 1327
Chris@1706 1328 fun git_command_output context libname args =
Chris@1706 1329 FileBits.command_output context libname ("git" :: args)
Chris@1706 1330
Chris@1706 1331 fun exists context libname =
Chris@1706 1332 OK (OS.FileSys.isDir (FileBits.subpath context libname ".git"))
Chris@1706 1333 handle _ => OK false
Chris@1706 1334
Chris@1706 1335 fun remote_for context (libname, source) =
Chris@1706 1336 Provider.remote_url context GIT source libname
Chris@1706 1337
Chris@1706 1338 fun branch_name branch = case branch of
Chris@1706 1339 DEFAULT_BRANCH => "master"
Chris@1706 1340 | BRANCH "" => "master"
Chris@1706 1341 | BRANCH b => b
Chris@1706 1342
Chris@1752 1343 val our_remote = "vext"
Chris@1752 1344
Chris@1752 1345 fun remote_branch_name branch = our_remote ^ "/" ^ branch_name branch
Chris@1706 1346
Chris@1706 1347 fun checkout context (libname, source, branch) =
Chris@1706 1348 let val url = remote_for context (libname, source)
Chris@1706 1349 in
Chris@1747 1350 (* make the lib dir rather than just the ext dir, since
Chris@1747 1351 the lib dir might be nested and git will happily check
Chris@1747 1352 out into an existing empty dir anyway *)
Chris@1747 1353 case FileBits.mkpath (FileBits.libpath context libname) of
Chris@1706 1354 OK () => git_command context ""
Chris@1752 1355 ["clone", "--origin", our_remote,
Chris@1752 1356 "--branch", branch_name branch,
Chris@1706 1357 url, libname]
Chris@1706 1358 | ERROR e => ERROR e
Chris@1706 1359 end
Chris@1706 1360
Chris@1752 1361 fun add_our_remote context (libname, source) =
Chris@1752 1362 (* When we do the checkout ourselves (above), we add the
Chris@1752 1363 remote at the same time. But if the repo was cloned by
Chris@1752 1364 someone else, we'll need to do it after the fact. Git
Chris@1752 1365 doesn't seem to have a means to add a remote or change its
Chris@1752 1366 url if it already exists; seems we have to do this: *)
Chris@1752 1367 let val url = remote_for context (libname, source)
Chris@1752 1368 in
Chris@1752 1369 case git_command context libname
Chris@1752 1370 ["remote", "set-url", our_remote, url] of
Chris@1752 1371 OK () => OK ()
Chris@1752 1372 | ERROR e => git_command context libname
Chris@1752 1373 ["remote", "add", "-f", our_remote, url]
Chris@1752 1374 end
Chris@1752 1375
Chris@1706 1376 (* NB git rev-parse HEAD shows revision id of current checkout;
Chris@1706 1377 git rev-list -1 <tag> shows revision id of revision with that tag *)
Chris@1706 1378
Chris@1706 1379 fun id_of context libname =
Chris@1706 1380 git_command_output context libname ["rev-parse", "HEAD"]
Chris@1706 1381
Chris@1706 1382 fun is_at context (libname, id_or_tag) =
Chris@1706 1383 case id_of context libname of
Chris@1752 1384 ERROR e => OK false (* HEAD nonexistent, expected in empty repo *)
Chris@1706 1385 | OK id =>
Chris@1706 1386 if String.isPrefix id_or_tag id orelse
Chris@1706 1387 String.isPrefix id id_or_tag
Chris@1706 1388 then OK true
Chris@1706 1389 else
Chris@1706 1390 case git_command_output context libname
Chris@1723 1391 ["show-ref",
Chris@1752 1392 "refs/tags/" ^ id_or_tag,
Chris@1752 1393 "--"] of
Chris@1723 1394 OK "" => OK false
Chris@1723 1395 | ERROR _ => OK false
Chris@1723 1396 | OK s => OK (id = hd (String.tokens (fn c => c = #" ") s))
Chris@1706 1397
Chris@1706 1398 fun branch_tip context (libname, branch) =
Chris@1752 1399 (* We don't have access to the source info or the network
Chris@1752 1400 here, as this is used by status (e.g. via is_on_branch) as
Chris@1752 1401 well as review. It's possible the remote branch won't exist,
Chris@1752 1402 e.g. if the repo was checked out by something other than
Chris@1752 1403 Vext, and if that's the case, we can't add it here; we'll
Chris@1752 1404 just have to fail, since checking against local branches
Chris@1752 1405 instead could produce the wrong result. *)
Chris@1706 1406 git_command_output context libname
Chris@1706 1407 ["rev-list", "-1",
Chris@1752 1408 remote_branch_name branch, "--"]
Chris@1706 1409
Chris@1706 1410 fun is_newest_locally context (libname, branch) =
Chris@1706 1411 case branch_tip context (libname, branch) of
Chris@1752 1412 ERROR e => OK false
Chris@1706 1413 | OK rev => is_at context (libname, rev)
Chris@1706 1414
Chris@1706 1415 fun is_on_branch context (libname, branch) =
Chris@1706 1416 case branch_tip context (libname, branch) of
Chris@1752 1417 ERROR e => OK false
Chris@1706 1418 | OK rev =>
Chris@1706 1419 case is_at context (libname, rev) of
Chris@1706 1420 ERROR e => ERROR e
Chris@1706 1421 | OK true => OK true
Chris@1706 1422 | OK false =>
Chris@1706 1423 case git_command context libname
Chris@1706 1424 ["merge-base", "--is-ancestor",
Chris@1706 1425 "HEAD", remote_branch_name branch] of
Chris@1706 1426 ERROR e => OK false (* cmd returns non-zero for no *)
Chris@1706 1427 | _ => OK true
Chris@1706 1428
Chris@1752 1429 fun fetch context (libname, source) =
Chris@1752 1430 case add_our_remote context (libname, source) of
Chris@1706 1431 ERROR e => ERROR e
Chris@1752 1432 | _ => git_command context libname ["fetch", our_remote]
Chris@1752 1433
Chris@1752 1434 fun is_newest context (libname, source, branch) =
Chris@1752 1435 case add_our_remote context (libname, source) of
Chris@1752 1436 ERROR e => ERROR e
Chris@1752 1437 | OK () =>
Chris@1752 1438 case is_newest_locally context (libname, branch) of
Chris@1706 1439 ERROR e => ERROR e
Chris@1752 1440 | OK false => OK false
Chris@1752 1441 | OK true =>
Chris@1752 1442 case fetch context (libname, source) of
Chris@1752 1443 ERROR e => ERROR e
Chris@1752 1444 | _ => is_newest_locally context (libname, branch)
Chris@1706 1445
Chris@1706 1446 fun is_modified_locally context libname =
Chris@1706 1447 case git_command_output context libname ["status", "--porcelain"] of
Chris@1706 1448 ERROR e => ERROR e
Chris@1706 1449 | OK "" => OK false
Chris@1706 1450 | OK _ => OK true
Chris@1706 1451
Chris@1706 1452 (* This function updates to the latest revision on a branch rather
Chris@1706 1453 than to a specific id or tag. We can't just checkout the given
Chris@1706 1454 branch, as that will succeed even if the branch isn't up to
Chris@1706 1455 date. We could checkout the branch and then fetch and merge,
Chris@1706 1456 but it's perhaps cleaner not to maintain a local branch at all,
Chris@1706 1457 but instead checkout the remote branch as a detached head. *)
Chris@1706 1458
Chris@1752 1459 fun update context (libname, source, branch) =
Chris@1752 1460 case fetch context (libname, source) of
Chris@1706 1461 ERROR e => ERROR e
Chris@1706 1462 | _ =>
Chris@1706 1463 case git_command context libname ["checkout", "--detach",
Chris@1706 1464 remote_branch_name branch] of
Chris@1706 1465 ERROR e => ERROR e
Chris@1706 1466 | _ => id_of context libname
Chris@1706 1467
Chris@1706 1468 (* This function is dealing with a specific id or tag, so if we
Chris@1723 1469 can successfully check it out (detached) then that's all we
Chris@1723 1470 need to do, regardless of whether fetch succeeded or not. We do
Chris@1723 1471 attempt the fetch first, though, purely in order to avoid ugly
Chris@1723 1472 error messages in the common case where we're being asked to
Chris@1723 1473 update to a new pin (from the lock file) that hasn't been
Chris@1723 1474 fetched yet. *)
Chris@1706 1475
Chris@1752 1476 fun update_to context (libname, _, "") =
Chris@1706 1477 ERROR "Non-empty id (tag or revision id) required for update_to"
Chris@1752 1478 | update_to context (libname, source, id) =
Chris@1752 1479 let val fetch_result = fetch context (libname, source)
Chris@1723 1480 in
Chris@1723 1481 case git_command context libname ["checkout", "--detach", id] of
Chris@1723 1482 OK _ => id_of context libname
Chris@1723 1483 | ERROR e =>
Chris@1723 1484 case fetch_result of
Chris@1723 1485 ERROR e' => ERROR e' (* this was the ur-error *)
Chris@1723 1486 | _ => ERROR e
Chris@1723 1487 end
Chris@1723 1488
Chris@1706 1489 end
Chris@1706 1490
Chris@1706 1491 structure AnyLibControl :> LIB_CONTROL = struct
Chris@1706 1492
Chris@1706 1493 structure H = LibControlFn(HgControl)
Chris@1706 1494 structure G = LibControlFn(GitControl)
Chris@1706 1495
Chris@1706 1496 fun review context (spec as { vcs, ... } : libspec) =
Chris@1706 1497 (fn HG => H.review | GIT => G.review) vcs context spec
Chris@1706 1498
Chris@1706 1499 fun status context (spec as { vcs, ... } : libspec) =
Chris@1706 1500 (fn HG => H.status | GIT => G.status) vcs context spec
Chris@1706 1501
Chris@1706 1502 fun update context (spec as { vcs, ... } : libspec) =
Chris@1706 1503 (fn HG => H.update | GIT => G.update) vcs context spec
Chris@1740 1504
Chris@1740 1505 fun id_of context (spec as { vcs, ... } : libspec) =
Chris@1740 1506 (fn HG => H.id_of | GIT => G.id_of) vcs context spec
Chris@1706 1507 end
Chris@1706 1508
Chris@1746 1509
Chris@1746 1510 type exclusions = string list
Chris@1746 1511
Chris@1746 1512 structure Archive :> sig
Chris@1746 1513
Chris@1746 1514 val archive : string * exclusions -> project -> OS.Process.status
Chris@1746 1515
Chris@1746 1516 end = struct
Chris@1746 1517
Chris@1746 1518 (* The idea of "archive" is to replace hg/git archive, which won't
Chris@1746 1519 include files, like the Vext-introduced external libraries,
Chris@1746 1520 that are not under version control with the main repo.
Chris@1746 1521
Chris@1746 1522 The process goes like this:
Chris@1746 1523
Chris@1746 1524 - Make sure we have a target filename from the user, and take
Chris@1746 1525 its basename as our archive directory name
Chris@1746 1526
Chris@1746 1527 - Make an "archive root" subdir of the project repo, named
Chris@1746 1528 typically .vext-archive
Chris@1746 1529
Chris@1746 1530 - Identify the VCS used for the project repo. Note that any
Chris@1746 1531 explicit references to VCS type in this structure are to
Chris@1746 1532 the VCS used for the project (something Vext doesn't
Chris@1746 1533 otherwise care about), not for an individual library
Chris@1746 1534
Chris@1746 1535 - Synthesise a Vext project with the archive root as its
Chris@1746 1536 root path, "." as its extdir, with one library whose
Chris@1746 1537 name is the user-supplied basename and whose explicit
Chris@1746 1538 source URL is the original project root; update that
Chris@1746 1539 project -- thus cloning the original project to a subdir
Chris@1746 1540 of the archive root
Chris@1746 1541
Chris@1746 1542 - Synthesise a Vext project identical to the original one for
Chris@1746 1543 this project, but with the newly-cloned copy as its root
Chris@1746 1544 path; update that project -- thus checking out clean copies
Chris@1746 1545 of the external library dirs
Chris@1746 1546
Chris@1746 1547 - Call out to an archive program to archive up the new copy,
Chris@1746 1548 running e.g.
Chris@1746 1549 tar cvzf project-release.tar.gz \
Chris@1746 1550 --exclude=.hg --exclude=.git project-release
Chris@1746 1551 in the archive root dir
Chris@1746 1552
Chris@1746 1553 - (We also omit the vext-project.json file and any trace of
Chris@1746 1554 Vext. It can't properly be run in a directory where the
Chris@1746 1555 external project folders already exist but their repo history
Chris@1746 1556 does not. End users shouldn't get to see Vext)
Chris@1746 1557
Chris@1746 1558 - Clean up by deleting the new copy
Chris@1746 1559 *)
Chris@1746 1560
Chris@1746 1561 fun project_vcs_and_id dir =
Chris@1746 1562 let val context = {
Chris@1746 1563 rootpath = dir,
Chris@1746 1564 extdir = ".",
Chris@1746 1565 providers = [],
Chris@1746 1566 accounts = []
Chris@1746 1567 }
Chris@1746 1568 val vcs_maybe =
Chris@1746 1569 case [HgControl.exists context ".",
Chris@1746 1570 GitControl.exists context "."] of
Chris@1746 1571 [OK true, OK false] => OK HG
Chris@1746 1572 | [OK false, OK true] => OK GIT
Chris@1746 1573 | _ => ERROR ("Unable to identify VCS for directory " ^ dir)
Chris@1746 1574 in
Chris@1746 1575 case vcs_maybe of
Chris@1746 1576 ERROR e => ERROR e
Chris@1746 1577 | OK vcs =>
Chris@1746 1578 case (fn HG => HgControl.id_of | GIT => GitControl.id_of)
Chris@1746 1579 vcs context "." of
Chris@1746 1580 ERROR e => ERROR ("Unable to obtain id of project repo: "
Chris@1746 1581 ^ e)
Chris@1746 1582 | OK id => OK (vcs, id)
Chris@1746 1583 end
Chris@1746 1584
Chris@1746 1585 fun make_archive_root (context : context) =
Chris@1746 1586 let val path = OS.Path.joinDirFile {
Chris@1746 1587 dir = #rootpath context,
Chris@1746 1588 file = VextFilenames.archive_dir
Chris@1746 1589 }
Chris@1746 1590 in
Chris@1746 1591 case FileBits.mkpath path of
Chris@1746 1592 ERROR e => raise Fail ("Failed to create archive directory \""
Chris@1746 1593 ^ path ^ "\": " ^ e)
Chris@1746 1594 | OK () => path
Chris@1746 1595 end
Chris@1746 1596
Chris@1746 1597 fun archive_path archive_dir target_name =
Chris@1746 1598 OS.Path.joinDirFile {
Chris@1746 1599 dir = archive_dir,
Chris@1746 1600 file = target_name
Chris@1746 1601 }
Chris@1746 1602
Chris@1746 1603 fun check_nonexistent path =
Chris@1746 1604 case SOME (OS.FileSys.fileSize path) handle OS.SysErr _ => NONE of
Chris@1746 1605 NONE => ()
Chris@1746 1606 | _ => raise Fail ("Path " ^ path ^ " exists, not overwriting")
Chris@1746 1607
Chris@1746 1608 fun file_url path =
Chris@1746 1609 let val forward_path =
Chris@1746 1610 String.translate (fn #"\\" => "/" |
Chris@1746 1611 c => Char.toString c) path
Chris@1746 1612 in
Chris@1746 1613 (* Path is expected to be absolute already, but if it
Chris@1746 1614 starts with a drive letter, we'll need an extra slash *)
Chris@1746 1615 case explode forward_path of
Chris@1746 1616 #"/"::rest => "file:///" ^ implode rest
Chris@1746 1617 | _ => "file:///" ^ forward_path
Chris@1746 1618 end
Chris@1746 1619
Chris@1746 1620 fun make_archive_copy target_name (vcs, project_id)
Chris@1746 1621 ({ context, ... } : project) =
Chris@1746 1622 let val archive_root = make_archive_root context
Chris@1746 1623 val synthetic_context = {
Chris@1746 1624 rootpath = archive_root,
Chris@1746 1625 extdir = ".",
Chris@1746 1626 providers = [],
Chris@1746 1627 accounts = []
Chris@1746 1628 }
Chris@1746 1629 val synthetic_library = {
Chris@1746 1630 libname = target_name,
Chris@1746 1631 vcs = vcs,
Chris@1746 1632 source = URL_SOURCE (file_url (#rootpath context)),
Chris@1746 1633 branch = DEFAULT_BRANCH, (* overridden by pinned id below *)
Chris@1746 1634 project_pin = PINNED project_id,
Chris@1746 1635 lock_pin = PINNED project_id
Chris@1746 1636 }
Chris@1746 1637 val path = archive_path archive_root target_name
Chris@1746 1638 val _ = print ("Cloning original project to " ^ path
Chris@1746 1639 ^ " at revision " ^ project_id ^ "...\n");
Chris@1746 1640 val _ = check_nonexistent path
Chris@1746 1641 in
Chris@1746 1642 case AnyLibControl.update synthetic_context synthetic_library of
Chris@1746 1643 ERROR e => ERROR ("Failed to clone original project to "
Chris@1746 1644 ^ path ^ ": " ^ e)
Chris@1746 1645 | OK _ => OK archive_root
Chris@1746 1646 end
Chris@1746 1647
Chris@1746 1648 fun update_archive archive_root target_name
Chris@1746 1649 (project as { context, ... } : project) =
Chris@1746 1650 let val synthetic_context = {
Chris@1746 1651 rootpath = archive_path archive_root target_name,
Chris@1746 1652 extdir = #extdir context,
Chris@1746 1653 providers = #providers context,
Chris@1746 1654 accounts = #accounts context
Chris@1746 1655 }
Chris@1746 1656 in
Chris@1746 1657 foldl (fn (lib, acc) =>
Chris@1746 1658 case acc of
Chris@1746 1659 ERROR e => ERROR e
Chris@1746 1660 | OK _ => AnyLibControl.update synthetic_context lib)
Chris@1746 1661 (OK "")
Chris@1746 1662 (#libs project)
Chris@1746 1663 end
Chris@1746 1664
Chris@1746 1665 datatype packer = TAR
Chris@1746 1666 | TAR_GZ
Chris@1746 1667 | TAR_BZ2
Chris@1746 1668 | TAR_XZ
Chris@1746 1669 (* could add other packers, e.g. zip, if we knew how to
Chris@1746 1670 handle the file omissions etc properly in pack_archive *)
Chris@1746 1671
Chris@1746 1672 fun packer_and_basename path =
Chris@1746 1673 let val extensions = [ (".tar", TAR),
Chris@1746 1674 (".tar.gz", TAR_GZ),
Chris@1746 1675 (".tar.bz2", TAR_BZ2),
Chris@1746 1676 (".tar.xz", TAR_XZ)]
Chris@1746 1677 val filename = OS.Path.file path
Chris@1746 1678 in
Chris@1746 1679 foldl (fn ((ext, packer), acc) =>
Chris@1746 1680 if String.isSuffix ext filename
Chris@1746 1681 then SOME (packer,
Chris@1746 1682 String.substring (filename, 0,
Chris@1746 1683 String.size filename -
Chris@1746 1684 String.size ext))
Chris@1746 1685 else acc)
Chris@1746 1686 NONE
Chris@1746 1687 extensions
Chris@1746 1688 end
Chris@1746 1689
Chris@1746 1690 fun pack_archive archive_root target_name target_path packer exclusions =
Chris@1746 1691 case FileBits.command {
Chris@1746 1692 rootpath = archive_root,
Chris@1746 1693 extdir = ".",
Chris@1746 1694 providers = [],
Chris@1746 1695 accounts = []
Chris@1746 1696 } "" ([
Chris@1746 1697 "tar",
Chris@1746 1698 case packer of
Chris@1746 1699 TAR => "cf"
Chris@1746 1700 | TAR_GZ => "czf"
Chris@1746 1701 | TAR_BZ2 => "cjf"
Chris@1746 1702 | TAR_XZ => "cJf",
Chris@1746 1703 target_path,
Chris@1746 1704 "--exclude=.hg",
Chris@1746 1705 "--exclude=.git",
Chris@1746 1706 "--exclude=vext",
Chris@1746 1707 "--exclude=vext.sml",
Chris@1746 1708 "--exclude=vext.ps1",
Chris@1746 1709 "--exclude=vext.bat",
Chris@1746 1710 "--exclude=vext-project.json",
Chris@1746 1711 "--exclude=vext-lock.json"
Chris@1746 1712 ] @ (map (fn e => "--exclude=" ^ e) exclusions) @
Chris@1746 1713 [ target_name ])
Chris@1746 1714 of
Chris@1746 1715 ERROR e => ERROR e
Chris@1746 1716 | OK _ => FileBits.rmpath (archive_path archive_root target_name)
Chris@1746 1717
Chris@1746 1718 fun archive (target_path, exclusions) (project : project) =
Chris@1746 1719 let val _ = check_nonexistent target_path
Chris@1746 1720 val (packer, name) =
Chris@1746 1721 case packer_and_basename target_path of
Chris@1746 1722 NONE => raise Fail ("Unsupported archive file extension in "
Chris@1746 1723 ^ target_path)
Chris@1746 1724 | SOME pn => pn
Chris@1746 1725 val details =
Chris@1746 1726 case project_vcs_and_id (#rootpath (#context project)) of
Chris@1746 1727 ERROR e => raise Fail e
Chris@1746 1728 | OK details => details
Chris@1746 1729 val archive_root =
Chris@1746 1730 case make_archive_copy name details project of
Chris@1746 1731 ERROR e => raise Fail e
Chris@1746 1732 | OK archive_root => archive_root
Chris@1746 1733 val outcome =
Chris@1746 1734 case update_archive archive_root name project of
Chris@1746 1735 ERROR e => ERROR e
Chris@1746 1736 | OK _ =>
Chris@1746 1737 case pack_archive archive_root name
Chris@1746 1738 target_path packer exclusions of
Chris@1746 1739 ERROR e => ERROR e
Chris@1746 1740 | OK _ => OK ()
Chris@1746 1741 in
Chris@1746 1742 case outcome of
Chris@1746 1743 ERROR e => raise Fail e
Chris@1746 1744 | OK () => OS.Process.success
Chris@1746 1745 end
Chris@1746 1746
Chris@1746 1747 end
Chris@1746 1748
Chris@1724 1749 val libobjname = "libraries"
Chris@1724 1750
Chris@1706 1751 fun load_libspec spec_json lock_json libname : libspec =
Chris@1706 1752 let open JsonBits
Chris@1724 1753 val libobj = lookup_mandatory spec_json [libobjname, libname]
Chris@1706 1754 val vcs = lookup_mandatory_string libobj ["vcs"]
Chris@1706 1755 val retrieve = lookup_optional_string libobj
Chris@1706 1756 val service = retrieve ["service"]
Chris@1706 1757 val owner = retrieve ["owner"]
Chris@1706 1758 val repo = retrieve ["repository"]
Chris@1706 1759 val url = retrieve ["url"]
Chris@1706 1760 val branch = retrieve ["branch"]
Chris@1740 1761 val project_pin = case retrieve ["pin"] of
Chris@1740 1762 NONE => UNPINNED
Chris@1740 1763 | SOME p => PINNED p
Chris@1724 1764 val lock_pin = case lookup_optional lock_json [libobjname, libname] of
Chris@1740 1765 NONE => UNPINNED
Chris@1740 1766 | SOME ll => case lookup_optional_string ll ["pin"] of
Chris@1740 1767 SOME p => PINNED p
Chris@1740 1768 | NONE => UNPINNED
Chris@1706 1769 in
Chris@1706 1770 {
Chris@1706 1771 libname = libname,
Chris@1706 1772 vcs = case vcs of
Chris@1706 1773 "hg" => HG
Chris@1706 1774 | "git" => GIT
Chris@1706 1775 | other => raise Fail ("Unknown version-control system \"" ^
Chris@1706 1776 other ^ "\""),
Chris@1706 1777 source = case (url, service, owner, repo) of
Chris@1721 1778 (SOME u, NONE, _, _) => URL_SOURCE u
Chris@1706 1779 | (NONE, SOME ss, owner, repo) =>
Chris@1721 1780 SERVICE_SOURCE { service = ss, owner = owner, repo = repo }
Chris@1706 1781 | _ => raise Fail ("Must have exactly one of service " ^
Chris@1706 1782 "or url string"),
Chris@1740 1783 project_pin = project_pin,
Chris@1740 1784 lock_pin = lock_pin,
Chris@1706 1785 branch = case branch of
Chris@1706 1786 SOME b => BRANCH b
Chris@1706 1787 | NONE => DEFAULT_BRANCH
Chris@1706 1788 }
Chris@1706 1789 end
Chris@1706 1790
Chris@1706 1791 fun load_userconfig () : userconfig =
Chris@1706 1792 let val home = FileBits.homedir ()
Chris@1706 1793 val conf_json =
Chris@1706 1794 JsonBits.load_json_from
Chris@1706 1795 (OS.Path.joinDirFile {
Chris@1706 1796 dir = home,
Chris@1706 1797 file = VextFilenames.user_config_file })
Chris@1706 1798 handle IO.Io _ => Json.OBJECT []
Chris@1706 1799 in
Chris@1706 1800 {
Chris@1706 1801 accounts = case JsonBits.lookup_optional conf_json ["accounts"] of
Chris@1706 1802 NONE => []
Chris@1706 1803 | SOME (Json.OBJECT aa) =>
Chris@1706 1804 map (fn (k, (Json.STRING v)) =>
Chris@1706 1805 { service = k, login = v }
Chris@1706 1806 | _ => raise Fail
Chris@1706 1807 "String expected for account name")
Chris@1706 1808 aa
Chris@1706 1809 | _ => raise Fail "Array expected for accounts",
Chris@1706 1810 providers = Provider.load_providers conf_json
Chris@1706 1811 }
Chris@1706 1812 end
Chris@1706 1813
Chris@1732 1814 datatype pintype =
Chris@1732 1815 NO_LOCKFILE |
Chris@1732 1816 USE_LOCKFILE
Chris@1732 1817
Chris@1732 1818 fun load_project (userconfig : userconfig) rootpath pintype : project =
Chris@1706 1819 let val spec_file = FileBits.project_spec_path rootpath
Chris@1706 1820 val lock_file = FileBits.project_lock_path rootpath
Chris@1706 1821 val _ = if OS.FileSys.access (spec_file, [OS.FileSys.A_READ])
Chris@1706 1822 handle OS.SysErr _ => false
Chris@1706 1823 then ()
Chris@1706 1824 else raise Fail ("Failed to open project spec file " ^
Chris@1706 1825 (VextFilenames.project_file) ^ " in " ^
Chris@1706 1826 rootpath ^
Chris@1706 1827 ".\nPlease ensure the spec file is in the " ^
Chris@1706 1828 "project root and run this from there.")
Chris@1706 1829 val spec_json = JsonBits.load_json_from spec_file
Chris@1732 1830 val lock_json = if pintype = USE_LOCKFILE
Chris@1706 1831 then JsonBits.load_json_from lock_file
Chris@1706 1832 handle IO.Io _ => Json.OBJECT []
Chris@1706 1833 else Json.OBJECT []
Chris@1706 1834 val extdir = JsonBits.lookup_mandatory_string spec_json
Chris@1706 1835 ["config", "extdir"]
Chris@1724 1836 val spec_libs = JsonBits.lookup_optional spec_json [libobjname]
Chris@1724 1837 val lock_libs = JsonBits.lookup_optional lock_json [libobjname]
Chris@1706 1838 val providers = Provider.load_more_providers
Chris@1706 1839 (#providers userconfig) spec_json
Chris@1706 1840 val libnames = case spec_libs of
Chris@1706 1841 NONE => []
Chris@1706 1842 | SOME (Json.OBJECT ll) => map (fn (k, v) => k) ll
Chris@1706 1843 | _ => raise Fail "Object expected for libs"
Chris@1706 1844 in
Chris@1706 1845 {
Chris@1706 1846 context = {
Chris@1706 1847 rootpath = rootpath,
Chris@1706 1848 extdir = extdir,
Chris@1706 1849 providers = providers,
Chris@1706 1850 accounts = #accounts userconfig
Chris@1706 1851 },
Chris@1706 1852 libs = map (load_libspec spec_json lock_json) libnames
Chris@1706 1853 }
Chris@1706 1854 end
Chris@1706 1855
Chris@1706 1856 fun save_lock_file rootpath locks =
Chris@1706 1857 let val lock_file = FileBits.project_lock_path rootpath
Chris@1706 1858 open Json
Chris@1706 1859 val lock_json =
Chris@1706 1860 OBJECT [
Chris@1724 1861 (libobjname,
Chris@1724 1862 OBJECT (map (fn { libname, id_or_tag } =>
Chris@1724 1863 (libname,
Chris@1724 1864 OBJECT [ ("pin", STRING id_or_tag) ]))
Chris@1724 1865 locks))
Chris@1706 1866 ]
Chris@1706 1867 in
Chris@1706 1868 JsonBits.save_json_to lock_file lock_json
Chris@1706 1869 end
Chris@1706 1870
Chris@1706 1871 fun pad_to n str =
Chris@1706 1872 if n <= String.size str then str
Chris@1706 1873 else pad_to n (str ^ " ")
Chris@1706 1874
Chris@1706 1875 fun hline_to 0 = ""
Chris@1706 1876 | hline_to n = "-" ^ hline_to (n-1)
Chris@1706 1877
Chris@1706 1878 val libname_width = 25
Chris@1706 1879 val libstate_width = 11
Chris@1740 1880 val localstate_width = 17
Chris@1706 1881 val notes_width = 5
Chris@1706 1882 val divider = " | "
Chris@1740 1883 val clear_line = "\r" ^ pad_to 80 "";
Chris@1706 1884
Chris@1706 1885 fun print_status_header () =
Chris@1740 1886 print (clear_line ^ "\n " ^
Chris@1706 1887 pad_to libname_width "Library" ^ divider ^
Chris@1706 1888 pad_to libstate_width "State" ^ divider ^
Chris@1706 1889 pad_to localstate_width "Local" ^ divider ^
Chris@1706 1890 "Notes" ^ "\n " ^
Chris@1706 1891 hline_to libname_width ^ "-+-" ^
Chris@1706 1892 hline_to libstate_width ^ "-+-" ^
Chris@1706 1893 hline_to localstate_width ^ "-+-" ^
Chris@1706 1894 hline_to notes_width ^ "\n")
Chris@1706 1895
Chris@1706 1896 fun print_outcome_header () =
Chris@1740 1897 print (clear_line ^ "\n " ^
Chris@1706 1898 pad_to libname_width "Library" ^ divider ^
Chris@1706 1899 pad_to libstate_width "Outcome" ^ divider ^
Chris@1706 1900 "Notes" ^ "\n " ^
Chris@1706 1901 hline_to libname_width ^ "-+-" ^
Chris@1706 1902 hline_to libstate_width ^ "-+-" ^
Chris@1706 1903 hline_to notes_width ^ "\n")
Chris@1706 1904
Chris@1706 1905 fun print_status with_network (libname, status) =
Chris@1706 1906 let val libstate_str =
Chris@1706 1907 case status of
Chris@1706 1908 OK (ABSENT, _) => "Absent"
Chris@1706 1909 | OK (CORRECT, _) => if with_network then "Correct" else "Present"
Chris@1706 1910 | OK (SUPERSEDED, _) => "Superseded"
Chris@1706 1911 | OK (WRONG, _) => "Wrong"
Chris@1706 1912 | ERROR _ => "Error"
Chris@1706 1913 val localstate_str =
Chris@1706 1914 case status of
Chris@1706 1915 OK (_, MODIFIED) => "Modified"
Chris@1740 1916 | OK (_, LOCK_MISMATCHED) => "Differs from Lock"
Chris@1740 1917 | OK (_, CLEAN) => "Clean"
Chris@1740 1918 | ERROR _ => ""
Chris@1706 1919 val error_str =
Chris@1706 1920 case status of
Chris@1706 1921 ERROR e => e
Chris@1706 1922 | _ => ""
Chris@1706 1923 in
Chris@1706 1924 print (" " ^
Chris@1706 1925 pad_to libname_width libname ^ divider ^
Chris@1706 1926 pad_to libstate_width libstate_str ^ divider ^
Chris@1706 1927 pad_to localstate_width localstate_str ^ divider ^
Chris@1706 1928 error_str ^ "\n")
Chris@1706 1929 end
Chris@1706 1930
Chris@1706 1931 fun print_update_outcome (libname, outcome) =
Chris@1706 1932 let val outcome_str =
Chris@1706 1933 case outcome of
Chris@1706 1934 OK id => "Ok"
Chris@1706 1935 | ERROR e => "Failed"
Chris@1706 1936 val error_str =
Chris@1706 1937 case outcome of
Chris@1706 1938 ERROR e => e
Chris@1706 1939 | _ => ""
Chris@1706 1940 in
Chris@1706 1941 print (" " ^
Chris@1706 1942 pad_to libname_width libname ^ divider ^
Chris@1706 1943 pad_to libstate_width outcome_str ^ divider ^
Chris@1706 1944 error_str ^ "\n")
Chris@1706 1945 end
Chris@1706 1946
Chris@1706 1947 fun act_and_print action print_header print_line (libs : libspec list) =
Chris@1706 1948 let val lines = map (fn lib => (#libname lib, action lib)) libs
Chris@1706 1949 val _ = print_header ()
Chris@1706 1950 in
Chris@1706 1951 app print_line lines;
Chris@1706 1952 lines
Chris@1706 1953 end
Chris@1708 1954
Chris@1708 1955 fun return_code_for outcomes =
Chris@1708 1956 foldl (fn ((_, result), acc) =>
Chris@1708 1957 case result of
Chris@1708 1958 ERROR _ => OS.Process.failure
Chris@1708 1959 | _ => acc)
Chris@1708 1960 OS.Process.success
Chris@1708 1961 outcomes
Chris@1706 1962
Chris@1706 1963 fun status_of_project ({ context, libs } : project) =
Chris@1708 1964 return_code_for (act_and_print (AnyLibControl.status context)
Chris@1708 1965 print_status_header (print_status false)
Chris@1708 1966 libs)
Chris@1706 1967
Chris@1706 1968 fun review_project ({ context, libs } : project) =
Chris@1708 1969 return_code_for (act_and_print (AnyLibControl.review context)
Chris@1708 1970 print_status_header (print_status true)
Chris@1708 1971 libs)
Chris@1706 1972
Chris@1706 1973 fun update_project ({ context, libs } : project) =
Chris@1706 1974 let val outcomes = act_and_print
Chris@1706 1975 (AnyLibControl.update context)
Chris@1706 1976 print_outcome_header print_update_outcome libs
Chris@1708 1977 val locks =
Chris@1708 1978 List.concat
Chris@1708 1979 (map (fn (libname, result) =>
Chris@1708 1980 case result of
Chris@1708 1981 ERROR _ => []
Chris@1708 1982 | OK id => [{ libname = libname, id_or_tag = id }])
Chris@1708 1983 outcomes)
Chris@1708 1984 val return_code = return_code_for outcomes
Chris@1706 1985 in
Chris@1708 1986 if OS.Process.isSuccess return_code
Chris@1708 1987 then save_lock_file (#rootpath context) locks
Chris@1708 1988 else ();
Chris@1708 1989 return_code
Chris@1706 1990 end
Chris@1706 1991
Chris@1740 1992 fun lock_project ({ context, libs } : project) =
Chris@1740 1993 let val outcomes = map (fn lib =>
Chris@1740 1994 (#libname lib, AnyLibControl.id_of context lib))
Chris@1740 1995 libs
Chris@1740 1996 val locks =
Chris@1740 1997 List.concat
Chris@1740 1998 (map (fn (libname, result) =>
Chris@1740 1999 case result of
Chris@1740 2000 ERROR _ => []
Chris@1740 2001 | OK id => [{ libname = libname, id_or_tag = id }])
Chris@1740 2002 outcomes)
Chris@1740 2003 val return_code = return_code_for outcomes
Chris@1740 2004 val _ = print clear_line
Chris@1740 2005 in
Chris@1740 2006 if OS.Process.isSuccess return_code
Chris@1740 2007 then save_lock_file (#rootpath context) locks
Chris@1740 2008 else ();
Chris@1740 2009 return_code
Chris@1740 2010 end
Chris@1746 2011
Chris@1732 2012 fun load_local_project pintype =
Chris@1706 2013 let val userconfig = load_userconfig ()
Chris@1706 2014 val rootpath = OS.FileSys.getDir ()
Chris@1706 2015 in
Chris@1732 2016 load_project userconfig rootpath pintype
Chris@1706 2017 end
Chris@1706 2018
Chris@1732 2019 fun with_local_project pintype f =
Chris@1732 2020 let val return_code = f (load_local_project pintype)
Chris@1746 2021 handle e => (print ("Error: " ^ exnMessage e);
Chris@1746 2022 OS.Process.failure)
Chris@1708 2023 val _ = print "\n";
Chris@1708 2024 in
Chris@1708 2025 return_code
Chris@1708 2026 end
Chris@1706 2027
Chris@1740 2028 fun review () = with_local_project USE_LOCKFILE review_project
Chris@1740 2029 fun status () = with_local_project USE_LOCKFILE status_of_project
Chris@1732 2030 fun update () = with_local_project NO_LOCKFILE update_project
Chris@1740 2031 fun lock () = with_local_project NO_LOCKFILE lock_project
Chris@1732 2032 fun install () = with_local_project USE_LOCKFILE update_project
Chris@1706 2033
Chris@1706 2034 fun version () =
Chris@1708 2035 (print ("v" ^ vext_version ^ "\n");
Chris@1708 2036 OS.Process.success)
Chris@1706 2037
Chris@1706 2038 fun usage () =
Chris@1706 2039 (print "\nVext ";
Chris@1706 2040 version ();
Chris@1706 2041 print ("\nA simple manager for third-party source code dependencies.\n\n"
Chris@1706 2042 ^ "Usage:\n\n"
Chris@1706 2043 ^ " vext <command>\n\n"
Chris@1706 2044 ^ "where <command> is one of:\n\n"
Chris@1716 2045 ^ " status print quick report on local status only, without using network\n"
Chris@1706 2046 ^ " review check configured libraries against their providers, and report\n"
Chris@1706 2047 ^ " install update configured libraries according to project specs and lock file\n"
Chris@1706 2048 ^ " update update configured libraries and lock file according to project specs\n"
Chris@1740 2049 ^ " lock update lock file to match local library status\n"
Chris@1746 2050 ^ " archive pack up project and all libraries into an archive file\n"
Chris@1746 2051 ^ " (invoke as 'vext archive target-file.tar.gz')\n"
Chris@1708 2052 ^ " version print the Vext version number and exit\n\n");
Chris@1708 2053 OS.Process.failure)
Chris@1706 2054
Chris@1746 2055 fun archive target args =
Chris@1746 2056 case args of
Chris@1746 2057 [] =>
Chris@1746 2058 with_local_project USE_LOCKFILE (Archive.archive (target, []))
Chris@1746 2059 | "--exclude"::xs =>
Chris@1746 2060 with_local_project USE_LOCKFILE (Archive.archive (target, xs))
Chris@1746 2061 | _ => usage ()
Chris@1746 2062
Chris@1706 2063 fun vext args =
Chris@1708 2064 let val return_code =
Chris@1708 2065 case args of
Chris@1708 2066 ["review"] => review ()
Chris@1708 2067 | ["status"] => status ()
Chris@1708 2068 | ["install"] => install ()
Chris@1708 2069 | ["update"] => update ()
Chris@1740 2070 | ["lock"] => lock ()
Chris@1708 2071 | ["version"] => version ()
Chris@1746 2072 | "archive"::target::args => archive target args
Chris@1708 2073 | _ => usage ()
Chris@1708 2074 in
Chris@1708 2075 OS.Process.exit return_code;
Chris@1708 2076 ()
Chris@1708 2077 end
Chris@1706 2078
Chris@1706 2079 fun main () =
Chris@1706 2080 vext (CommandLine.arguments ())