annotate repoint.sml @ 56:5e9027b32179 tip master

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