annotate repoint.sml @ 698:ee97c742d184 tip

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