annotate repoint.sml @ 64:0c94d3065ecd

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