annotate repoint.sml @ 2309:50ff14c0b1e5

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