annotate repoint.sml @ 266:d04675d44928 tip master

Refer to SDK from Github
author Chris Cannam <cannam@all-day-breakfast.com>
date Wed, 02 Jun 2021 14:41:26 +0100
parents 84de57beb8d6
children
rev   line source
c@219 1 (*
c@219 2 DO NOT EDIT THIS FILE.
c@219 3 This file is automatically generated from the individual
c@219 4 source files in the Repoint repository.
c@219 5 *)
c@219 6
c@219 7 (*
c@219 8 Repoint
c@219 9
c@219 10 A simple manager for third-party source code dependencies
c@219 11
c@219 12 Copyright 2018 Chris Cannam, Particular Programs Ltd,
c@219 13 and Queen Mary, University of London
c@219 14
c@219 15 Permission is hereby granted, free of charge, to any person
c@219 16 obtaining a copy of this software and associated documentation
c@219 17 files (the "Software"), to deal in the Software without
c@219 18 restriction, including without limitation the rights to use, copy,
c@219 19 modify, merge, publish, distribute, sublicense, and/or sell copies
c@219 20 of the Software, and to permit persons to whom the Software is
c@219 21 furnished to do so, subject to the following conditions:
c@219 22
c@219 23 The above copyright notice and this permission notice shall be
c@219 24 included in all copies or substantial portions of the Software.
c@219 25
c@219 26 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
c@219 27 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
c@219 28 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
c@219 29 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
c@219 30 ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
c@219 31 CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
c@219 32 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
c@219 33
c@219 34 Except as contained in this notice, the names of Chris Cannam,
c@219 35 Particular Programs Ltd, and Queen Mary, University of London
c@219 36 shall not be used in advertising or otherwise to promote the sale,
c@219 37 use or other dealings in this Software without prior written
c@219 38 authorization.
c@219 39 *)
c@219 40
cannam@258 41 val repoint_version = "1.2"
c@219 42
c@219 43
c@219 44 datatype vcs =
c@219 45 HG |
c@219 46 GIT |
c@219 47 SVN
c@219 48
c@219 49 datatype source =
c@219 50 URL_SOURCE of string |
c@219 51 SERVICE_SOURCE of {
c@219 52 service : string,
c@219 53 owner : string option,
c@219 54 repo : string option
c@219 55 }
c@219 56
c@219 57 type id_or_tag = string
c@219 58
c@219 59 datatype pin =
c@219 60 UNPINNED |
c@219 61 PINNED of id_or_tag
c@219 62
c@219 63 datatype libstate =
c@219 64 ABSENT |
c@219 65 CORRECT |
c@219 66 SUPERSEDED |
c@219 67 WRONG
c@219 68
c@219 69 datatype localstate =
c@219 70 MODIFIED |
c@219 71 LOCK_MISMATCHED |
c@219 72 CLEAN
c@219 73
c@219 74 datatype branch =
c@219 75 BRANCH of string |
c@219 76 DEFAULT_BRANCH
c@219 77
c@219 78 (* If we can recover from an error, for example by reporting failure
c@219 79 for this one thing and going on to the next thing, then the error
c@219 80 should usually be returned through a result type rather than an
c@219 81 exception. *)
c@219 82
c@219 83 datatype 'a result =
c@219 84 OK of 'a |
c@219 85 ERROR of string
c@219 86
c@219 87 type libname = string
c@219 88
c@219 89 type libspec = {
c@219 90 libname : libname,
c@219 91 vcs : vcs,
c@219 92 source : source,
c@219 93 branch : branch,
c@219 94 project_pin : pin,
c@219 95 lock_pin : pin
c@219 96 }
c@219 97
c@219 98 type lock = {
c@219 99 libname : libname,
c@219 100 id_or_tag : id_or_tag
c@219 101 }
c@219 102
c@219 103 type remote_spec = {
c@219 104 anon : string option,
c@219 105 auth : string option
c@219 106 }
c@219 107
c@219 108 type provider = {
c@219 109 service : string,
c@219 110 supports : vcs list,
c@219 111 remote_spec : remote_spec
c@219 112 }
c@219 113
c@219 114 type account = {
c@219 115 service : string,
c@219 116 login : string
c@219 117 }
c@219 118
c@219 119 type context = {
c@219 120 rootpath : string,
c@219 121 extdir : string,
c@219 122 providers : provider list,
c@219 123 accounts : account list
c@219 124 }
c@219 125
c@219 126 type userconfig = {
c@219 127 providers : provider list,
c@219 128 accounts : account list
c@219 129 }
c@219 130
c@219 131 type project = {
c@219 132 context : context,
c@219 133 libs : libspec list
c@219 134 }
c@219 135
c@219 136 structure RepointFilenames = struct
c@219 137 val project_file = "repoint-project.json"
c@219 138 val project_lock_file = "repoint-lock.json"
cannam@258 139 val project_completion_file = ".repoint.point"
c@219 140 val user_config_file = ".repoint.json"
c@219 141 val archive_dir = ".repoint-archive"
c@219 142 end
c@219 143
c@219 144 signature VCS_CONTROL = sig
c@219 145
c@219 146 (** Check whether the given VCS is installed and working *)
c@219 147 val is_working : context -> bool result
c@219 148
c@219 149 (** Test whether the library is present locally at all *)
c@219 150 val exists : context -> libname -> bool result
c@219 151
c@219 152 (** Return the id (hash) of the current revision for the library *)
c@219 153 val id_of : context -> libname -> id_or_tag result
c@219 154
c@219 155 (** Test whether the library is at the given id *)
c@219 156 val is_at : context -> libname * id_or_tag -> bool result
c@219 157
c@219 158 (** Test whether the library is on the given branch, i.e. is at
c@219 159 the branch tip or an ancestor of it *)
c@219 160 val is_on_branch : context -> libname * branch -> bool result
c@219 161
c@219 162 (** Test whether the library is at the newest revision for the
c@219 163 given branch. False may indicate that the branch has advanced
c@219 164 or that the library is not on the branch at all. This function
c@219 165 may use the network to check for new revisions *)
c@219 166 val is_newest : context -> libname * source * branch -> bool result
c@219 167
c@219 168 (** Test whether the library is at the newest revision available
c@219 169 locally for the given branch. False may indicate that the
c@219 170 branch has advanced or that the library is not on the branch
c@219 171 at all. This function must not use the network *)
c@219 172 val is_newest_locally : context -> libname * branch -> bool result
c@219 173
c@219 174 (** Test whether the library has been modified in the local
c@219 175 working copy *)
c@219 176 val is_modified_locally : context -> libname -> bool result
c@219 177
c@219 178 (** Check out, i.e. clone a fresh copy of, the repo for the given
c@219 179 library on the given branch *)
c@219 180 val checkout : context -> libname * source * branch -> unit result
c@219 181
c@219 182 (** Update the library to the given branch tip. Assumes that a
c@219 183 local copy of the library already exists *)
c@219 184 val update : context -> libname * source * branch -> unit result
c@219 185
c@219 186 (** Update the library to the given specific id or tag *)
c@219 187 val update_to : context -> libname * source * id_or_tag -> unit result
c@219 188
c@219 189 (** Return a URL from which the library can be cloned, given that
c@219 190 the local copy already exists. For a DVCS this can be the
c@219 191 local copy, but for a centralised VCS it will have to be the
c@219 192 remote repository URL. Used for archiving *)
c@219 193 val copy_url_for : context -> libname -> string result
c@219 194 end
c@219 195
c@219 196 signature LIB_CONTROL = sig
c@219 197 val review : context -> libspec -> (libstate * localstate) result
c@219 198 val status : context -> libspec -> (libstate * localstate) result
c@219 199 val update : context -> libspec -> unit result
c@219 200 val id_of : context -> libspec -> id_or_tag result
c@219 201 val is_working : context -> vcs -> bool result
c@219 202 end
c@219 203
c@219 204 structure FileBits :> sig
c@219 205 val extpath : context -> string
c@219 206 val libpath : context -> libname -> string
c@219 207 val subpath : context -> libname -> string -> string
c@219 208 val command_output : context -> libname -> string list -> string result
c@219 209 val command : context -> libname -> string list -> unit result
c@219 210 val file_url : string -> string
c@219 211 val file_contents : string -> string
c@219 212 val mydir : unit -> string
c@219 213 val homedir : unit -> string
c@219 214 val mkpath : string -> unit result
c@219 215 val rmpath : string -> unit result
c@219 216 val nonempty_dir_exists : string -> bool
c@219 217 val project_spec_path : string -> string
c@219 218 val project_lock_path : string -> string
cannam@258 219 val project_completion_path : string -> string
c@219 220 val verbose : unit -> bool
c@219 221 end = struct
c@219 222
c@219 223 fun verbose () =
c@219 224 case OS.Process.getEnv "REPOINT_VERBOSE" of
c@219 225 SOME "0" => false
c@219 226 | SOME _ => true
c@219 227 | NONE => false
c@219 228
c@219 229 fun split_relative path desc =
c@219 230 case OS.Path.fromString path of
c@219 231 { isAbs = true, ... } => raise Fail (desc ^ " may not be absolute")
c@219 232 | { arcs, ... } => arcs
c@219 233
c@219 234 fun extpath ({ rootpath, extdir, ... } : context) =
c@219 235 let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
c@219 236 in OS.Path.toString {
c@219 237 isAbs = isAbs,
c@219 238 vol = vol,
c@219 239 arcs = arcs @
c@219 240 split_relative extdir "extdir"
c@219 241 }
c@219 242 end
c@219 243
c@219 244 fun subpath ({ rootpath, extdir, ... } : context) libname remainder =
c@219 245 (* NB libname is allowed to be a path fragment, e.g. foo/bar *)
c@219 246 let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
c@219 247 in OS.Path.toString {
c@219 248 isAbs = isAbs,
c@219 249 vol = vol,
c@219 250 arcs = arcs @
c@219 251 split_relative extdir "extdir" @
c@219 252 split_relative libname "library path" @
c@219 253 split_relative remainder "subpath"
c@219 254 }
c@219 255 end
c@219 256
c@219 257 fun libpath context "" =
c@219 258 extpath context
c@219 259 | libpath context libname =
c@219 260 subpath context libname ""
c@219 261
c@219 262 fun project_file_path rootpath filename =
c@219 263 let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
c@219 264 in OS.Path.toString {
c@219 265 isAbs = isAbs,
c@219 266 vol = vol,
c@219 267 arcs = arcs @ [ filename ]
c@219 268 }
c@219 269 end
c@219 270
c@219 271 fun project_spec_path rootpath =
c@219 272 project_file_path rootpath (RepointFilenames.project_file)
c@219 273
c@219 274 fun project_lock_path rootpath =
c@219 275 project_file_path rootpath (RepointFilenames.project_lock_file)
c@219 276
cannam@258 277 fun project_completion_path rootpath =
cannam@258 278 project_file_path rootpath (RepointFilenames.project_completion_file)
cannam@258 279
c@219 280 fun trim str =
c@219 281 hd (String.fields (fn x => x = #"\n" orelse x = #"\r") str)
cannam@258 282
cannam@258 283 fun make_canonical path =
cannam@258 284 (* SML/NJ doesn't properly handle "/" when splitting paths -
cannam@258 285 it should be a path separator even on Windows, but SML/NJ
cannam@258 286 treats it as a normal filename character there. So we must
cannam@258 287 convert these explicitly *)
cannam@258 288 OS.Path.mkCanonical
cannam@258 289 (if OS.Path.concat ("a", "b") = "a\\b"
cannam@258 290 then String.translate (fn #"/" => "\\" |
cannam@258 291 c => Char.toString c)
cannam@258 292 path
cannam@258 293 else path)
c@219 294
c@219 295 fun file_url path =
c@219 296 let val forward_path =
c@219 297 String.translate (fn #"\\" => "/" |
c@219 298 c => Char.toString c)
c@219 299 (OS.Path.mkCanonical path)
c@219 300 in
c@219 301 (* Path is expected to be absolute already, but if it
c@219 302 starts with a drive letter, we'll need an extra slash *)
c@219 303 case explode forward_path of
c@219 304 #"/"::rest => "file:///" ^ implode rest
c@219 305 | _ => "file:///" ^ forward_path
c@219 306 end
c@219 307
c@219 308 fun file_contents filename =
c@219 309 let val stream = TextIO.openIn filename
c@219 310 fun read_all str acc =
c@219 311 case TextIO.inputLine str of
c@219 312 SOME line => read_all str (trim line :: acc)
c@219 313 | NONE => rev acc
c@219 314 val contents = read_all stream []
c@219 315 val _ = TextIO.closeIn stream
c@219 316 in
c@219 317 String.concatWith "\n" contents
c@219 318 end
c@219 319
c@219 320 fun expand_commandline cmdlist =
c@219 321 (* We are quite strict about what we accept here, except
c@219 322 for the first element in cmdlist which is assumed to be a
c@219 323 known command location rather than arbitrary user input. *)
c@219 324 let open Char
c@219 325 fun quote arg =
c@219 326 if List.all
c@219 327 (fn c => isAlphaNum c orelse c = #"-" orelse c = #"_")
c@219 328 (explode arg)
c@219 329 then arg
c@219 330 else "\"" ^ arg ^ "\""
c@219 331 fun check arg =
cannam@258 332 let val valid = explode " /#:;?,._-{}@=+%"
c@219 333 in
c@219 334 app (fn c =>
c@219 335 if isAlphaNum c orelse
c@219 336 List.exists (fn v => v = c) valid orelse
c@219 337 c > chr 127
c@219 338 then ()
c@219 339 else raise Fail ("Invalid character '" ^
c@219 340 (Char.toString c) ^
c@219 341 "' in command list"))
c@219 342 (explode arg);
c@219 343 arg
c@219 344 end
c@219 345 in
c@219 346 String.concatWith " "
c@219 347 (map quote
c@219 348 (hd cmdlist :: map check (tl cmdlist)))
c@219 349 end
c@219 350
c@219 351 val tick_cycle = ref 0
c@219 352 val tick_chars = Vector.fromList (map String.str (explode "|/-\\"))
c@219 353
c@219 354 fun tick libname cmdlist =
c@219 355 let val n = Vector.length tick_chars
c@219 356 fun pad_to n str =
c@219 357 if n <= String.size str then str
c@219 358 else pad_to n (str ^ " ")
c@219 359 val name = if libname <> "" then libname
c@219 360 else if cmdlist = nil then ""
c@219 361 else hd (rev cmdlist)
c@219 362 in
c@219 363 print (" " ^
c@219 364 Vector.sub(tick_chars, !tick_cycle) ^ " " ^
c@219 365 pad_to 70 name ^
c@219 366 "\r");
c@219 367 tick_cycle := (if !tick_cycle = n - 1 then 0 else 1 + !tick_cycle)
c@219 368 end
c@219 369
c@219 370 fun run_command context libname cmdlist redirect =
c@219 371 let open OS
c@219 372 val dir = libpath context libname
c@219 373 val cmd = expand_commandline cmdlist
c@219 374 val _ = if verbose ()
c@219 375 then print ("\n=== " ^ dir ^ "\n<<< " ^ cmd ^ "\n")
c@219 376 else tick libname cmdlist
c@219 377 val _ = FileSys.chDir dir
c@219 378 val status = case redirect of
c@219 379 NONE => Process.system cmd
c@219 380 | SOME file => Process.system (cmd ^ ">" ^ file)
c@219 381 in
c@219 382 if Process.isSuccess status
c@219 383 then OK ()
c@219 384 else ERROR ("Command failed: " ^ cmd ^ " (in dir " ^ dir ^ ")")
c@219 385 end
c@219 386 handle ex => ERROR ("Unable to run command: " ^ exnMessage ex)
c@219 387
c@219 388 fun command context libname cmdlist =
c@219 389 run_command context libname cmdlist NONE
c@219 390
c@219 391 fun command_output context libname cmdlist =
c@219 392 let open OS
c@219 393 val tmpFile = FileSys.tmpName ()
c@219 394 val result = run_command context libname cmdlist (SOME tmpFile)
c@219 395 val contents = file_contents tmpFile
c@219 396 val _ = if verbose ()
c@219 397 then print (">>> \"" ^ contents ^ "\"\n")
c@219 398 else ()
c@219 399 in
c@219 400 FileSys.remove tmpFile handle _ => ();
c@219 401 case result of
c@219 402 OK () => OK contents
c@219 403 | ERROR e => ERROR e
c@219 404 end
c@219 405
c@219 406 fun mydir () =
c@219 407 let open OS
c@219 408 val { dir, file } = Path.splitDirFile (CommandLine.name ())
c@219 409 in
c@219 410 FileSys.realPath
c@219 411 (if Path.isAbsolute dir
c@219 412 then dir
c@219 413 else Path.concat (FileSys.getDir (), dir))
c@219 414 end
c@219 415
c@219 416 fun homedir () =
c@219 417 (* Failure is not routine, so we use an exception here *)
c@219 418 case (OS.Process.getEnv "HOME",
c@219 419 OS.Process.getEnv "HOMEPATH") of
c@219 420 (SOME home, _) => home
c@219 421 | (NONE, SOME home) => home
c@219 422 | (NONE, NONE) =>
c@219 423 raise Fail "Failed to look up home directory from environment"
c@219 424
c@219 425 fun mkpath' path =
c@219 426 if OS.FileSys.isDir path handle _ => false
c@219 427 then OK ()
c@219 428 else case OS.Path.fromString path of
c@219 429 { arcs = nil, ... } => OK ()
c@219 430 | { isAbs = false, ... } => ERROR "mkpath requires absolute path"
c@219 431 | { isAbs, vol, arcs } =>
c@219 432 case mkpath' (OS.Path.toString { (* parent *)
c@219 433 isAbs = isAbs,
c@219 434 vol = vol,
c@219 435 arcs = rev (tl (rev arcs)) }) of
c@219 436 ERROR e => ERROR e
c@219 437 | OK () => ((OS.FileSys.mkDir path; OK ())
c@219 438 handle OS.SysErr (e, _) =>
c@219 439 ERROR ("Directory creation failed: " ^ e))
c@219 440
c@219 441 fun mkpath path =
cannam@258 442 mkpath' (make_canonical path)
c@219 443
c@219 444 fun dir_contents dir =
c@219 445 let open OS
c@219 446 fun files_from dirstream =
c@219 447 case FileSys.readDir dirstream of
c@219 448 NONE => []
c@219 449 | SOME file =>
c@219 450 (* readDir is supposed to filter these,
c@219 451 but let's be extra cautious: *)
c@219 452 if file = Path.parentArc orelse file = Path.currentArc
c@219 453 then files_from dirstream
c@219 454 else file :: files_from dirstream
c@219 455 val stream = FileSys.openDir dir
c@219 456 val files = map (fn f => Path.joinDirFile
c@219 457 { dir = dir, file = f })
c@219 458 (files_from stream)
c@219 459 val _ = FileSys.closeDir stream
c@219 460 in
c@219 461 files
c@219 462 end
c@219 463
c@219 464 fun rmpath' path =
c@219 465 let open OS
c@219 466 fun remove path =
c@219 467 if FileSys.isLink path (* dangling links bother isDir *)
c@219 468 then FileSys.remove path
c@219 469 else if FileSys.isDir path
c@219 470 then (app remove (dir_contents path); FileSys.rmDir path)
c@219 471 else FileSys.remove path
c@219 472 in
c@219 473 (remove path; OK ())
c@219 474 handle SysErr (e, _) => ERROR ("Path removal failed: " ^ e)
c@219 475 end
c@219 476
c@219 477 fun rmpath path =
cannam@258 478 rmpath' (make_canonical path)
c@219 479
c@219 480 fun nonempty_dir_exists path =
c@219 481 let open OS.FileSys
c@219 482 in
c@219 483 (not (isLink path) andalso
c@219 484 isDir path andalso
c@219 485 dir_contents path <> [])
c@219 486 handle _ => false
c@219 487 end
c@219 488
c@219 489 end
c@219 490
c@219 491 functor LibControlFn (V: VCS_CONTROL) :> LIB_CONTROL = struct
c@219 492
c@219 493 (* Valid states for unpinned libraries:
c@219 494
c@219 495 - CORRECT: We are on the right branch and are up-to-date with
c@219 496 it as far as we can tell. (If not using the network, this
c@219 497 should be reported to user as "Present" rather than "Correct"
c@219 498 as the remote repo may have advanced without us knowing.)
c@219 499
c@219 500 - SUPERSEDED: We are on the right branch but we can see that
c@219 501 there is a newer revision either locally or on the remote (in
c@219 502 Git terms, we are at an ancestor of the desired branch tip).
c@219 503
c@219 504 - WRONG: We are on the wrong branch (in Git terms, we are not
c@219 505 at the desired branch tip or any ancestor of it).
c@219 506
c@219 507 - ABSENT: Repo doesn't exist here at all.
c@219 508
c@219 509 Valid states for pinned libraries:
c@219 510
c@219 511 - CORRECT: We are at the pinned revision.
c@219 512
c@219 513 - WRONG: We are at any revision other than the pinned one.
c@219 514
c@219 515 - ABSENT: Repo doesn't exist here at all.
c@219 516 *)
c@219 517
c@219 518 fun check with_network context
c@219 519 ({ libname, source, branch,
c@219 520 project_pin, lock_pin, ... } : libspec) =
c@219 521 let fun check_unpinned () =
c@219 522 let val newest =
c@219 523 if with_network
c@219 524 then V.is_newest context (libname, source, branch)
c@219 525 else V.is_newest_locally context (libname, branch)
c@219 526 in
c@219 527 case newest of
c@219 528 ERROR e => ERROR e
c@219 529 | OK true => OK CORRECT
c@219 530 | OK false =>
c@219 531 case V.is_on_branch context (libname, branch) of
c@219 532 ERROR e => ERROR e
c@219 533 | OK true => OK SUPERSEDED
c@219 534 | OK false => OK WRONG
c@219 535 end
c@219 536 fun check_pinned target =
c@219 537 case V.is_at context (libname, target) of
c@219 538 ERROR e => ERROR e
c@219 539 | OK true => OK CORRECT
c@219 540 | OK false => OK WRONG
c@219 541 fun check_remote () =
c@219 542 case project_pin of
c@219 543 UNPINNED => check_unpinned ()
c@219 544 | PINNED target => check_pinned target
c@219 545 fun check_local () =
c@219 546 case V.is_modified_locally context libname of
c@219 547 ERROR e => ERROR e
c@219 548 | OK true => OK MODIFIED
c@219 549 | OK false =>
c@219 550 case lock_pin of
c@219 551 UNPINNED => OK CLEAN
c@219 552 | PINNED target =>
c@219 553 case V.is_at context (libname, target) of
c@219 554 ERROR e => ERROR e
c@219 555 | OK true => OK CLEAN
c@219 556 | OK false => OK LOCK_MISMATCHED
c@219 557 in
c@219 558 case V.exists context libname of
c@219 559 ERROR e => ERROR e
c@219 560 | OK false => OK (ABSENT, CLEAN)
c@219 561 | OK true =>
c@219 562 case (check_remote (), check_local ()) of
c@219 563 (ERROR e, _) => ERROR e
c@219 564 | (_, ERROR e) => ERROR e
c@219 565 | (OK r, OK l) => OK (r, l)
c@219 566 end
c@219 567
c@219 568 val review = check true
c@219 569 val status = check false
c@219 570
c@219 571 fun update context
c@219 572 ({ libname, source, branch,
c@219 573 project_pin, lock_pin, ... } : libspec) =
c@219 574 let fun update_unpinned () =
c@219 575 case V.is_newest context (libname, source, branch) of
c@219 576 ERROR e => ERROR e
c@219 577 | OK true => OK ()
c@219 578 | OK false => V.update context (libname, source, branch)
c@219 579 fun update_pinned target =
c@219 580 case V.is_at context (libname, target) of
c@219 581 ERROR e => ERROR e
c@219 582 | OK true => OK ()
c@219 583 | OK false => V.update_to context (libname, source, target)
c@219 584 fun update' () =
c@219 585 case lock_pin of
c@219 586 PINNED target => update_pinned target
c@219 587 | UNPINNED =>
c@219 588 case project_pin of
c@219 589 PINNED target => update_pinned target
c@219 590 | UNPINNED => update_unpinned ()
c@219 591 in
c@219 592 case V.exists context libname of
c@219 593 ERROR e => ERROR e
c@219 594 | OK true => update' ()
c@219 595 | OK false =>
c@219 596 case V.checkout context (libname, source, branch) of
c@219 597 ERROR e => ERROR e
c@219 598 | OK () => update' ()
c@219 599 end
c@219 600
c@219 601 fun id_of context ({ libname, ... } : libspec) =
c@219 602 V.id_of context libname
c@219 603
c@219 604 fun is_working context vcs =
c@219 605 V.is_working context
c@219 606
c@219 607 end
c@219 608
c@219 609 (* Simple Standard ML JSON parser
c@219 610 https://bitbucket.org/cannam/sml-simplejson
c@219 611 Copyright 2017 Chris Cannam. BSD licence.
c@219 612 Parts based on the JSON parser in the Ponyo library by Phil Eaton.
c@219 613 *)
c@219 614
c@219 615 signature JSON = sig
c@219 616
c@219 617 datatype json = OBJECT of (string * json) list
c@219 618 | ARRAY of json list
c@219 619 | NUMBER of real
c@219 620 | STRING of string
c@219 621 | BOOL of bool
c@219 622 | NULL
c@219 623
c@219 624 datatype 'a result = OK of 'a
c@219 625 | ERROR of string
c@219 626
c@219 627 val parse : string -> json result
c@219 628 val serialise : json -> string
c@219 629 val serialiseIndented : json -> string
c@219 630
c@219 631 end
c@219 632
c@219 633 structure Json :> JSON = struct
c@219 634
c@219 635 datatype json = OBJECT of (string * json) list
c@219 636 | ARRAY of json list
c@219 637 | NUMBER of real
c@219 638 | STRING of string
c@219 639 | BOOL of bool
c@219 640 | NULL
c@219 641
c@219 642 datatype 'a result = OK of 'a
c@219 643 | ERROR of string
c@219 644
c@219 645 structure T = struct
c@219 646 datatype token = NUMBER of char list
c@219 647 | STRING of string
c@219 648 | BOOL of bool
c@219 649 | NULL
c@219 650 | CURLY_L
c@219 651 | CURLY_R
c@219 652 | SQUARE_L
c@219 653 | SQUARE_R
c@219 654 | COLON
c@219 655 | COMMA
c@219 656
c@219 657 fun toString t =
c@219 658 case t of NUMBER digits => implode digits
c@219 659 | STRING s => s
c@219 660 | BOOL b => Bool.toString b
c@219 661 | NULL => "null"
c@219 662 | CURLY_L => "{"
c@219 663 | CURLY_R => "}"
c@219 664 | SQUARE_L => "["
c@219 665 | SQUARE_R => "]"
c@219 666 | COLON => ":"
c@219 667 | COMMA => ","
c@219 668 end
c@219 669
c@219 670 fun bmpToUtf8 cp = (* convert a codepoint in Unicode BMP to utf8 bytes *)
c@219 671 let open Word
c@219 672 infix 6 orb andb >>
c@219 673 in
c@219 674 map (Char.chr o toInt)
c@219 675 (if cp < 0wx80 then
c@219 676 [cp]
c@219 677 else if cp < 0wx800 then
c@219 678 [0wxc0 orb (cp >> 0w6), 0wx80 orb (cp andb 0wx3f)]
c@219 679 else if cp < 0wx10000 then
c@219 680 [0wxe0 orb (cp >> 0w12),
c@219 681 0wx80 orb ((cp >> 0w6) andb 0wx3f),
c@219 682 0wx80 orb (cp andb 0wx3f)]
c@219 683 else raise Fail ("Invalid BMP point " ^ (Word.toString cp)))
c@219 684 end
c@219 685
c@219 686 fun error pos text = ERROR (text ^ " at character position " ^
c@219 687 Int.toString (pos - 1))
c@219 688 fun token_error pos = error pos ("Unexpected token")
c@219 689
c@219 690 fun lexNull pos acc (#"u" :: #"l" :: #"l" :: xs) =
c@219 691 lex (pos + 3) (T.NULL :: acc) xs
c@219 692 | lexNull pos acc _ = token_error pos
c@219 693
c@219 694 and lexTrue pos acc (#"r" :: #"u" :: #"e" :: xs) =
c@219 695 lex (pos + 3) (T.BOOL true :: acc) xs
c@219 696 | lexTrue pos acc _ = token_error pos
c@219 697
c@219 698 and lexFalse pos acc (#"a" :: #"l" :: #"s" :: #"e" :: xs) =
c@219 699 lex (pos + 4) (T.BOOL false :: acc) xs
c@219 700 | lexFalse pos acc _ = token_error pos
c@219 701
c@219 702 and lexChar tok pos acc xs =
c@219 703 lex pos (tok :: acc) xs
c@219 704
c@219 705 and lexString pos acc cc =
c@219 706 let datatype escaped = ESCAPED | NORMAL
c@219 707 fun lexString' pos text ESCAPED [] =
c@219 708 error pos "End of input during escape sequence"
c@219 709 | lexString' pos text NORMAL [] =
c@219 710 error pos "End of input during string"
c@219 711 | lexString' pos text ESCAPED (x :: xs) =
c@219 712 let fun esc c = lexString' (pos + 1) (c :: text) NORMAL xs
c@219 713 in case x of
c@219 714 #"\"" => esc x
c@219 715 | #"\\" => esc x
c@219 716 | #"/" => esc x
c@219 717 | #"b" => esc #"\b"
c@219 718 | #"f" => esc #"\f"
c@219 719 | #"n" => esc #"\n"
c@219 720 | #"r" => esc #"\r"
c@219 721 | #"t" => esc #"\t"
c@219 722 | _ => error pos ("Invalid escape \\" ^
c@219 723 Char.toString x)
c@219 724 end
c@219 725 | lexString' pos text NORMAL (#"\\" :: #"u" ::a::b::c::d:: xs) =
c@219 726 if List.all Char.isHexDigit [a,b,c,d]
c@219 727 then case Word.fromString ("0wx" ^ (implode [a,b,c,d])) of
c@219 728 SOME w => (let val utf = rev (bmpToUtf8 w) in
c@219 729 lexString' (pos + 6) (utf @ text)
c@219 730 NORMAL xs
c@219 731 end
c@219 732 handle Fail err => error pos err)
c@219 733 | NONE => error pos "Invalid Unicode BMP escape sequence"
c@219 734 else error pos "Invalid Unicode BMP escape sequence"
c@219 735 | lexString' pos text NORMAL (x :: xs) =
c@219 736 if Char.ord x < 0x20
c@219 737 then error pos "Invalid unescaped control character"
c@219 738 else
c@219 739 case x of
c@219 740 #"\"" => OK (rev text, xs, pos + 1)
c@219 741 | #"\\" => lexString' (pos + 1) text ESCAPED xs
c@219 742 | _ => lexString' (pos + 1) (x :: text) NORMAL xs
c@219 743 in
c@219 744 case lexString' pos [] NORMAL cc of
c@219 745 OK (text, rest, newpos) =>
c@219 746 lex newpos (T.STRING (implode text) :: acc) rest
c@219 747 | ERROR e => ERROR e
c@219 748 end
c@219 749
c@219 750 and lexNumber firstChar pos acc cc =
c@219 751 let val valid = explode ".+-e"
c@219 752 fun lexNumber' pos digits [] = (rev digits, [], pos)
c@219 753 | lexNumber' pos digits (x :: xs) =
c@219 754 if x = #"E" then lexNumber' (pos + 1) (#"e" :: digits) xs
c@219 755 else if Char.isDigit x orelse List.exists (fn c => x = c) valid
c@219 756 then lexNumber' (pos + 1) (x :: digits) xs
c@219 757 else (rev digits, x :: xs, pos)
c@219 758 val (digits, rest, newpos) =
c@219 759 lexNumber' (pos - 1) [] (firstChar :: cc)
c@219 760 in
c@219 761 case digits of
c@219 762 [] => token_error pos
c@219 763 | _ => lex newpos (T.NUMBER digits :: acc) rest
c@219 764 end
c@219 765
c@219 766 and lex pos acc [] = OK (rev acc)
c@219 767 | lex pos acc (x::xs) =
c@219 768 (case x of
c@219 769 #" " => lex
c@219 770 | #"\t" => lex
c@219 771 | #"\n" => lex
c@219 772 | #"\r" => lex
c@219 773 | #"{" => lexChar T.CURLY_L
c@219 774 | #"}" => lexChar T.CURLY_R
c@219 775 | #"[" => lexChar T.SQUARE_L
c@219 776 | #"]" => lexChar T.SQUARE_R
c@219 777 | #":" => lexChar T.COLON
c@219 778 | #"," => lexChar T.COMMA
c@219 779 | #"\"" => lexString
c@219 780 | #"t" => lexTrue
c@219 781 | #"f" => lexFalse
c@219 782 | #"n" => lexNull
c@219 783 | x => lexNumber x) (pos + 1) acc xs
c@219 784
c@219 785 fun show [] = "end of input"
c@219 786 | show (tok :: _) = T.toString tok
c@219 787
c@219 788 fun parseNumber digits =
c@219 789 (* Note lexNumber already case-insensitised the E for us *)
c@219 790 let open Char
c@219 791
c@219 792 fun okExpDigits [] = false
c@219 793 | okExpDigits (c :: []) = isDigit c
c@219 794 | okExpDigits (c :: cs) = isDigit c andalso okExpDigits cs
c@219 795
c@219 796 fun okExponent [] = false
c@219 797 | okExponent (#"+" :: cs) = okExpDigits cs
c@219 798 | okExponent (#"-" :: cs) = okExpDigits cs
c@219 799 | okExponent cc = okExpDigits cc
c@219 800
c@219 801 fun okFracTrailing [] = true
c@219 802 | okFracTrailing (c :: cs) =
c@219 803 (isDigit c andalso okFracTrailing cs) orelse
c@219 804 (c = #"e" andalso okExponent cs)
c@219 805
c@219 806 fun okFraction [] = false
c@219 807 | okFraction (c :: cs) =
c@219 808 isDigit c andalso okFracTrailing cs
c@219 809
c@219 810 fun okPosTrailing [] = true
c@219 811 | okPosTrailing (#"." :: cs) = okFraction cs
c@219 812 | okPosTrailing (#"e" :: cs) = okExponent cs
c@219 813 | okPosTrailing (c :: cs) =
c@219 814 isDigit c andalso okPosTrailing cs
c@219 815
c@219 816 fun okPositive [] = false
c@219 817 | okPositive (#"0" :: []) = true
c@219 818 | okPositive (#"0" :: #"." :: cs) = okFraction cs
c@219 819 | okPositive (#"0" :: #"e" :: cs) = okExponent cs
c@219 820 | okPositive (#"0" :: cs) = false
c@219 821 | okPositive (c :: cs) = isDigit c andalso okPosTrailing cs
c@219 822
c@219 823 fun okNumber (#"-" :: cs) = okPositive cs
c@219 824 | okNumber cc = okPositive cc
c@219 825 in
c@219 826 if okNumber digits
c@219 827 then case Real.fromString (implode digits) of
c@219 828 NONE => ERROR "Number out of range"
c@219 829 | SOME r => OK r
c@219 830 else ERROR ("Invalid number \"" ^ (implode digits) ^ "\"")
c@219 831 end
c@219 832
c@219 833 fun parseObject (T.CURLY_R :: xs) = OK (OBJECT [], xs)
c@219 834 | parseObject tokens =
c@219 835 let fun parsePair (T.STRING key :: T.COLON :: xs) =
c@219 836 (case parseTokens xs of
c@219 837 ERROR e => ERROR e
c@219 838 | OK (j, xs) => OK ((key, j), xs))
c@219 839 | parsePair other =
c@219 840 ERROR ("Object key/value pair expected around \"" ^
c@219 841 show other ^ "\"")
c@219 842 fun parseObject' acc [] = ERROR "End of input during object"
c@219 843 | parseObject' acc tokens =
c@219 844 case parsePair tokens of
c@219 845 ERROR e => ERROR e
c@219 846 | OK (pair, T.COMMA :: xs) =>
c@219 847 parseObject' (pair :: acc) xs
c@219 848 | OK (pair, T.CURLY_R :: xs) =>
c@219 849 OK (OBJECT (rev (pair :: acc)), xs)
c@219 850 | OK (_, _) => ERROR "Expected , or } after object element"
c@219 851 in
c@219 852 parseObject' [] tokens
c@219 853 end
c@219 854
c@219 855 and parseArray (T.SQUARE_R :: xs) = OK (ARRAY [], xs)
c@219 856 | parseArray tokens =
c@219 857 let fun parseArray' acc [] = ERROR "End of input during array"
c@219 858 | parseArray' acc tokens =
c@219 859 case parseTokens tokens of
c@219 860 ERROR e => ERROR e
c@219 861 | OK (j, T.COMMA :: xs) => parseArray' (j :: acc) xs
c@219 862 | OK (j, T.SQUARE_R :: xs) => OK (ARRAY (rev (j :: acc)), xs)
c@219 863 | OK (_, _) => ERROR "Expected , or ] after array element"
c@219 864 in
c@219 865 parseArray' [] tokens
c@219 866 end
c@219 867
c@219 868 and parseTokens [] = ERROR "Value expected"
c@219 869 | parseTokens (tok :: xs) =
c@219 870 (case tok of
c@219 871 T.NUMBER d => (case parseNumber d of
c@219 872 OK r => OK (NUMBER r, xs)
c@219 873 | ERROR e => ERROR e)
c@219 874 | T.STRING s => OK (STRING s, xs)
c@219 875 | T.BOOL b => OK (BOOL b, xs)
c@219 876 | T.NULL => OK (NULL, xs)
c@219 877 | T.CURLY_L => parseObject xs
c@219 878 | T.SQUARE_L => parseArray xs
c@219 879 | _ => ERROR ("Unexpected token " ^ T.toString tok ^
c@219 880 " before " ^ show xs))
c@219 881
c@219 882 fun parse str =
c@219 883 case lex 1 [] (explode str) of
c@219 884 ERROR e => ERROR e
c@219 885 | OK tokens => case parseTokens tokens of
c@219 886 OK (value, []) => OK value
c@219 887 | OK (_, _) => ERROR "Extra data after input"
c@219 888 | ERROR e => ERROR e
c@219 889
c@219 890 fun stringEscape s =
c@219 891 let fun esc x = [x, #"\\"]
c@219 892 fun escape' acc [] = rev acc
c@219 893 | escape' acc (x :: xs) =
c@219 894 escape' (case x of
c@219 895 #"\"" => esc x @ acc
c@219 896 | #"\\" => esc x @ acc
c@219 897 | #"\b" => esc #"b" @ acc
c@219 898 | #"\f" => esc #"f" @ acc
c@219 899 | #"\n" => esc #"n" @ acc
c@219 900 | #"\r" => esc #"r" @ acc
c@219 901 | #"\t" => esc #"t" @ acc
c@219 902 | _ =>
c@219 903 let val c = Char.ord x
c@219 904 in
c@219 905 if c < 0x20
c@219 906 then let val hex = Word.toString (Word.fromInt c)
c@219 907 in (rev o explode) (if c < 0x10
c@219 908 then ("\\u000" ^ hex)
c@219 909 else ("\\u00" ^ hex))
c@219 910 end @ acc
c@219 911 else
c@219 912 x :: acc
c@219 913 end)
c@219 914 xs
c@219 915 in
c@219 916 implode (escape' [] (explode s))
c@219 917 end
c@219 918
c@219 919 fun serialise json =
c@219 920 case json of
c@219 921 OBJECT pp => "{" ^ String.concatWith
c@219 922 "," (map (fn (key, value) =>
c@219 923 serialise (STRING key) ^ ":" ^
c@219 924 serialise value) pp) ^
c@219 925 "}"
c@219 926 | ARRAY arr => "[" ^ String.concatWith "," (map serialise arr) ^ "]"
c@219 927 | NUMBER n => implode (map (fn #"~" => #"-" | c => c)
c@219 928 (explode (Real.toString n)))
c@219 929 | STRING s => "\"" ^ stringEscape s ^ "\""
c@219 930 | BOOL b => Bool.toString b
c@219 931 | NULL => "null"
c@219 932
c@219 933 fun serialiseIndented json =
c@219 934 let fun indent 0 = ""
c@219 935 | indent i = " " ^ indent (i - 1)
c@219 936 fun serialiseIndented' i json =
c@219 937 let val ser = serialiseIndented' (i + 1)
c@219 938 in
c@219 939 case json of
c@219 940 OBJECT [] => "{}"
c@219 941 | ARRAY [] => "[]"
c@219 942 | OBJECT pp => "{\n" ^ indent (i + 1) ^
c@219 943 String.concatWith
c@219 944 (",\n" ^ indent (i + 1))
c@219 945 (map (fn (key, value) =>
c@219 946 ser (STRING key) ^ ": " ^
c@219 947 ser value) pp) ^
c@219 948 "\n" ^ indent i ^ "}"
c@219 949 | ARRAY arr => "[\n" ^ indent (i + 1) ^
c@219 950 String.concatWith
c@219 951 (",\n" ^ indent (i + 1))
c@219 952 (map ser arr) ^
c@219 953 "\n" ^ indent i ^ "]"
c@219 954 | other => serialise other
c@219 955 end
c@219 956 in
c@219 957 serialiseIndented' 0 json ^ "\n"
c@219 958 end
c@219 959
c@219 960 end
c@219 961
c@219 962
c@219 963 structure JsonBits :> sig
c@219 964 exception Config of string
c@219 965 val load_json_from : string -> Json.json (* filename -> json *)
c@219 966 val save_json_to : string -> Json.json -> unit
c@219 967 val lookup_optional : Json.json -> string list -> Json.json option
c@219 968 val lookup_optional_string : Json.json -> string list -> string option
c@219 969 val lookup_mandatory : Json.json -> string list -> Json.json
c@219 970 val lookup_mandatory_string : Json.json -> string list -> string
c@219 971 end = struct
c@219 972
c@219 973 exception Config of string
c@219 974
c@219 975 fun load_json_from filename =
c@219 976 case Json.parse (FileBits.file_contents filename) of
c@219 977 Json.OK json => json
c@219 978 | Json.ERROR e => raise Config ("Failed to parse file: " ^ e)
c@219 979
c@219 980 fun save_json_to filename json =
c@219 981 (* using binary I/O to avoid ever writing CR/LF line endings *)
c@219 982 let val jstr = Json.serialiseIndented json
c@219 983 val stream = BinIO.openOut filename
c@219 984 in
c@219 985 BinIO.output (stream, Byte.stringToBytes jstr);
c@219 986 BinIO.closeOut stream
c@219 987 end
c@219 988
c@219 989 fun lookup_optional json kk =
c@219 990 let fun lookup key =
c@219 991 case json of
c@219 992 Json.OBJECT kvs =>
c@219 993 (case List.filter (fn (k, v) => k = key) kvs of
c@219 994 [] => NONE
c@219 995 | [(_,v)] => SOME v
c@219 996 | _ => raise Config ("Duplicate key: " ^
c@219 997 (String.concatWith " -> " kk)))
c@219 998 | _ => raise Config "Object expected"
c@219 999 in
c@219 1000 case kk of
c@219 1001 [] => NONE
c@219 1002 | key::[] => lookup key
c@219 1003 | key::kk => case lookup key of
c@219 1004 NONE => NONE
c@219 1005 | SOME j => lookup_optional j kk
c@219 1006 end
c@219 1007
c@219 1008 fun lookup_optional_string json kk =
c@219 1009 case lookup_optional json kk of
c@219 1010 SOME (Json.STRING s) => SOME s
c@219 1011 | SOME _ => raise Config ("Value (if present) must be string: " ^
c@219 1012 (String.concatWith " -> " kk))
c@219 1013 | NONE => NONE
c@219 1014
c@219 1015 fun lookup_mandatory json kk =
c@219 1016 case lookup_optional json kk of
c@219 1017 SOME v => v
c@219 1018 | NONE => raise Config ("Value is mandatory: " ^
c@219 1019 (String.concatWith " -> " kk))
c@219 1020
c@219 1021 fun lookup_mandatory_string json kk =
c@219 1022 case lookup_optional json kk of
c@219 1023 SOME (Json.STRING s) => s
c@219 1024 | _ => raise Config ("Value must be string: " ^
c@219 1025 (String.concatWith " -> " kk))
c@219 1026 end
c@219 1027
c@219 1028 structure Provider :> sig
c@219 1029 val load_providers : Json.json -> provider list
c@219 1030 val load_more_providers : provider list -> Json.json -> provider list
c@219 1031 val remote_url : context -> vcs -> source -> libname -> string
c@219 1032 end = struct
c@219 1033
c@219 1034 val known_providers : provider list =
c@219 1035 [ {
c@219 1036 service = "bitbucket",
c@219 1037 supports = [HG, GIT],
c@219 1038 remote_spec = {
c@219 1039 anon = SOME "https://bitbucket.org/{owner}/{repository}",
c@219 1040 auth = SOME "ssh://{vcs}@bitbucket.org/{owner}/{repository}"
c@219 1041 }
c@219 1042 },
c@219 1043 {
c@219 1044 service = "github",
c@219 1045 supports = [GIT],
c@219 1046 remote_spec = {
c@219 1047 anon = SOME "https://github.com/{owner}/{repository}",
c@219 1048 auth = SOME "ssh://{vcs}@github.com/{owner}/{repository}"
c@219 1049 }
cannam@258 1050 },
cannam@258 1051 { service = "sourcehut",
cannam@258 1052 supports = [HG, GIT],
cannam@258 1053 remote_spec = {
cannam@258 1054 anon = SOME "https://{vcs}.sr.ht/%7E{owner}/{repository}",
cannam@258 1055 auth = SOME "ssh://{vcs}@{vcs}.sr.ht/%7E{owner}/{repository}"
cannam@258 1056 }
c@219 1057 }
c@219 1058 ]
c@219 1059
c@219 1060 fun vcs_name vcs =
c@219 1061 case vcs of HG => "hg"
c@219 1062 | GIT => "git"
c@219 1063 | SVN => "svn"
c@219 1064
c@219 1065 fun vcs_from_name name =
c@219 1066 case name of "hg" => HG
c@219 1067 | "git" => GIT
c@219 1068 | "svn" => SVN
c@219 1069 | other => raise Fail ("Unknown vcs name \"" ^ name ^ "\"")
c@219 1070
c@219 1071 fun load_more_providers previously_loaded json =
c@219 1072 let open JsonBits
c@219 1073 fun load pjson pname : provider =
c@219 1074 {
c@219 1075 service = pname,
c@219 1076 supports =
c@219 1077 case lookup_mandatory pjson ["vcs"] of
c@219 1078 Json.ARRAY vv =>
c@219 1079 map (fn (Json.STRING v) => vcs_from_name v
c@219 1080 | _ => raise Fail "Strings expected in vcs array")
c@219 1081 vv
c@219 1082 | _ => raise Fail "Array expected for vcs",
c@219 1083 remote_spec = {
c@219 1084 anon = lookup_optional_string pjson ["anonymous"],
c@219 1085 auth = lookup_optional_string pjson ["authenticated"]
c@219 1086 }
c@219 1087 }
c@219 1088 val loaded =
c@219 1089 case lookup_optional json ["services"] of
c@219 1090 NONE => []
c@219 1091 | SOME (Json.OBJECT pl) => map (fn (k, v) => load v k) pl
c@219 1092 | _ => raise Fail "Object expected for services in config"
c@219 1093 val newly_loaded =
c@219 1094 List.filter (fn p => not (List.exists (fn pp => #service p =
c@219 1095 #service pp)
c@219 1096 previously_loaded))
c@219 1097 loaded
c@219 1098 in
c@219 1099 previously_loaded @ newly_loaded
c@219 1100 end
c@219 1101
c@219 1102 fun load_providers json =
c@219 1103 load_more_providers known_providers json
c@219 1104
c@219 1105 fun expand_spec spec { vcs, service, owner, repo } login =
c@219 1106 (* ugly *)
c@219 1107 let fun replace str =
c@219 1108 case str of
c@219 1109 "vcs" => vcs_name vcs
c@219 1110 | "service" => service
c@219 1111 | "owner" =>
c@219 1112 (case owner of
c@219 1113 SOME ostr => ostr
c@219 1114 | NONE => raise Fail ("Owner not specified for service " ^
c@219 1115 service))
c@219 1116 | "repository" => repo
c@219 1117 | "account" =>
c@219 1118 (case login of
c@219 1119 SOME acc => acc
c@219 1120 | NONE => raise Fail ("Account not given for service " ^
c@219 1121 service))
c@219 1122 | other => raise Fail ("Unknown variable \"" ^ other ^
c@219 1123 "\" in spec for service " ^ service)
c@219 1124 fun expand' acc sstr =
c@219 1125 case Substring.splitl (fn c => c <> #"{") sstr of
c@219 1126 (pfx, sfx) =>
c@219 1127 if Substring.isEmpty sfx
c@219 1128 then rev (pfx :: acc)
c@219 1129 else
c@219 1130 case Substring.splitl (fn c => c <> #"}") sfx of
c@219 1131 (tok, remainder) =>
c@219 1132 if Substring.isEmpty remainder
c@219 1133 then rev (tok :: pfx :: acc)
c@219 1134 else let val replacement =
c@219 1135 replace
c@219 1136 (* tok begins with "{": *)
c@219 1137 (Substring.string
c@219 1138 (Substring.triml 1 tok))
c@219 1139 in
c@219 1140 expand' (Substring.full replacement ::
c@219 1141 pfx :: acc)
c@219 1142 (* remainder begins with "}": *)
c@219 1143 (Substring.triml 1 remainder)
c@219 1144 end
c@219 1145 in
c@219 1146 Substring.concat (expand' [] (Substring.full spec))
c@219 1147 end
c@219 1148
c@219 1149 fun provider_url req login providers =
c@219 1150 case providers of
c@219 1151 [] => raise Fail ("Unknown service \"" ^ (#service req) ^
c@219 1152 "\" for vcs \"" ^ (vcs_name (#vcs req)) ^ "\"")
c@219 1153 | ({ service, supports, remote_spec : remote_spec } :: rest) =>
c@219 1154 if service <> (#service req) orelse
c@219 1155 not (List.exists (fn v => v = (#vcs req)) supports)
c@219 1156 then provider_url req login rest
c@219 1157 else
c@219 1158 case (login, #auth remote_spec, #anon remote_spec) of
c@219 1159 (SOME _, SOME auth, _) => expand_spec auth req login
c@219 1160 | (SOME _, _, SOME anon) => expand_spec anon req NONE
c@219 1161 | (NONE, _, SOME anon) => expand_spec anon req NONE
c@219 1162 | _ => raise Fail ("No suitable anonymous or authenticated " ^
c@219 1163 "URL spec provided for service \"" ^
c@219 1164 service ^ "\"")
c@219 1165
c@219 1166 fun login_for ({ accounts, ... } : context) service =
c@219 1167 case List.find (fn a => service = #service a) accounts of
c@219 1168 SOME { login, ... } => SOME login
c@219 1169 | NONE => NONE
c@219 1170
c@219 1171 fun reponame_for path =
c@219 1172 case String.tokens (fn c => c = #"/") path of
c@219 1173 [] => raise Fail "Non-empty library path required"
c@219 1174 | toks => hd (rev toks)
c@219 1175
c@219 1176 fun remote_url (context : context) vcs source libname =
c@219 1177 case source of
c@219 1178 URL_SOURCE u => u
c@219 1179 | SERVICE_SOURCE { service, owner, repo } =>
c@219 1180 provider_url { vcs = vcs,
c@219 1181 service = service,
c@219 1182 owner = owner,
c@219 1183 repo = case repo of
c@219 1184 SOME r => r
c@219 1185 | NONE => reponame_for libname }
c@219 1186 (login_for context service)
c@219 1187 (#providers context)
c@219 1188 end
c@219 1189
c@219 1190 structure HgControl :> VCS_CONTROL = struct
c@219 1191
c@219 1192 (* Pulls always use an explicit URL, never just the default
c@219 1193 remote, in order to ensure we update properly if the location
c@219 1194 given in the project file changes. *)
c@219 1195
c@219 1196 type vcsstate = { id: string, modified: bool,
c@219 1197 branch: string, tags: string list }
c@219 1198
c@219 1199 val hg_program = "hg"
c@219 1200
c@219 1201 val hg_args = [ "--config", "ui.interactive=true",
c@219 1202 "--config", "ui.merge=:merge" ]
c@219 1203
c@219 1204 fun hg_command context libname args =
c@219 1205 FileBits.command context libname (hg_program :: hg_args @ args)
c@219 1206
c@219 1207 fun hg_command_output context libname args =
c@219 1208 FileBits.command_output context libname (hg_program :: hg_args @ args)
c@219 1209
c@219 1210 fun is_working context =
c@219 1211 case hg_command_output context "" ["--version"] of
c@219 1212 OK "" => OK false
c@219 1213 | OK _ => OK true
c@219 1214 | ERROR e => ERROR e
c@219 1215
c@219 1216 fun exists context libname =
c@219 1217 OK (OS.FileSys.isDir (FileBits.subpath context libname ".hg"))
c@219 1218 handle _ => OK false
c@219 1219
c@219 1220 fun remote_for context (libname, source) =
c@219 1221 Provider.remote_url context HG source libname
c@219 1222
c@219 1223 fun current_state context libname : vcsstate result =
c@219 1224 let fun is_branch text = text <> "" andalso #"(" = hd (explode text)
c@219 1225 and extract_branch b =
c@219 1226 if is_branch b (* need to remove enclosing parens *)
c@219 1227 then (implode o rev o tl o rev o tl o explode) b
c@219 1228 else "default"
c@219 1229 and is_modified id = id <> "" andalso #"+" = hd (rev (explode id))
c@219 1230 and extract_id id =
c@219 1231 if is_modified id (* need to remove trailing "+" *)
c@219 1232 then (implode o rev o tl o rev o explode) id
c@219 1233 else id
c@219 1234 and split_tags tags = String.tokens (fn c => c = #"/") tags
c@219 1235 and state_for (id, branch, tags) =
c@219 1236 OK { id = extract_id id,
c@219 1237 modified = is_modified id,
c@219 1238 branch = extract_branch branch,
c@219 1239 tags = split_tags tags }
c@219 1240 in
c@219 1241 case hg_command_output context libname ["id"] of
c@219 1242 ERROR e => ERROR e
c@219 1243 | OK out =>
c@219 1244 case String.tokens (fn x => x = #" ") out of
c@219 1245 [id, branch, tags] => state_for (id, branch, tags)
c@219 1246 | [id, other] => if is_branch other
c@219 1247 then state_for (id, other, "")
c@219 1248 else state_for (id, "", other)
c@219 1249 | [id] => state_for (id, "", "")
c@219 1250 | _ => ERROR ("Unexpected output from hg id: " ^ out)
c@219 1251 end
c@219 1252
c@219 1253 fun branch_name branch = case branch of
c@219 1254 DEFAULT_BRANCH => "default"
c@219 1255 | BRANCH "" => "default"
c@219 1256 | BRANCH b => b
c@219 1257
c@219 1258 fun id_of context libname =
c@219 1259 case current_state context libname of
c@219 1260 ERROR e => ERROR e
c@219 1261 | OK { id, ... } => OK id
c@219 1262
c@219 1263 fun is_at context (libname, id_or_tag) =
c@219 1264 case current_state context libname of
c@219 1265 ERROR e => ERROR e
c@219 1266 | OK { id, tags, ... } =>
c@219 1267 OK (String.isPrefix id_or_tag id orelse
c@219 1268 String.isPrefix id id_or_tag orelse
c@219 1269 List.exists (fn t => t = id_or_tag) tags)
c@219 1270
c@219 1271 fun is_on_branch context (libname, b) =
c@219 1272 case current_state context libname of
c@219 1273 ERROR e => ERROR e
c@219 1274 | OK { branch, ... } => OK (branch = branch_name b)
c@219 1275
c@219 1276 fun is_newest_locally context (libname, branch) =
c@219 1277 case hg_command_output context libname
c@219 1278 ["log", "-l1",
c@219 1279 "-b", branch_name branch,
c@219 1280 "--template", "{node}"] of
c@219 1281 ERROR e => OK false (* desired branch does not exist *)
c@219 1282 | OK newest_in_repo => is_at context (libname, newest_in_repo)
c@219 1283
c@219 1284 fun pull context (libname, source) =
c@219 1285 let val url = remote_for context (libname, source)
c@219 1286 in
c@219 1287 hg_command context libname
c@219 1288 (if FileBits.verbose ()
c@219 1289 then ["pull", url]
c@219 1290 else ["pull", "-q", url])
c@219 1291 end
c@219 1292
c@219 1293 fun is_newest context (libname, source, branch) =
c@219 1294 case is_newest_locally context (libname, branch) of
c@219 1295 ERROR e => ERROR e
c@219 1296 | OK false => OK false
c@219 1297 | OK true =>
c@219 1298 case pull context (libname, source) of
c@219 1299 ERROR e => ERROR e
c@219 1300 | _ => is_newest_locally context (libname, branch)
c@219 1301
c@219 1302 fun is_modified_locally context libname =
c@219 1303 case current_state context libname of
c@219 1304 ERROR e => ERROR e
c@219 1305 | OK { modified, ... } => OK modified
c@219 1306
c@219 1307 fun checkout context (libname, source, branch) =
c@219 1308 let val url = remote_for context (libname, source)
c@219 1309 in
c@219 1310 (* make the lib dir rather than just the ext dir, since
c@219 1311 the lib dir might be nested and hg will happily check
c@219 1312 out into an existing empty dir anyway *)
c@219 1313 case FileBits.mkpath (FileBits.libpath context libname) of
c@219 1314 ERROR e => ERROR e
c@219 1315 | _ => hg_command context ""
c@219 1316 ["clone", "-u", branch_name branch,
c@219 1317 url, libname]
c@219 1318 end
c@219 1319
c@219 1320 fun update context (libname, source, branch) =
c@219 1321 let val pull_result = pull context (libname, source)
c@219 1322 in
c@219 1323 case hg_command context libname ["update", branch_name branch] of
c@219 1324 ERROR e => ERROR e
c@219 1325 | _ =>
c@219 1326 case pull_result of
c@219 1327 ERROR e => ERROR e
c@219 1328 | _ => OK ()
c@219 1329 end
c@219 1330
c@219 1331 fun update_to context (libname, _, "") =
c@219 1332 ERROR "Non-empty id (tag or revision id) required for update_to"
c@219 1333 | update_to context (libname, source, id) =
c@219 1334 let val pull_result = pull context (libname, source)
c@219 1335 in
c@219 1336 case hg_command context libname ["update", "-r", id] of
c@219 1337 OK _ => OK ()
c@219 1338 | ERROR e =>
c@219 1339 case pull_result of
c@219 1340 ERROR e' => ERROR e' (* this was the ur-error *)
c@219 1341 | _ => ERROR e
c@219 1342 end
c@219 1343
c@219 1344 fun copy_url_for context libname =
c@219 1345 OK (FileBits.file_url (FileBits.libpath context libname))
c@219 1346
c@219 1347 end
c@219 1348
c@219 1349 structure GitControl :> VCS_CONTROL = struct
c@219 1350
c@219 1351 (* With Git repos we always operate in detached HEAD state. Even
c@219 1352 the master branch is checked out using a remote reference
c@219 1353 (repoint/master). The remote we use is always named repoint, and we
c@219 1354 update it to the expected URL each time we fetch, in order to
c@219 1355 ensure we update properly if the location given in the project
c@219 1356 file changes. The origin remote is unused. *)
c@219 1357
c@219 1358 val git_program = "git"
c@219 1359
c@219 1360 fun git_command context libname args =
c@219 1361 FileBits.command context libname (git_program :: args)
c@219 1362
c@219 1363 fun git_command_output context libname args =
c@219 1364 FileBits.command_output context libname (git_program :: args)
c@219 1365
c@219 1366 fun is_working context =
c@219 1367 case git_command_output context "" ["--version"] of
c@219 1368 OK "" => OK false
c@219 1369 | OK _ => OK true
c@219 1370 | ERROR e => ERROR e
c@219 1371
c@219 1372 fun exists context libname =
c@219 1373 OK (OS.FileSys.isDir (FileBits.subpath context libname ".git"))
c@219 1374 handle _ => OK false
c@219 1375
c@219 1376 fun remote_for context (libname, source) =
c@219 1377 Provider.remote_url context GIT source libname
c@219 1378
c@219 1379 fun branch_name branch = case branch of
c@219 1380 DEFAULT_BRANCH => "master"
c@219 1381 | BRANCH "" => "master"
c@219 1382 | BRANCH b => b
c@219 1383
c@219 1384 val our_remote = "repoint"
c@219 1385
c@219 1386 fun remote_branch_name branch = our_remote ^ "/" ^ branch_name branch
c@219 1387
c@219 1388 fun checkout context (libname, source, branch) =
c@219 1389 let val url = remote_for context (libname, source)
c@219 1390 in
c@219 1391 (* make the lib dir rather than just the ext dir, since
c@219 1392 the lib dir might be nested and git will happily check
c@219 1393 out into an existing empty dir anyway *)
c@219 1394 case FileBits.mkpath (FileBits.libpath context libname) of
c@219 1395 OK () => git_command context ""
c@219 1396 ["clone", "--origin", our_remote,
c@219 1397 "--branch", branch_name branch,
c@219 1398 url, libname]
c@219 1399 | ERROR e => ERROR e
c@219 1400 end
c@219 1401
c@219 1402 fun add_our_remote context (libname, source) =
c@219 1403 (* When we do the checkout ourselves (above), we add the
c@219 1404 remote at the same time. But if the repo was cloned by
c@219 1405 someone else, we'll need to do it after the fact. Git
c@219 1406 doesn't seem to have a means to add a remote or change its
c@219 1407 url if it already exists; seems we have to do this: *)
c@219 1408 let val url = remote_for context (libname, source)
c@219 1409 in
c@219 1410 case git_command context libname
c@219 1411 ["remote", "set-url", our_remote, url] of
c@219 1412 OK () => OK ()
c@219 1413 | ERROR e => git_command context libname
c@219 1414 ["remote", "add", "-f", our_remote, url]
c@219 1415 end
c@219 1416
c@219 1417 (* NB git rev-parse HEAD shows revision id of current checkout;
c@219 1418 git rev-list -1 <tag> shows revision id of revision with that tag *)
c@219 1419
c@219 1420 fun id_of context libname =
c@219 1421 git_command_output context libname ["rev-parse", "HEAD"]
c@219 1422
c@219 1423 fun is_at context (libname, id_or_tag) =
c@219 1424 case id_of context libname of
c@219 1425 ERROR e => OK false (* HEAD nonexistent, expected in empty repo *)
c@219 1426 | OK id =>
c@219 1427 if String.isPrefix id_or_tag id orelse
c@219 1428 String.isPrefix id id_or_tag
c@219 1429 then OK true
c@219 1430 else is_at_tag context (libname, id, id_or_tag)
c@219 1431
c@219 1432 and is_at_tag context (libname, id, tag) =
c@219 1433 (* For annotated tags (with message) show-ref returns the tag
c@219 1434 object ref rather than that of the revision being tagged;
c@219 1435 we need the subsequent rev-list to chase that up. In fact
c@219 1436 the rev-list on its own is enough to get us the id direct
c@219 1437 from the tag name, but it fails with an error if the tag
c@219 1438 doesn't exist, whereas we want to handle that quietly in
c@219 1439 case the tag simply hasn't been pulled yet *)
c@219 1440 case git_command_output context libname
c@219 1441 ["show-ref", "refs/tags/" ^ tag, "--"] of
c@219 1442 OK "" => OK false (* Not a tag *)
c@219 1443 | ERROR _ => OK false
c@219 1444 | OK s =>
c@219 1445 let val tag_ref = hd (String.tokens (fn c => c = #" ") s)
c@219 1446 in
c@219 1447 case git_command_output context libname
c@219 1448 ["rev-list", "-1", tag_ref] of
c@219 1449 OK tagged => OK (id = tagged)
c@219 1450 | ERROR _ => OK false
c@219 1451 end
c@219 1452
c@219 1453 fun branch_tip context (libname, branch) =
c@219 1454 (* We don't have access to the source info or the network
c@219 1455 here, as this is used by status (e.g. via is_on_branch) as
c@219 1456 well as review. It's possible the remote branch won't exist,
c@219 1457 e.g. if the repo was checked out by something other than
c@219 1458 Repoint, and if that's the case, we can't add it here; we'll
c@219 1459 just have to fail, since checking against local branches
c@219 1460 instead could produce the wrong result. *)
c@219 1461 git_command_output context libname
c@219 1462 ["rev-list", "-1",
c@219 1463 remote_branch_name branch, "--"]
c@219 1464
c@219 1465 fun is_newest_locally context (libname, branch) =
c@219 1466 case branch_tip context (libname, branch) of
c@219 1467 ERROR e => OK false
c@219 1468 | OK rev => is_at context (libname, rev)
c@219 1469
c@219 1470 fun is_on_branch context (libname, branch) =
c@219 1471 case branch_tip context (libname, branch) of
c@219 1472 ERROR e => OK false
c@219 1473 | OK rev =>
c@219 1474 case is_at context (libname, rev) of
c@219 1475 ERROR e => ERROR e
c@219 1476 | OK true => OK true
c@219 1477 | OK false =>
c@219 1478 case git_command context libname
c@219 1479 ["merge-base", "--is-ancestor",
c@219 1480 "HEAD", remote_branch_name branch] of
c@219 1481 ERROR e => OK false (* cmd returns non-zero for no *)
c@219 1482 | _ => OK true
c@219 1483
c@219 1484 fun fetch context (libname, source) =
c@219 1485 case add_our_remote context (libname, source) of
c@219 1486 ERROR e => ERROR e
c@219 1487 | _ => git_command context libname ["fetch", our_remote]
c@219 1488
c@219 1489 fun is_newest context (libname, source, branch) =
c@219 1490 case add_our_remote context (libname, source) of
c@219 1491 ERROR e => ERROR e
c@219 1492 | OK () =>
c@219 1493 case is_newest_locally context (libname, branch) of
c@219 1494 ERROR e => ERROR e
c@219 1495 | OK false => OK false
c@219 1496 | OK true =>
c@219 1497 case fetch context (libname, source) of
c@219 1498 ERROR e => ERROR e
c@219 1499 | _ => is_newest_locally context (libname, branch)
c@219 1500
c@219 1501 fun is_modified_locally context libname =
c@219 1502 case git_command_output context libname ["status", "--porcelain"] of
c@219 1503 ERROR e => ERROR e
c@219 1504 | OK "" => OK false
c@219 1505 | OK _ => OK true
c@219 1506
c@219 1507 (* This function updates to the latest revision on a branch rather
c@219 1508 than to a specific id or tag. We can't just checkout the given
c@219 1509 branch, as that will succeed even if the branch isn't up to
c@219 1510 date. We could checkout the branch and then fetch and merge,
c@219 1511 but it's perhaps cleaner not to maintain a local branch at all,
c@219 1512 but instead checkout the remote branch as a detached head. *)
c@219 1513
c@219 1514 fun update context (libname, source, branch) =
c@219 1515 case fetch context (libname, source) of
c@219 1516 ERROR e => ERROR e
c@219 1517 | _ =>
c@219 1518 case git_command context libname ["checkout", "--detach",
c@219 1519 remote_branch_name branch] of
c@219 1520 ERROR e => ERROR e
c@219 1521 | _ => OK ()
c@219 1522
c@219 1523 (* This function is dealing with a specific id or tag, so if we
c@219 1524 can successfully check it out (detached) then that's all we
c@219 1525 need to do, regardless of whether fetch succeeded or not. We do
c@219 1526 attempt the fetch first, though, purely in order to avoid ugly
c@219 1527 error messages in the common case where we're being asked to
c@219 1528 update to a new pin (from the lock file) that hasn't been
c@219 1529 fetched yet. *)
c@219 1530
c@219 1531 fun update_to context (libname, _, "") =
c@219 1532 ERROR "Non-empty id (tag or revision id) required for update_to"
c@219 1533 | update_to context (libname, source, id) =
c@219 1534 let val fetch_result = fetch context (libname, source)
c@219 1535 in
c@219 1536 case git_command context libname ["checkout", "--detach", id] of
c@219 1537 OK _ => OK ()
c@219 1538 | ERROR e =>
c@219 1539 case fetch_result of
c@219 1540 ERROR e' => ERROR e' (* this was the ur-error *)
c@219 1541 | _ => ERROR e
c@219 1542 end
c@219 1543
c@219 1544 fun copy_url_for context libname =
c@219 1545 OK (FileBits.file_url (FileBits.libpath context libname))
c@219 1546
c@219 1547 end
c@219 1548
c@219 1549 (* SubXml - A parser for a subset of XML
c@219 1550 https://bitbucket.org/cannam/sml-subxml
c@219 1551 Copyright 2018 Chris Cannam. BSD licence.
c@219 1552 *)
c@219 1553
c@219 1554 signature SUBXML = sig
c@219 1555
c@219 1556 datatype node = ELEMENT of { name : string, children : node list }
c@219 1557 | ATTRIBUTE of { name : string, value : string }
c@219 1558 | TEXT of string
c@219 1559 | CDATA of string
c@219 1560 | COMMENT of string
c@219 1561
c@219 1562 datatype document = DOCUMENT of { name : string, children : node list }
c@219 1563
c@219 1564 datatype 'a result = OK of 'a
c@219 1565 | ERROR of string
c@219 1566
c@219 1567 val parse : string -> document result
c@219 1568 val serialise : document -> string
c@219 1569
c@219 1570 end
c@219 1571
c@219 1572 structure SubXml :> SUBXML = struct
c@219 1573
c@219 1574 datatype node = ELEMENT of { name : string, children : node list }
c@219 1575 | ATTRIBUTE of { name : string, value : string }
c@219 1576 | TEXT of string
c@219 1577 | CDATA of string
c@219 1578 | COMMENT of string
c@219 1579
c@219 1580 datatype document = DOCUMENT of { name : string, children : node list }
c@219 1581
c@219 1582 datatype 'a result = OK of 'a
c@219 1583 | ERROR of string
c@219 1584
c@219 1585 structure T = struct
c@219 1586 datatype token = ANGLE_L
c@219 1587 | ANGLE_R
c@219 1588 | ANGLE_SLASH_L
c@219 1589 | SLASH_ANGLE_R
c@219 1590 | EQUAL
c@219 1591 | NAME of string
c@219 1592 | TEXT of string
c@219 1593 | CDATA of string
c@219 1594 | COMMENT of string
c@219 1595
c@219 1596 fun name t =
c@219 1597 case t of ANGLE_L => "<"
c@219 1598 | ANGLE_R => ">"
c@219 1599 | ANGLE_SLASH_L => "</"
c@219 1600 | SLASH_ANGLE_R => "/>"
c@219 1601 | EQUAL => "="
c@219 1602 | NAME s => "name \"" ^ s ^ "\""
c@219 1603 | TEXT s => "text"
c@219 1604 | CDATA _ => "CDATA section"
c@219 1605 | COMMENT _ => "comment"
c@219 1606 end
c@219 1607
c@219 1608 structure Lex :> sig
c@219 1609 val lex : string -> T.token list result
c@219 1610 end = struct
c@219 1611
c@219 1612 fun error pos text =
c@219 1613 ERROR (text ^ " at character position " ^ Int.toString (pos-1))
c@219 1614 fun tokenError pos token =
c@219 1615 error pos ("Unexpected token '" ^ Char.toString token ^ "'")
c@219 1616
c@219 1617 val nameEnd = explode " \t\n\r\"'</>!=?"
c@219 1618
c@219 1619 fun quoted quote pos acc cc =
c@219 1620 let fun quoted' pos text [] =
c@219 1621 error pos "Document ends during quoted string"
c@219 1622 | quoted' pos text (x::xs) =
c@219 1623 if x = quote
c@219 1624 then OK (rev text, xs, pos+1)
c@219 1625 else quoted' (pos+1) (x::text) xs
c@219 1626 in
c@219 1627 case quoted' pos [] cc of
c@219 1628 ERROR e => ERROR e
c@219 1629 | OK (text, rest, newpos) =>
c@219 1630 inside newpos (T.TEXT (implode text) :: acc) rest
c@219 1631 end
c@219 1632
c@219 1633 and name first pos acc cc =
c@219 1634 let fun name' pos text [] =
c@219 1635 error pos "Document ends during name"
c@219 1636 | name' pos text (x::xs) =
c@219 1637 if List.find (fn c => c = x) nameEnd <> NONE
c@219 1638 then OK (rev text, (x::xs), pos)
c@219 1639 else name' (pos+1) (x::text) xs
c@219 1640 in
c@219 1641 case name' (pos-1) [] (first::cc) of
c@219 1642 ERROR e => ERROR e
c@219 1643 | OK ([], [], pos) => error pos "Document ends before name"
c@219 1644 | OK ([], (x::xs), pos) => tokenError pos x
c@219 1645 | OK (text, rest, pos) =>
c@219 1646 inside pos (T.NAME (implode text) :: acc) rest
c@219 1647 end
c@219 1648
c@219 1649 and comment pos acc cc =
c@219 1650 let fun comment' pos text cc =
c@219 1651 case cc of
c@219 1652 #"-" :: #"-" :: #">" :: xs => OK (rev text, xs, pos+3)
c@219 1653 | x :: xs => comment' (pos+1) (x::text) xs
c@219 1654 | [] => error pos "Document ends during comment"
c@219 1655 in
c@219 1656 case comment' pos [] cc of
c@219 1657 ERROR e => ERROR e
c@219 1658 | OK (text, rest, pos) =>
c@219 1659 outside pos (T.COMMENT (implode text) :: acc) rest
c@219 1660 end
c@219 1661
c@219 1662 and instruction pos acc cc =
c@219 1663 case cc of
c@219 1664 #"?" :: #">" :: xs => outside (pos+2) acc xs
c@219 1665 | #">" :: _ => tokenError pos #">"
c@219 1666 | x :: xs => instruction (pos+1) acc xs
c@219 1667 | [] => error pos "Document ends during processing instruction"
c@219 1668
c@219 1669 and cdata pos acc cc =
c@219 1670 let fun cdata' pos text cc =
c@219 1671 case cc of
c@219 1672 #"]" :: #"]" :: #">" :: xs => OK (rev text, xs, pos+3)
c@219 1673 | x :: xs => cdata' (pos+1) (x::text) xs
c@219 1674 | [] => error pos "Document ends during CDATA section"
c@219 1675 in
c@219 1676 case cdata' pos [] cc of
c@219 1677 ERROR e => ERROR e
c@219 1678 | OK (text, rest, pos) =>
c@219 1679 outside pos (T.CDATA (implode text) :: acc) rest
c@219 1680 end
c@219 1681
c@219 1682 and doctype pos acc cc =
c@219 1683 case cc of
c@219 1684 #">" :: xs => outside (pos+1) acc xs
c@219 1685 | x :: xs => doctype (pos+1) acc xs
c@219 1686 | [] => error pos "Document ends during DOCTYPE"
c@219 1687
c@219 1688 and declaration pos acc cc =
c@219 1689 case cc of
c@219 1690 #"-" :: #"-" :: xs =>
c@219 1691 comment (pos+2) acc xs
c@219 1692 | #"[" :: #"C" :: #"D" :: #"A" :: #"T" :: #"A" :: #"[" :: xs =>
c@219 1693 cdata (pos+7) acc xs
c@219 1694 | #"D" :: #"O" :: #"C" :: #"T" :: #"Y" :: #"P" :: #"E" :: xs =>
c@219 1695 doctype (pos+7) acc xs
c@219 1696 | [] => error pos "Document ends during declaration"
c@219 1697 | _ => error pos "Unsupported declaration type"
c@219 1698
c@219 1699 and left pos acc cc =
c@219 1700 case cc of
c@219 1701 #"/" :: xs => inside (pos+1) (T.ANGLE_SLASH_L :: acc) xs
c@219 1702 | #"!" :: xs => declaration (pos+1) acc xs
c@219 1703 | #"?" :: xs => instruction (pos+1) acc xs
c@219 1704 | xs => inside pos (T.ANGLE_L :: acc) xs
c@219 1705
c@219 1706 and slash pos acc cc =
c@219 1707 case cc of
c@219 1708 #">" :: xs => outside (pos+1) (T.SLASH_ANGLE_R :: acc) xs
c@219 1709 | x :: _ => tokenError pos x
c@219 1710 | [] => error pos "Document ends before element closed"
c@219 1711
c@219 1712 and close pos acc xs = outside pos (T.ANGLE_R :: acc) xs
c@219 1713
c@219 1714 and equal pos acc xs = inside pos (T.EQUAL :: acc) xs
c@219 1715
c@219 1716 and outside pos acc [] = OK acc
c@219 1717 | outside pos acc cc =
c@219 1718 let fun textOf text = T.TEXT (implode (rev text))
c@219 1719 fun outside' pos [] acc [] = OK acc
c@219 1720 | outside' pos text acc [] = OK (textOf text :: acc)
c@219 1721 | outside' pos text acc (x::xs) =
c@219 1722 case x of
c@219 1723 #"<" => if text = []
c@219 1724 then left (pos+1) acc xs
c@219 1725 else left (pos+1) (textOf text :: acc) xs
c@219 1726 | x => outside' (pos+1) (x::text) acc xs
c@219 1727 in
c@219 1728 outside' pos [] acc cc
c@219 1729 end
c@219 1730
c@219 1731 and inside pos acc [] = error pos "Document ends within tag"
c@219 1732 | inside pos acc (#"<"::_) = tokenError pos #"<"
c@219 1733 | inside pos acc (x::xs) =
c@219 1734 (case x of
c@219 1735 #" " => inside | #"\t" => inside
c@219 1736 | #"\n" => inside | #"\r" => inside
c@219 1737 | #"\"" => quoted x | #"'" => quoted x
c@219 1738 | #"/" => slash | #">" => close | #"=" => equal
c@219 1739 | x => name x) (pos+1) acc xs
c@219 1740
c@219 1741 fun lex str =
c@219 1742 case outside 1 [] (explode str) of
c@219 1743 ERROR e => ERROR e
c@219 1744 | OK tokens => OK (rev tokens)
c@219 1745 end
c@219 1746
c@219 1747 structure Parse :> sig
c@219 1748 val parse : string -> document result
c@219 1749 end = struct
c@219 1750
c@219 1751 fun show [] = "end of input"
c@219 1752 | show (tok :: _) = T.name tok
c@219 1753
c@219 1754 fun error toks text = ERROR (text ^ " before " ^ show toks)
c@219 1755
c@219 1756 fun attribute elt name toks =
c@219 1757 case toks of
c@219 1758 T.EQUAL :: T.TEXT value :: xs =>
c@219 1759 namedElement {
c@219 1760 name = #name elt,
c@219 1761 children = ATTRIBUTE { name = name, value = value } ::
c@219 1762 #children elt
c@219 1763 } xs
c@219 1764 | T.EQUAL :: xs => error xs "Expected attribute value"
c@219 1765 | toks => error toks "Expected attribute assignment"
c@219 1766
c@219 1767 and content elt toks =
c@219 1768 case toks of
c@219 1769 T.ANGLE_SLASH_L :: T.NAME n :: T.ANGLE_R :: xs =>
c@219 1770 if n = #name elt
c@219 1771 then OK (elt, xs)
c@219 1772 else ERROR ("Closing tag </" ^ n ^ "> " ^
c@219 1773 "does not match opening <" ^ #name elt ^ ">")
c@219 1774 | T.TEXT text :: xs =>
c@219 1775 content {
c@219 1776 name = #name elt,
c@219 1777 children = TEXT text :: #children elt
c@219 1778 } xs
c@219 1779 | T.CDATA text :: xs =>
c@219 1780 content {
c@219 1781 name = #name elt,
c@219 1782 children = CDATA text :: #children elt
c@219 1783 } xs
c@219 1784 | T.COMMENT text :: xs =>
c@219 1785 content {
c@219 1786 name = #name elt,
c@219 1787 children = COMMENT text :: #children elt
c@219 1788 } xs
c@219 1789 | T.ANGLE_L :: xs =>
c@219 1790 (case element xs of
c@219 1791 ERROR e => ERROR e
c@219 1792 | OK (child, xs) =>
c@219 1793 content {
c@219 1794 name = #name elt,
c@219 1795 children = ELEMENT child :: #children elt
c@219 1796 } xs)
c@219 1797 | tok :: xs =>
c@219 1798 error xs ("Unexpected token " ^ T.name tok)
c@219 1799 | [] =>
c@219 1800 ERROR ("Document ends within element \"" ^ #name elt ^ "\"")
c@219 1801
c@219 1802 and namedElement elt toks =
c@219 1803 case toks of
c@219 1804 T.SLASH_ANGLE_R :: xs => OK (elt, xs)
c@219 1805 | T.NAME name :: xs => attribute elt name xs
c@219 1806 | T.ANGLE_R :: xs => content elt xs
c@219 1807 | x :: xs => error xs ("Unexpected token " ^ T.name x)
c@219 1808 | [] => ERROR "Document ends within opening tag"
c@219 1809
c@219 1810 and element toks =
c@219 1811 case toks of
c@219 1812 T.NAME name :: xs =>
c@219 1813 (case namedElement { name = name, children = [] } xs of
c@219 1814 ERROR e => ERROR e
c@219 1815 | OK ({ name, children }, xs) =>
c@219 1816 OK ({ name = name, children = rev children }, xs))
c@219 1817 | toks => error toks "Expected element name"
c@219 1818
c@219 1819 and document [] = ERROR "Empty document"
c@219 1820 | document (tok :: xs) =
c@219 1821 case tok of
c@219 1822 T.TEXT _ => document xs
c@219 1823 | T.COMMENT _ => document xs
c@219 1824 | T.ANGLE_L =>
c@219 1825 (case element xs of
c@219 1826 ERROR e => ERROR e
c@219 1827 | OK (elt, []) => OK (DOCUMENT elt)
c@219 1828 | OK (elt, (T.TEXT _ :: xs)) => OK (DOCUMENT elt)
c@219 1829 | OK (elt, xs) => error xs "Extra data after document")
c@219 1830 | _ => error xs ("Unexpected token " ^ T.name tok)
c@219 1831
c@219 1832 fun parse str =
c@219 1833 case Lex.lex str of
c@219 1834 ERROR e => ERROR e
c@219 1835 | OK tokens => document tokens
c@219 1836 end
c@219 1837
c@219 1838 structure Serialise :> sig
c@219 1839 val serialise : document -> string
c@219 1840 end = struct
c@219 1841
c@219 1842 fun attributes nodes =
c@219 1843 String.concatWith
c@219 1844 " "
c@219 1845 (map node (List.filter
c@219 1846 (fn ATTRIBUTE _ => true | _ => false)
c@219 1847 nodes))
c@219 1848
c@219 1849 and nonAttributes nodes =
c@219 1850 String.concat
c@219 1851 (map node (List.filter
c@219 1852 (fn ATTRIBUTE _ => false | _ => true)
c@219 1853 nodes))
c@219 1854
c@219 1855 and node n =
c@219 1856 case n of
c@219 1857 TEXT string =>
c@219 1858 string
c@219 1859 | CDATA string =>
c@219 1860 "<![CDATA[" ^ string ^ "]]>"
c@219 1861 | COMMENT string =>
c@219 1862 "<!-- " ^ string ^ "-->"
c@219 1863 | ATTRIBUTE { name, value } =>
c@219 1864 name ^ "=" ^ "\"" ^ value ^ "\"" (*!!!*)
c@219 1865 | ELEMENT { name, children } =>
c@219 1866 "<" ^ name ^
c@219 1867 (case (attributes children) of
c@219 1868 "" => ""
c@219 1869 | s => " " ^ s) ^
c@219 1870 (case (nonAttributes children) of
c@219 1871 "" => "/>"
c@219 1872 | s => ">" ^ s ^ "</" ^ name ^ ">")
c@219 1873
c@219 1874 fun serialise (DOCUMENT { name, children }) =
c@219 1875 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" ^
c@219 1876 node (ELEMENT { name = name, children = children })
c@219 1877 end
c@219 1878
c@219 1879 val parse = Parse.parse
c@219 1880 val serialise = Serialise.serialise
c@219 1881
c@219 1882 end
c@219 1883
c@219 1884
c@219 1885 structure SvnControl :> VCS_CONTROL = struct
c@219 1886
c@219 1887 val svn_program = "svn"
c@219 1888
c@219 1889 fun svn_command context libname args =
c@219 1890 FileBits.command context libname (svn_program :: args)
c@219 1891
c@219 1892 fun svn_command_output context libname args =
c@219 1893 FileBits.command_output context libname (svn_program :: args)
c@219 1894
c@219 1895 fun svn_command_lines context libname args =
c@219 1896 case svn_command_output context libname args of
c@219 1897 ERROR e => ERROR e
c@219 1898 | OK s => OK (String.tokens (fn c => c = #"\n" orelse c = #"\r") s)
c@219 1899
c@219 1900 fun split_line_pair line =
c@219 1901 let fun strip_leading_ws str = case explode str of
c@219 1902 #" "::rest => implode rest
c@219 1903 | _ => str
c@219 1904 in
c@219 1905 case String.tokens (fn c => c = #":") line of
c@219 1906 [] => ("", "")
c@219 1907 | first::rest =>
c@219 1908 (first, strip_leading_ws (String.concatWith ":" rest))
c@219 1909 end
c@219 1910
c@219 1911 fun is_working context =
c@219 1912 case svn_command_output context "" ["--version"] of
c@219 1913 OK "" => OK false
c@219 1914 | OK _ => OK true
c@219 1915 | ERROR e => ERROR e
c@219 1916
c@219 1917 structure X = SubXml
c@219 1918
c@219 1919 fun svn_info context libname route =
c@219 1920 (* SVN 1.9 has info --show-item which is just what we need,
c@219 1921 but at this point we still have 1.8 on the CI boxes so we
c@219 1922 might as well aim to support it. For that we really have to
c@219 1923 use the XML output format, since the default info output is
c@219 1924 localised. This is the only thing our mini-XML parser is
c@219 1925 used for though, so it would be good to trim it at some
c@219 1926 point *)
c@219 1927 let fun find elt [] = OK elt
c@219 1928 | find { children, ... } (first :: rest) =
c@219 1929 case List.find (fn (X.ELEMENT { name, ... }) => name = first
c@219 1930 | _ => false)
c@219 1931 children of
c@219 1932 NONE => ERROR ("No element \"" ^ first ^ "\" in SVN XML")
c@219 1933 | SOME (X.ELEMENT e) => find e rest
c@219 1934 | SOME _ => ERROR "Internal error"
c@219 1935 in
c@219 1936 case svn_command_output context libname ["info", "--xml"] of
c@219 1937 ERROR e => ERROR e
c@219 1938 | OK xml =>
c@219 1939 case X.parse xml of
c@219 1940 X.ERROR e => ERROR e
c@219 1941 | X.OK (X.DOCUMENT doc) => find doc route
c@219 1942 end
c@219 1943
c@219 1944 fun exists context libname =
c@219 1945 OK (OS.FileSys.isDir (FileBits.subpath context libname ".svn"))
c@219 1946 handle _ => OK false
c@219 1947
c@219 1948 fun remote_for context (libname, source) =
c@219 1949 Provider.remote_url context SVN source libname
c@219 1950
c@219 1951 (* Remote the checkout came from, not necessarily the one we want *)
c@219 1952 fun actual_remote_for context libname =
c@219 1953 case svn_info context libname ["entry", "url"] of
c@219 1954 ERROR e => ERROR e
c@219 1955 | OK { children, ... } =>
c@219 1956 case List.find (fn (X.TEXT _) => true | _ => false) children of
c@219 1957 NONE => ERROR "No content for URL in SVN info XML"
c@219 1958 | SOME (X.TEXT url) => OK url
c@219 1959 | SOME _ => ERROR "Internal error"
c@219 1960
c@219 1961 fun id_of context libname =
c@219 1962 case svn_info context libname ["entry"] of
c@219 1963 ERROR e => ERROR e
c@219 1964 | OK { children, ... } =>
c@219 1965 case List.find
c@219 1966 (fn (X.ATTRIBUTE { name = "revision", ... }) => true
c@219 1967 | _ => false)
c@219 1968 children of
c@219 1969 NONE => ERROR "No revision for entry in SVN info XML"
c@219 1970 | SOME (X.ATTRIBUTE { value, ... }) => OK value
c@219 1971 | SOME _ => ERROR "Internal error"
c@219 1972
c@219 1973 fun is_at context (libname, id_or_tag) =
c@219 1974 case id_of context libname of
c@219 1975 ERROR e => ERROR e
c@219 1976 | OK id => OK (id = id_or_tag)
c@219 1977
c@219 1978 fun is_on_branch context (libname, b) =
c@219 1979 OK (b = DEFAULT_BRANCH)
c@219 1980
c@219 1981 fun check_remote context (libname, source) =
c@219 1982 case (remote_for context (libname, source),
c@219 1983 actual_remote_for context libname) of
c@219 1984 (_, ERROR e) => ERROR e
c@219 1985 | (url, OK actual) =>
c@219 1986 if actual = url
c@219 1987 then OK ()
c@219 1988 else svn_command context libname ["relocate", url]
c@219 1989
c@219 1990 fun is_newest context (libname, source, branch) =
c@219 1991 case check_remote context (libname, source) of
c@219 1992 ERROR e => ERROR e
c@219 1993 | OK () =>
c@219 1994 case svn_command_lines context libname
c@219 1995 ["status", "--show-updates"] of
c@219 1996 ERROR e => ERROR e
c@219 1997 | OK lines =>
c@219 1998 case rev lines of
c@219 1999 [] => ERROR "No result returned for server status"
c@219 2000 | last_line::_ =>
c@219 2001 case rev (String.tokens (fn c => c = #" ") last_line) of
c@219 2002 [] => ERROR "No revision field found in server status"
c@219 2003 | server_id::_ => is_at context (libname, server_id)
c@219 2004
c@219 2005 fun is_newest_locally context (libname, branch) =
c@219 2006 OK true (* no local history *)
c@219 2007
c@219 2008 fun is_modified_locally context libname =
c@219 2009 case svn_command_output context libname ["status"] of
c@219 2010 ERROR e => ERROR e
c@219 2011 | OK "" => OK false
c@219 2012 | OK _ => OK true
c@219 2013
c@219 2014 fun checkout context (libname, source, branch) =
c@219 2015 let val url = remote_for context (libname, source)
c@219 2016 val path = FileBits.libpath context libname
c@219 2017 in
c@219 2018 if FileBits.nonempty_dir_exists path
c@219 2019 then (* Surprisingly, SVN itself has no problem with
c@219 2020 this. But for consistency with other VCSes we
c@219 2021 don't allow it *)
c@219 2022 ERROR ("Refusing checkout to nonempty dir \"" ^ path ^ "\"")
c@219 2023 else
c@219 2024 (* make the lib dir rather than just the ext dir, since
c@219 2025 the lib dir might be nested and svn will happily check
c@219 2026 out into an existing empty dir anyway *)
c@219 2027 case FileBits.mkpath (FileBits.libpath context libname) of
c@219 2028 ERROR e => ERROR e
c@219 2029 | _ => svn_command context "" ["checkout", url, libname]
c@219 2030 end
c@219 2031
c@219 2032 fun update context (libname, source, branch) =
c@219 2033 case check_remote context (libname, source) of
c@219 2034 ERROR e => ERROR e
c@219 2035 | OK () =>
c@219 2036 case svn_command context libname
c@219 2037 ["update", "--accept", "postpone"] of
c@219 2038 ERROR e => ERROR e
c@219 2039 | _ => OK ()
c@219 2040
c@219 2041 fun update_to context (libname, _, "") =
c@219 2042 ERROR "Non-empty id (tag or revision id) required for update_to"
c@219 2043 | update_to context (libname, source, id) =
c@219 2044 case check_remote context (libname, source) of
c@219 2045 ERROR e => ERROR e
c@219 2046 | OK () =>
c@219 2047 case svn_command context libname
c@219 2048 ["update", "-r", id, "--accept", "postpone"] of
c@219 2049 ERROR e => ERROR e
c@219 2050 | OK _ => OK ()
c@219 2051
c@219 2052 fun copy_url_for context libname =
c@219 2053 actual_remote_for context libname
c@219 2054
c@219 2055 end
c@219 2056
c@219 2057 structure AnyLibControl :> LIB_CONTROL = struct
c@219 2058
c@219 2059 structure H = LibControlFn(HgControl)
c@219 2060 structure G = LibControlFn(GitControl)
c@219 2061 structure S = LibControlFn(SvnControl)
c@219 2062
c@219 2063 fun review context (spec as { vcs, ... } : libspec) =
c@219 2064 (fn HG => H.review | GIT => G.review | SVN => S.review) vcs context spec
c@219 2065
c@219 2066 fun status context (spec as { vcs, ... } : libspec) =
c@219 2067 (fn HG => H.status | GIT => G.status | SVN => S.status) vcs context spec
c@219 2068
c@219 2069 fun update context (spec as { vcs, ... } : libspec) =
c@219 2070 (fn HG => H.update | GIT => G.update | SVN => S.update) vcs context spec
c@219 2071
c@219 2072 fun id_of context (spec as { vcs, ... } : libspec) =
c@219 2073 (fn HG => H.id_of | GIT => G.id_of | SVN => S.id_of) vcs context spec
c@219 2074
c@219 2075 fun is_working context vcs =
c@219 2076 (fn HG => H.is_working | GIT => G.is_working | SVN => S.is_working)
c@219 2077 vcs context vcs
c@219 2078
c@219 2079 end
c@219 2080
c@219 2081
c@219 2082 type exclusions = string list
c@219 2083
c@219 2084 structure Archive :> sig
c@219 2085
c@219 2086 val archive : string * exclusions -> project -> OS.Process.status
c@219 2087
c@219 2088 end = struct
c@219 2089
c@219 2090 (* The idea of "archive" is to replace hg/git archive, which won't
c@219 2091 include files, like the Repoint-introduced external libraries,
c@219 2092 that are not under version control with the main repo.
c@219 2093
c@219 2094 The process goes like this:
c@219 2095
c@219 2096 - Make sure we have a target filename from the user, and take
c@219 2097 its basename as our archive directory name
c@219 2098
c@219 2099 - Make an "archive root" subdir of the project repo, named
c@219 2100 typically .repoint-archive
c@219 2101
c@219 2102 - Identify the VCS used for the project repo. Note that any
c@219 2103 explicit references to VCS type in this structure are to
c@219 2104 the VCS used for the project (something Repoint doesn't
c@219 2105 otherwise care about), not for an individual library
c@219 2106
c@219 2107 - Synthesise a Repoint project with the archive root as its
c@219 2108 root path, "." as its extdir, with one library whose
c@219 2109 name is the user-supplied basename and whose explicit
c@219 2110 source URL is the original project root; update that
c@219 2111 project -- thus cloning the original project to a subdir
c@219 2112 of the archive root
c@219 2113
c@219 2114 - Synthesise a Repoint project identical to the original one for
c@219 2115 this project, but with the newly-cloned copy as its root
c@219 2116 path; update that project -- thus checking out clean copies
c@219 2117 of the external library dirs
c@219 2118
c@219 2119 - Call out to an archive program to archive up the new copy,
c@219 2120 running e.g.
c@219 2121 tar cvzf project-release.tar.gz \
c@219 2122 --exclude=.hg --exclude=.git project-release
c@219 2123 in the archive root dir
c@219 2124
c@219 2125 - (We also omit the repoint-project.json file and any trace of
c@219 2126 Repoint. It can't properly be run in a directory where the
c@219 2127 external project folders already exist but their repo history
c@219 2128 does not. End users shouldn't get to see Repoint)
c@219 2129
c@219 2130 - Clean up by deleting the new copy
c@219 2131 *)
c@219 2132
c@219 2133 fun project_vcs_id_and_url dir =
c@219 2134 let val context = {
c@219 2135 rootpath = dir,
c@219 2136 extdir = ".",
c@219 2137 providers = [],
c@219 2138 accounts = []
c@219 2139 }
c@219 2140 val vcs_maybe =
c@219 2141 case [HgControl.exists context ".",
c@219 2142 GitControl.exists context ".",
c@219 2143 SvnControl.exists context "."] of
c@219 2144 [OK true, OK false, OK false] => OK HG
c@219 2145 | [OK false, OK true, OK false] => OK GIT
c@219 2146 | [OK false, OK false, OK true] => OK SVN
c@219 2147 | _ => ERROR ("Unable to identify VCS for directory " ^ dir)
c@219 2148 in
c@219 2149 case vcs_maybe of
c@219 2150 ERROR e => ERROR e
c@219 2151 | OK vcs =>
c@219 2152 case (fn HG => HgControl.id_of
c@219 2153 | GIT => GitControl.id_of
c@219 2154 | SVN => SvnControl.id_of)
c@219 2155 vcs context "." of
c@219 2156 ERROR e => ERROR ("Unable to find id of project repo: " ^ e)
c@219 2157 | OK id =>
c@219 2158 case (fn HG => HgControl.copy_url_for
c@219 2159 | GIT => GitControl.copy_url_for
c@219 2160 | SVN => SvnControl.copy_url_for)
c@219 2161 vcs context "." of
c@219 2162 ERROR e => ERROR ("Unable to find URL of project repo: "
c@219 2163 ^ e)
c@219 2164 | OK url => OK (vcs, id, url)
c@219 2165 end
c@219 2166
c@219 2167 fun make_archive_root (context : context) =
c@219 2168 let val path = OS.Path.joinDirFile {
c@219 2169 dir = #rootpath context,
c@219 2170 file = RepointFilenames.archive_dir
c@219 2171 }
c@219 2172 in
c@219 2173 case FileBits.mkpath path of
c@219 2174 ERROR e => raise Fail ("Failed to create archive directory \""
c@219 2175 ^ path ^ "\": " ^ e)
c@219 2176 | OK () => path
c@219 2177 end
c@219 2178
c@219 2179 fun archive_path archive_dir target_name =
c@219 2180 OS.Path.joinDirFile {
c@219 2181 dir = archive_dir,
c@219 2182 file = target_name
c@219 2183 }
c@219 2184
c@219 2185 fun check_nonexistent path =
c@219 2186 case SOME (OS.FileSys.fileSize path) handle OS.SysErr _ => NONE of
c@219 2187 NONE => ()
c@219 2188 | _ => raise Fail ("Path " ^ path ^ " exists, not overwriting")
c@219 2189
c@219 2190 fun make_archive_copy target_name (vcs, project_id, source_url)
c@219 2191 ({ context, ... } : project) =
c@219 2192 let val archive_root = make_archive_root context
c@219 2193 val synthetic_context = {
c@219 2194 rootpath = archive_root,
c@219 2195 extdir = ".",
c@219 2196 providers = [],
c@219 2197 accounts = []
c@219 2198 }
c@219 2199 val synthetic_library = {
c@219 2200 libname = target_name,
c@219 2201 vcs = vcs,
c@219 2202 source = URL_SOURCE source_url,
c@219 2203 branch = DEFAULT_BRANCH, (* overridden by pinned id below *)
c@219 2204 project_pin = PINNED project_id,
c@219 2205 lock_pin = PINNED project_id
c@219 2206 }
c@219 2207 val path = archive_path archive_root target_name
c@219 2208 val _ = print ("Cloning original project to " ^ path
c@219 2209 ^ " at revision " ^ project_id ^ "...\n");
c@219 2210 val _ = check_nonexistent path
c@219 2211 in
c@219 2212 case AnyLibControl.update synthetic_context synthetic_library of
c@219 2213 ERROR e => ERROR ("Failed to clone original project to "
c@219 2214 ^ path ^ ": " ^ e)
c@219 2215 | OK _ => OK archive_root
c@219 2216 end
c@219 2217
c@219 2218 fun update_archive archive_root target_name
c@219 2219 (project as { context, ... } : project) =
c@219 2220 let val synthetic_context = {
c@219 2221 rootpath = archive_path archive_root target_name,
c@219 2222 extdir = #extdir context,
c@219 2223 providers = #providers context,
c@219 2224 accounts = #accounts context
c@219 2225 }
c@219 2226 in
c@219 2227 foldl (fn (lib, acc) =>
c@219 2228 case acc of
c@219 2229 ERROR e => ERROR e
c@219 2230 | OK () => AnyLibControl.update synthetic_context lib)
c@219 2231 (OK ())
c@219 2232 (#libs project)
c@219 2233 end
c@219 2234
c@219 2235 datatype packer = TAR
c@219 2236 | TAR_GZ
c@219 2237 | TAR_BZ2
c@219 2238 | TAR_XZ
c@219 2239 (* could add other packers, e.g. zip, if we knew how to
c@219 2240 handle the file omissions etc properly in pack_archive *)
c@219 2241
c@219 2242 fun packer_and_basename path =
c@219 2243 let val extensions = [ (".tar", TAR),
c@219 2244 (".tar.gz", TAR_GZ),
c@219 2245 (".tar.bz2", TAR_BZ2),
c@219 2246 (".tar.xz", TAR_XZ)]
c@219 2247 val filename = OS.Path.file path
c@219 2248 in
c@219 2249 foldl (fn ((ext, packer), acc) =>
c@219 2250 if String.isSuffix ext filename
c@219 2251 then SOME (packer,
c@219 2252 String.substring (filename, 0,
c@219 2253 String.size filename -
c@219 2254 String.size ext))
c@219 2255 else acc)
c@219 2256 NONE
c@219 2257 extensions
c@219 2258 end
c@219 2259
c@219 2260 fun pack_archive archive_root target_name target_path packer exclusions =
c@219 2261 case FileBits.command {
c@219 2262 rootpath = archive_root,
c@219 2263 extdir = ".",
c@219 2264 providers = [],
c@219 2265 accounts = []
c@219 2266 } "" ([
c@219 2267 "tar",
c@219 2268 case packer of
c@219 2269 TAR => "cf"
c@219 2270 | TAR_GZ => "czf"
c@219 2271 | TAR_BZ2 => "cjf"
c@219 2272 | TAR_XZ => "cJf",
c@219 2273 target_path,
c@219 2274 "--exclude=.hg",
c@219 2275 "--exclude=.git",
c@219 2276 "--exclude=.svn",
c@219 2277 "--exclude=repoint",
c@219 2278 "--exclude=repoint.sml",
c@219 2279 "--exclude=repoint.ps1",
c@219 2280 "--exclude=repoint.bat",
c@219 2281 "--exclude=repoint-project.json",
c@219 2282 "--exclude=repoint-lock.json"
c@219 2283 ] @ (map (fn e => "--exclude=" ^ e) exclusions) @
c@219 2284 [ target_name ])
c@219 2285 of
c@219 2286 ERROR e => ERROR e
c@219 2287 | OK _ => FileBits.rmpath (archive_path archive_root target_name)
c@219 2288
c@219 2289 fun archive (target_path, exclusions) (project : project) =
c@219 2290 let val _ = check_nonexistent target_path
c@219 2291 val (packer, name) =
c@219 2292 case packer_and_basename target_path of
c@219 2293 NONE => raise Fail ("Unsupported archive file extension in "
c@219 2294 ^ target_path)
c@219 2295 | SOME pn => pn
c@219 2296 val details =
c@219 2297 case project_vcs_id_and_url (#rootpath (#context project)) of
c@219 2298 ERROR e => raise Fail e
c@219 2299 | OK details => details
c@219 2300 val archive_root =
c@219 2301 case make_archive_copy name details project of
c@219 2302 ERROR e => raise Fail e
c@219 2303 | OK archive_root => archive_root
c@219 2304 val outcome =
c@219 2305 case update_archive archive_root name project of
c@219 2306 ERROR e => ERROR e
c@219 2307 | OK _ =>
c@219 2308 case pack_archive archive_root name
c@219 2309 target_path packer exclusions of
c@219 2310 ERROR e => ERROR e
c@219 2311 | OK _ => OK ()
c@219 2312 in
c@219 2313 case outcome of
c@219 2314 ERROR e => raise Fail e
c@219 2315 | OK () => OS.Process.success
c@219 2316 end
c@219 2317
c@219 2318 end
c@219 2319
c@219 2320 val libobjname = "libraries"
c@219 2321
c@219 2322 fun load_libspec spec_json lock_json libname : libspec =
c@219 2323 let open JsonBits
c@219 2324 val libobj = lookup_mandatory spec_json [libobjname, libname]
c@219 2325 val vcs = lookup_mandatory_string libobj ["vcs"]
c@219 2326 val retrieve = lookup_optional_string libobj
c@219 2327 val service = retrieve ["service"]
c@219 2328 val owner = retrieve ["owner"]
c@219 2329 val repo = retrieve ["repository"]
c@219 2330 val url = retrieve ["url"]
c@219 2331 val branch = retrieve ["branch"]
c@219 2332 val project_pin = case retrieve ["pin"] of
c@219 2333 NONE => UNPINNED
c@219 2334 | SOME p => PINNED p
c@219 2335 val lock_pin = case lookup_optional lock_json [libobjname, libname] of
c@219 2336 NONE => UNPINNED
c@219 2337 | SOME ll => case lookup_optional_string ll ["pin"] of
c@219 2338 SOME p => PINNED p
c@219 2339 | NONE => UNPINNED
c@219 2340 in
c@219 2341 {
c@219 2342 libname = libname,
c@219 2343 vcs = case vcs of
c@219 2344 "hg" => HG
c@219 2345 | "git" => GIT
c@219 2346 | "svn" => SVN
c@219 2347 | other => raise Fail ("Unknown version-control system \"" ^
c@219 2348 other ^ "\""),
c@219 2349 source = case (url, service, owner, repo) of
c@219 2350 (SOME u, NONE, _, _) => URL_SOURCE u
c@219 2351 | (NONE, SOME ss, owner, repo) =>
c@219 2352 SERVICE_SOURCE { service = ss, owner = owner, repo = repo }
c@219 2353 | _ => raise Fail ("Must have exactly one of service " ^
c@219 2354 "or url string"),
c@219 2355 project_pin = project_pin,
c@219 2356 lock_pin = lock_pin,
c@219 2357 branch = case branch of
c@219 2358 NONE => DEFAULT_BRANCH
c@219 2359 | SOME b =>
c@219 2360 case vcs of
c@219 2361 "svn" => raise Fail ("Branches not supported for " ^
c@219 2362 "svn repositories; change " ^
c@219 2363 "URL instead")
c@219 2364 | _ => BRANCH b
c@219 2365 }
c@219 2366 end
c@219 2367
c@219 2368 fun load_userconfig () : userconfig =
c@219 2369 let val home = FileBits.homedir ()
c@219 2370 val conf_json =
c@219 2371 JsonBits.load_json_from
c@219 2372 (OS.Path.joinDirFile {
c@219 2373 dir = home,
c@219 2374 file = RepointFilenames.user_config_file })
c@219 2375 handle IO.Io _ => Json.OBJECT []
c@219 2376 in
c@219 2377 {
c@219 2378 accounts = case JsonBits.lookup_optional conf_json ["accounts"] of
c@219 2379 NONE => []
c@219 2380 | SOME (Json.OBJECT aa) =>
c@219 2381 map (fn (k, (Json.STRING v)) =>
c@219 2382 { service = k, login = v }
c@219 2383 | _ => raise Fail
c@219 2384 "String expected for account name")
c@219 2385 aa
c@219 2386 | _ => raise Fail "Array expected for accounts",
c@219 2387 providers = Provider.load_providers conf_json
c@219 2388 }
c@219 2389 end
c@219 2390
c@219 2391 datatype pintype =
c@219 2392 NO_LOCKFILE |
c@219 2393 USE_LOCKFILE
c@219 2394
c@219 2395 fun load_project (userconfig : userconfig) rootpath pintype : project =
c@219 2396 let val spec_file = FileBits.project_spec_path rootpath
c@219 2397 val lock_file = FileBits.project_lock_path rootpath
c@219 2398 val _ = if OS.FileSys.access (spec_file, [OS.FileSys.A_READ])
c@219 2399 handle OS.SysErr _ => false
c@219 2400 then ()
c@219 2401 else raise Fail ("Failed to open project spec file " ^
c@219 2402 (RepointFilenames.project_file) ^ " in " ^
c@219 2403 rootpath ^
c@219 2404 ".\nPlease ensure the spec file is in the " ^
c@219 2405 "project root and run this from there.")
c@219 2406 val spec_json = JsonBits.load_json_from spec_file
c@219 2407 val lock_json = if pintype = USE_LOCKFILE
c@219 2408 then JsonBits.load_json_from lock_file
c@219 2409 handle IO.Io _ => Json.OBJECT []
c@219 2410 else Json.OBJECT []
c@219 2411 val extdir = JsonBits.lookup_mandatory_string spec_json
c@219 2412 ["config", "extdir"]
c@219 2413 val spec_libs = JsonBits.lookup_optional spec_json [libobjname]
c@219 2414 val lock_libs = JsonBits.lookup_optional lock_json [libobjname]
c@219 2415 val providers = Provider.load_more_providers
c@219 2416 (#providers userconfig) spec_json
c@219 2417 val libnames = case spec_libs of
c@219 2418 NONE => []
c@219 2419 | SOME (Json.OBJECT ll) => map (fn (k, v) => k) ll
c@219 2420 | _ => raise Fail "Object expected for libs"
c@219 2421 in
c@219 2422 {
c@219 2423 context = {
c@219 2424 rootpath = rootpath,
c@219 2425 extdir = extdir,
c@219 2426 providers = providers,
c@219 2427 accounts = #accounts userconfig
c@219 2428 },
c@219 2429 libs = map (load_libspec spec_json lock_json) libnames
c@219 2430 }
c@219 2431 end
c@219 2432
c@219 2433 fun save_lock_file rootpath locks =
c@219 2434 let val lock_file = FileBits.project_lock_path rootpath
c@219 2435 open Json
c@219 2436 val lock_json =
c@219 2437 OBJECT [
c@219 2438 (libobjname,
c@219 2439 OBJECT (map (fn { libname, id_or_tag } =>
c@219 2440 (libname,
c@219 2441 OBJECT [ ("pin", STRING id_or_tag) ]))
c@219 2442 locks))
c@219 2443 ]
c@219 2444 in
c@219 2445 JsonBits.save_json_to lock_file lock_json
c@219 2446 end
cannam@258 2447
cannam@258 2448 fun checkpoint_completion_file rootpath =
cannam@258 2449 let val completion_file = FileBits.project_completion_path rootpath
cannam@258 2450 val stream = TextIO.openOut completion_file
cannam@258 2451 in
cannam@258 2452 TextIO.closeOut stream
cannam@258 2453 end
cannam@258 2454
c@219 2455 fun pad_to n str =
c@219 2456 if n <= String.size str then str
c@219 2457 else pad_to n (str ^ " ")
c@219 2458
c@219 2459 fun hline_to 0 = ""
c@219 2460 | hline_to n = "-" ^ hline_to (n-1)
c@219 2461
c@219 2462 val libname_width = 28
c@219 2463 val libstate_width = 11
c@219 2464 val localstate_width = 17
c@219 2465 val notes_width = 5
c@219 2466 val divider = " | "
c@219 2467 val clear_line = "\r" ^ pad_to 80 "";
c@219 2468
c@219 2469 fun print_status_header () =
c@219 2470 print (clear_line ^ "\n " ^
c@219 2471 pad_to libname_width "Library" ^ divider ^
c@219 2472 pad_to libstate_width "State" ^ divider ^
c@219 2473 pad_to localstate_width "Local" ^ divider ^
c@219 2474 "Notes" ^ "\n " ^
c@219 2475 hline_to libname_width ^ "-+-" ^
c@219 2476 hline_to libstate_width ^ "-+-" ^
c@219 2477 hline_to localstate_width ^ "-+-" ^
c@219 2478 hline_to notes_width ^ "\n")
c@219 2479
c@219 2480 fun print_outcome_header () =
c@219 2481 print (clear_line ^ "\n " ^
c@219 2482 pad_to libname_width "Library" ^ divider ^
c@219 2483 pad_to libstate_width "Outcome" ^ divider ^
c@219 2484 "Notes" ^ "\n " ^
c@219 2485 hline_to libname_width ^ "-+-" ^
c@219 2486 hline_to libstate_width ^ "-+-" ^
c@219 2487 hline_to notes_width ^ "\n")
c@219 2488
c@219 2489 fun print_status with_network (lib : libspec, status) =
c@219 2490 let val libstate_str =
c@219 2491 case status of
c@219 2492 OK (ABSENT, _) => "Absent"
c@219 2493 | OK (CORRECT, _) => if with_network then "Correct" else "Present"
c@219 2494 | OK (SUPERSEDED, _) => "Superseded"
c@219 2495 | OK (WRONG, _) => "Wrong"
c@219 2496 | ERROR _ => "Error"
c@219 2497 val localstate_str =
c@219 2498 case status of
c@219 2499 OK (_, MODIFIED) => "Modified"
c@219 2500 | OK (_, LOCK_MISMATCHED) => "Differs from Lock"
c@219 2501 | OK (_, CLEAN) => "Clean"
c@219 2502 | ERROR _ => ""
c@219 2503 val error_str =
c@219 2504 case status of
c@219 2505 ERROR e => e
c@219 2506 | _ => ""
c@219 2507 in
c@219 2508 print (" " ^
c@219 2509 pad_to libname_width (#libname lib) ^ divider ^
c@219 2510 pad_to libstate_width libstate_str ^ divider ^
c@219 2511 pad_to localstate_width localstate_str ^ divider ^
c@219 2512 error_str ^ "\n")
c@219 2513 end
c@219 2514
c@219 2515 fun print_update_outcome (lib : libspec, outcome) =
c@219 2516 let val outcome_str =
c@219 2517 case outcome of
c@219 2518 OK id => "Ok"
c@219 2519 | ERROR e => "Failed"
c@219 2520 val error_str =
c@219 2521 case outcome of
c@219 2522 ERROR e => e
c@219 2523 | _ => ""
c@219 2524 in
c@219 2525 print (" " ^
c@219 2526 pad_to libname_width (#libname lib) ^ divider ^
c@219 2527 pad_to libstate_width outcome_str ^ divider ^
c@219 2528 error_str ^ "\n")
c@219 2529 end
c@219 2530
c@219 2531 fun vcs_name HG = ("Mercurial", "hg")
c@219 2532 | vcs_name GIT = ("Git", "git")
c@219 2533 | vcs_name SVN = ("Subversion", "svn")
c@219 2534
c@219 2535 fun print_problem_summary context lines =
c@219 2536 let val failed_vcs =
c@219 2537 foldl (fn (({ vcs, ... } : libspec, ERROR _), acc) => vcs::acc
c@219 2538 | (_, acc) => acc) [] lines
c@219 2539 fun report_nonworking vcs error =
c@219 2540 print ((if error = "" then "" else error ^ "\n\n") ^
c@219 2541 "Error: The project uses the " ^ (#1 (vcs_name vcs)) ^
c@219 2542 " version control system, but its\n" ^
c@219 2543 "executable program (" ^ (#2 (vcs_name vcs)) ^
c@219 2544 ") does not appear to be installed in the program path\n\n")
c@219 2545 fun check_working [] checked = ()
c@219 2546 | check_working (vcs::rest) checked =
c@219 2547 if List.exists (fn v => vcs = v) checked
c@219 2548 then check_working rest checked
c@219 2549 else
c@219 2550 case AnyLibControl.is_working context vcs of
c@219 2551 OK true => check_working rest checked
c@219 2552 | OK false => (report_nonworking vcs "";
c@219 2553 check_working rest (vcs::checked))
c@219 2554 | ERROR e => (report_nonworking vcs e;
c@219 2555 check_working rest (vcs::checked))
c@219 2556 in
c@219 2557 print "\nError: Some operations failed\n\n";
c@219 2558 check_working failed_vcs []
c@219 2559 end
c@219 2560
c@219 2561 fun act_and_print action print_header print_line context (libs : libspec list) =
c@219 2562 let val lines = map (fn lib => (lib, action lib)) libs
c@219 2563 val imperfect = List.exists (fn (_, ERROR _) => true | _ => false) lines
c@219 2564 val _ = print_header ()
c@219 2565 in
c@219 2566 app print_line lines;
c@219 2567 if imperfect then print_problem_summary context lines else ();
c@219 2568 lines
c@219 2569 end
c@219 2570
c@219 2571 fun return_code_for outcomes =
c@219 2572 foldl (fn ((_, result), acc) =>
c@219 2573 case result of
c@219 2574 ERROR _ => OS.Process.failure
c@219 2575 | _ => acc)
c@219 2576 OS.Process.success
c@219 2577 outcomes
c@219 2578
c@219 2579 fun status_of_project ({ context, libs } : project) =
c@219 2580 return_code_for (act_and_print (AnyLibControl.status context)
c@219 2581 print_status_header (print_status false)
c@219 2582 context libs)
c@219 2583
c@219 2584 fun review_project ({ context, libs } : project) =
c@219 2585 return_code_for (act_and_print (AnyLibControl.review context)
c@219 2586 print_status_header (print_status true)
c@219 2587 context libs)
c@219 2588
c@219 2589 fun lock_project ({ context, libs } : project) =
c@219 2590 let val _ = if FileBits.verbose ()
c@219 2591 then print ("Scanning IDs for lock file...\n")
c@219 2592 else ()
c@219 2593 val outcomes = map (fn lib => (lib, AnyLibControl.id_of context lib))
c@219 2594 libs
c@219 2595 val locks =
c@219 2596 List.concat
c@219 2597 (map (fn (lib : libspec, result) =>
c@219 2598 case result of
c@219 2599 ERROR _ => []
c@219 2600 | OK id => [{ libname = #libname lib,
c@219 2601 id_or_tag = id }])
c@219 2602 outcomes)
c@219 2603 val return_code = return_code_for outcomes
c@219 2604 val _ = print clear_line
c@219 2605 in
c@219 2606 if OS.Process.isSuccess return_code
c@219 2607 then save_lock_file (#rootpath context) locks
c@219 2608 else ();
c@219 2609 return_code
c@219 2610 end
c@219 2611
c@219 2612 fun update_project (project as { context, libs }) =
c@219 2613 let val outcomes = act_and_print
c@219 2614 (AnyLibControl.update context)
c@219 2615 print_outcome_header print_update_outcome
c@219 2616 context libs
c@219 2617 val _ = if List.exists (fn (_, OK _) => true | _ => false) outcomes
c@219 2618 then lock_project project
c@219 2619 else OS.Process.success
cannam@258 2620 val return_code = return_code_for outcomes
c@219 2621 in
cannam@258 2622 if OS.Process.isSuccess return_code
cannam@258 2623 then checkpoint_completion_file (#rootpath context)
cannam@258 2624 else ();
cannam@258 2625 return_code
c@219 2626 end
c@219 2627
c@219 2628 fun load_local_project pintype =
c@219 2629 let val userconfig = load_userconfig ()
c@219 2630 val rootpath = OS.FileSys.getDir ()
c@219 2631 in
c@219 2632 load_project userconfig rootpath pintype
c@219 2633 end
c@219 2634
c@219 2635 fun with_local_project pintype f =
c@219 2636 let open OS.Process
c@219 2637 val return_code =
c@219 2638 f (load_local_project pintype)
c@219 2639 handle Fail msg =>
c@219 2640 failure before print ("Error: " ^ msg)
c@219 2641 | JsonBits.Config msg =>
c@219 2642 failure before print ("Error in configuration: " ^ msg)
c@219 2643 | e =>
c@219 2644 failure before print ("Error: " ^ exnMessage e)
c@219 2645 val _ = print "\n";
c@219 2646 in
c@219 2647 return_code
c@219 2648 end
c@219 2649
c@219 2650 fun review () = with_local_project USE_LOCKFILE review_project
c@219 2651 fun status () = with_local_project USE_LOCKFILE status_of_project
c@219 2652 fun update () = with_local_project NO_LOCKFILE update_project
c@219 2653 fun lock () = with_local_project NO_LOCKFILE lock_project
c@219 2654 fun install () = with_local_project USE_LOCKFILE update_project
c@219 2655
c@219 2656 fun version () =
c@219 2657 (print ("v" ^ repoint_version ^ "\n");
c@219 2658 OS.Process.success)
c@219 2659
c@219 2660 fun usage () =
c@219 2661 (print "\nRepoint ";
c@219 2662 version ();
cannam@258 2663 print ("\n A simple manager for third-party source code dependencies.\n"
cannam@258 2664 ^ " http://all-day-breakfast.com/repoint/\n\n"
c@219 2665 ^ "Usage:\n\n"
cannam@258 2666 ^ " repoint <command> [<options>]\n\n"
c@219 2667 ^ "where <command> is one of:\n\n"
c@219 2668 ^ " status print quick report on local status only, without using network\n"
c@219 2669 ^ " review check configured libraries against their providers, and report\n"
c@219 2670 ^ " install update configured libraries according to project specs and lock file\n"
c@219 2671 ^ " update update configured libraries and lock file according to project specs\n"
cannam@258 2672 ^ " lock rewrite lock file to match local library status\n"
cannam@258 2673 ^ " archive pack up project and all libraries into an archive file:\n"
cannam@258 2674 ^ " invoke as 'repoint archive targetfile.tar.gz --exclude unwanted.txt'\n"
cannam@258 2675 ^ " version print the Repoint version number and exit\n\n"
cannam@258 2676 ^ "and <options> may include:\n\n"
cannam@258 2677 ^ " --directory <dir>\n"
cannam@258 2678 ^ " change to directory <dir> before doing anything; in particular,\n"
cannam@258 2679 ^ " expect to find project spec file in that directory\n\n");
c@219 2680 OS.Process.failure)
c@219 2681
c@219 2682 fun archive target args =
c@219 2683 case args of
c@219 2684 [] =>
c@219 2685 with_local_project USE_LOCKFILE (Archive.archive (target, []))
c@219 2686 | "--exclude"::xs =>
c@219 2687 with_local_project USE_LOCKFILE (Archive.archive (target, xs))
c@219 2688 | _ => usage ()
c@219 2689
cannam@258 2690 fun handleSystemArgs args =
cannam@258 2691 let fun handleSystemArgs' leftover args =
cannam@258 2692 case args of
cannam@258 2693 "--directory"::dir::rest =>
cannam@258 2694 (OS.FileSys.chDir dir;
cannam@258 2695 handleSystemArgs' leftover rest)
cannam@258 2696 | arg::rest =>
cannam@258 2697 handleSystemArgs' (leftover @ [arg]) rest
cannam@258 2698 | [] => leftover
cannam@258 2699 in
cannam@258 2700 OK (handleSystemArgs' [] args)
cannam@258 2701 handle e => ERROR (exnMessage e)
cannam@258 2702 end
cannam@258 2703
c@219 2704 fun repoint args =
cannam@258 2705 case handleSystemArgs args of
cannam@258 2706 ERROR e => (print ("Error: " ^ e ^ "\n");
cannam@258 2707 OS.Process.exit OS.Process.failure)
cannam@258 2708 | OK args =>
cannam@258 2709 let val return_code =
c@219 2710 case args of
c@219 2711 ["review"] => review ()
c@219 2712 | ["status"] => status ()
c@219 2713 | ["install"] => install ()
c@219 2714 | ["update"] => update ()
c@219 2715 | ["lock"] => lock ()
c@219 2716 | ["version"] => version ()
c@219 2717 | "archive"::target::args => archive target args
c@219 2718 | arg::_ => (print ("Error: unknown argument \"" ^ arg ^ "\"\n");
c@219 2719 usage ())
c@219 2720 | _ => usage ()
cannam@258 2721 in
cannam@258 2722 OS.Process.exit return_code
cannam@258 2723 end
c@219 2724
c@219 2725 fun main () =
c@219 2726 repoint (CommandLine.arguments ())