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