annotate repoint.sml @ 14:44b86c346a5a perf

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