comparison vext.sml @ 1752:716e13004b19

Update Vext and subrepos
author Chris Cannam
date Thu, 09 Nov 2017 15:59:18 +0000
parents ffe59b457557
children edaa018a3731
comparison
equal deleted inserted replaced
1751:5cbe4eb3d189 1752:716e13004b19
36 shall not be used in advertising or otherwise to promote the sale, 36 shall not be used in advertising or otherwise to promote the sale,
37 use or other dealings in this Software without prior written 37 use or other dealings in this Software without prior written
38 authorization. 38 authorization.
39 *) 39 *)
40 40
41 val vext_version = "0.9.9" 41 val vext_version = "0.9.91"
42 42
43 43
44 datatype vcs = 44 datatype vcs =
45 HG | 45 HG |
46 GIT 46 GIT
156 156
157 (** Test whether the library is at the newest revision for the 157 (** Test whether the library is at the newest revision for the
158 given branch. False may indicate that the branch has advanced 158 given branch. False may indicate that the branch has advanced
159 or that the library is not on the branch at all. This function 159 or that the library is not on the branch at all. This function
160 may use the network to check for new revisions *) 160 may use the network to check for new revisions *)
161 val is_newest : context -> libname * branch -> bool result 161 val is_newest : context -> libname * source * branch -> bool result
162 162
163 (** Test whether the library is at the newest revision available 163 (** Test whether the library is at the newest revision available
164 locally for the given branch. False may indicate that the 164 locally for the given branch. False may indicate that the
165 branch has advanced or that the library is not on the branch 165 branch has advanced or that the library is not on the branch
166 at all. This function must not use the network *) 166 at all. This function must not use the network *)
173 (** Check out, i.e. clone a fresh copy of, the repo for the given 173 (** Check out, i.e. clone a fresh copy of, the repo for the given
174 library on the given branch *) 174 library on the given branch *)
175 val checkout : context -> libname * source * branch -> unit result 175 val checkout : context -> libname * source * branch -> unit result
176 176
177 (** Update the library to the given branch tip *) 177 (** Update the library to the given branch tip *)
178 val update : context -> libname * branch -> id_or_tag result 178 val update : context -> libname * source * branch -> id_or_tag result
179 179
180 (** Update the library to the given specific id or tag *) 180 (** Update the library to the given specific id or tag *)
181 val update_to : context -> libname * id_or_tag -> id_or_tag result 181 val update_to : context -> libname * source * id_or_tag -> id_or_tag result
182 end 182 end
183 183
184 signature LIB_CONTROL = sig 184 signature LIB_CONTROL = sig
185 val review : context -> libspec -> (libstate * localstate) result 185 val review : context -> libspec -> (libstate * localstate) result
186 val status : context -> libspec -> (libstate * localstate) result 186 val status : context -> libspec -> (libstate * localstate) result
208 case OS.Process.getEnv "VEXT_VERBOSE" of 208 case OS.Process.getEnv "VEXT_VERBOSE" of
209 SOME "0" => false 209 SOME "0" => false
210 | SOME _ => true 210 | SOME _ => true
211 | NONE => false 211 | NONE => false
212 212
213 fun split_relative path desc =
214 case OS.Path.fromString path of
215 { isAbs = true, ... } => raise Fail (desc ^ " may not be absolute")
216 | { arcs, ... } => arcs
217
213 fun extpath ({ rootpath, extdir, ... } : context) = 218 fun extpath ({ rootpath, extdir, ... } : context) =
214 let val { isAbs, vol, arcs } = OS.Path.fromString rootpath 219 let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
215 in OS.Path.toString { 220 in OS.Path.toString {
216 isAbs = isAbs, 221 isAbs = isAbs,
217 vol = vol, 222 vol = vol,
218 arcs = arcs @ [ extdir ] 223 arcs = arcs @
224 split_relative extdir "extdir"
219 } 225 }
220 end 226 end
221 227
222 fun subpath ({ rootpath, extdir, ... } : context) libname remainder = 228 fun subpath ({ rootpath, extdir, ... } : context) libname remainder =
223 (* NB libname is allowed to be a path fragment, e.g. foo/bar *) 229 (* NB libname is allowed to be a path fragment, e.g. foo/bar *)
224 let val { isAbs, vol, arcs } = OS.Path.fromString rootpath 230 let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
225 val split = String.fields (fn c => c = #"/")
226 in OS.Path.toString { 231 in OS.Path.toString {
227 isAbs = isAbs, 232 isAbs = isAbs,
228 vol = vol, 233 vol = vol,
229 arcs = arcs @ [ extdir ] @ split libname @ split remainder 234 arcs = arcs @
235 split_relative extdir "extdir" @
236 split_relative libname "library path" @
237 split_relative remainder "subpath"
230 } 238 }
231 end 239 end
232 240
233 fun libpath context "" = 241 fun libpath context "" =
234 extpath context 242 extpath context
383 | OK () => ((OS.FileSys.mkDir path; OK ()) 391 | OK () => ((OS.FileSys.mkDir path; OK ())
384 handle OS.SysErr (e, _) => 392 handle OS.SysErr (e, _) =>
385 ERROR ("Directory creation failed: " ^ e)) 393 ERROR ("Directory creation failed: " ^ e))
386 394
387 fun mkpath path = 395 fun mkpath path =
388 (* strip any trailing '/', something isDir doesn't always handle *) 396 mkpath' (OS.Path.mkCanonical path)
389 case rev (explode path) of 397
390 #"/"::rest => mkpath (implode (rev rest)) 398 fun rmpath' path =
391 | _ => mkpath' path
392
393 fun rmpath path =
394 let open OS 399 let open OS
395 fun files_from dirstream = 400 fun files_from dirstream =
396 case FileSys.readDir dirstream of 401 case FileSys.readDir dirstream of
397 NONE => [] 402 NONE => []
398 | SOME file => 403 | SOME file =>
417 else FileSys.remove path 422 else FileSys.remove path
418 in 423 in
419 (remove path; OK ()) 424 (remove path; OK ())
420 handle SysErr (e, _) => ERROR ("Path removal failed: " ^ e) 425 handle SysErr (e, _) => ERROR ("Path removal failed: " ^ e)
421 end 426 end
427
428 fun rmpath path =
429 rmpath' (OS.Path.mkCanonical path)
430
422 end 431 end
423 432
424 functor LibControlFn (V: VCS_CONTROL) :> LIB_CONTROL = struct 433 functor LibControlFn (V: VCS_CONTROL) :> LIB_CONTROL = struct
425 434
426 (* Valid states for unpinned libraries: 435 (* Valid states for unpinned libraries:
447 456
448 - ABSENT: Repo doesn't exist here at all. 457 - ABSENT: Repo doesn't exist here at all.
449 *) 458 *)
450 459
451 fun check with_network context 460 fun check with_network context
452 ({ libname, branch, project_pin, lock_pin, ... } : libspec) = 461 ({ libname, source, branch,
462 project_pin, lock_pin, ... } : libspec) =
453 let fun check_unpinned () = 463 let fun check_unpinned () =
454 let val is_newest = if with_network 464 let val newest =
455 then V.is_newest 465 if with_network
456 else V.is_newest_locally 466 then V.is_newest context (libname, source, branch)
467 else V.is_newest_locally context (libname, branch)
457 in 468 in
458 case is_newest context (libname, branch) of 469 case newest of
459 ERROR e => ERROR e 470 ERROR e => ERROR e
460 | OK true => OK CORRECT 471 | OK true => OK CORRECT
461 | OK false => 472 | OK false =>
462 case V.is_on_branch context (libname, branch) of 473 case V.is_on_branch context (libname, branch) of
463 ERROR e => ERROR e 474 ERROR e => ERROR e
501 512
502 fun update context 513 fun update context
503 ({ libname, source, branch, 514 ({ libname, source, branch,
504 project_pin, lock_pin, ... } : libspec) = 515 project_pin, lock_pin, ... } : libspec) =
505 let fun update_unpinned () = 516 let fun update_unpinned () =
506 case V.is_newest context (libname, branch) of 517 case V.is_newest context (libname, source, branch) of
507 ERROR e => ERROR e 518 ERROR e => ERROR e
508 | OK true => V.id_of context libname 519 | OK true => V.id_of context libname
509 | OK false => V.update context (libname, branch) 520 | OK false => V.update context (libname, source, branch)
510 fun update_pinned target = 521 fun update_pinned target =
511 case V.is_at context (libname, target) of 522 case V.is_at context (libname, target) of
512 ERROR e => ERROR e 523 ERROR e => ERROR e
513 | OK true => OK target 524 | OK true => OK target
514 | OK false => V.update_to context (libname, target) 525 | OK false => V.update_to context (libname, source, target)
515 fun update' () = 526 fun update' () =
516 case lock_pin of 527 case lock_pin of
517 PINNED target => update_pinned target 528 PINNED target => update_pinned target
518 | UNPINNED => 529 | UNPINNED =>
519 case project_pin of 530 case project_pin of
1154 (login_for context service) 1165 (login_for context service)
1155 (#providers context) 1166 (#providers context)
1156 end 1167 end
1157 1168
1158 structure HgControl :> VCS_CONTROL = struct 1169 structure HgControl :> VCS_CONTROL = struct
1159 1170
1171 (* Pulls always use an explicit URL, never just the default
1172 remote, in order to ensure we update properly if the location
1173 given in the project file changes. *)
1174
1160 type vcsstate = { id: string, modified: bool, 1175 type vcsstate = { id: string, modified: bool,
1161 branch: string, tags: string list } 1176 branch: string, tags: string list }
1162 1177
1163 val hg_args = [ "--config", "ui.interactive=true" ] 1178 val hg_args = [ "--config", "ui.interactive=true" ]
1164 1179
1231 fun is_newest_locally context (libname, branch) = 1246 fun is_newest_locally context (libname, branch) =
1232 case hg_command_output context libname 1247 case hg_command_output context libname
1233 ["log", "-l1", 1248 ["log", "-l1",
1234 "-b", branch_name branch, 1249 "-b", branch_name branch,
1235 "--template", "{node}"] of 1250 "--template", "{node}"] of
1236 ERROR e => ERROR e 1251 ERROR e => OK false (* desired branch does not exist *)
1237 | OK newest_in_repo => is_at context (libname, newest_in_repo) 1252 | OK newest_in_repo => is_at context (libname, newest_in_repo)
1238 1253
1239 fun pull context libname = 1254 fun pull context (libname, source) =
1240 hg_command context libname 1255 let val url = remote_for context (libname, source)
1241 (if FileBits.verbose () 1256 in
1242 then ["pull"] 1257 hg_command context libname
1243 else ["pull", "-q"]) 1258 (if FileBits.verbose ()
1244 1259 then ["pull", url]
1245 fun is_newest context (libname, branch) = 1260 else ["pull", "-q", url])
1261 end
1262
1263 fun is_newest context (libname, source, branch) =
1246 case is_newest_locally context (libname, branch) of 1264 case is_newest_locally context (libname, branch) of
1247 ERROR e => ERROR e 1265 ERROR e => ERROR e
1248 | OK false => OK false 1266 | OK false => OK false
1249 | OK true => 1267 | OK true =>
1250 case pull context libname of 1268 case pull context (libname, source) of
1251 ERROR e => ERROR e 1269 ERROR e => ERROR e
1252 | _ => is_newest_locally context (libname, branch) 1270 | _ => is_newest_locally context (libname, branch)
1253 1271
1254 fun is_modified_locally context libname = 1272 fun is_modified_locally context libname =
1255 case current_state context libname of 1273 case current_state context libname of
1267 | _ => hg_command context "" 1285 | _ => hg_command context ""
1268 ["clone", "-u", branch_name branch, 1286 ["clone", "-u", branch_name branch,
1269 url, libname] 1287 url, libname]
1270 end 1288 end
1271 1289
1272 fun update context (libname, branch) = 1290 fun update context (libname, source, branch) =
1273 let val pull_result = pull context libname 1291 let val pull_result = pull context (libname, source)
1274 in 1292 in
1275 case hg_command context libname ["update", branch_name branch] of 1293 case hg_command context libname ["update", branch_name branch] of
1276 ERROR e => ERROR e 1294 ERROR e => ERROR e
1277 | _ => 1295 | _ =>
1278 case pull_result of 1296 case pull_result of
1279 ERROR e => ERROR e 1297 ERROR e => ERROR e
1280 | _ => id_of context libname 1298 | _ => id_of context libname
1281 end 1299 end
1282 1300
1283 fun update_to context (libname, "") = 1301 fun update_to context (libname, _, "") =
1284 ERROR "Non-empty id (tag or revision id) required for update_to" 1302 ERROR "Non-empty id (tag or revision id) required for update_to"
1285 | update_to context (libname, id) = 1303 | update_to context (libname, source, id) =
1286 let val pull_result = pull context libname 1304 let val pull_result = pull context (libname, source)
1287 in 1305 in
1288 case hg_command context libname ["update", "-r", id] of 1306 case hg_command context libname ["update", "-r", id] of
1289 OK _ => id_of context libname 1307 OK _ => id_of context libname
1290 | ERROR e => 1308 | ERROR e =>
1291 case pull_result of 1309 case pull_result of
1296 end 1314 end
1297 1315
1298 structure GitControl :> VCS_CONTROL = struct 1316 structure GitControl :> VCS_CONTROL = struct
1299 1317
1300 (* With Git repos we always operate in detached HEAD state. Even 1318 (* With Git repos we always operate in detached HEAD state. Even
1301 the master branch is checked out using the remote reference, 1319 the master branch is checked out using a remote reference
1302 origin/master. *) 1320 (vext/master). The remote we use is always named vext, and we
1321 update it to the expected URL each time we fetch, in order to
1322 ensure we update properly if the location given in the project
1323 file changes. The origin remote is unused. *)
1303 1324
1304 fun git_command context libname args = 1325 fun git_command context libname args =
1305 FileBits.command context libname ("git" :: args) 1326 FileBits.command context libname ("git" :: args)
1306 1327
1307 fun git_command_output context libname args = 1328 fun git_command_output context libname args =
1317 fun branch_name branch = case branch of 1338 fun branch_name branch = case branch of
1318 DEFAULT_BRANCH => "master" 1339 DEFAULT_BRANCH => "master"
1319 | BRANCH "" => "master" 1340 | BRANCH "" => "master"
1320 | BRANCH b => b 1341 | BRANCH b => b
1321 1342
1322 fun remote_branch_name branch = "origin/" ^ branch_name branch 1343 val our_remote = "vext"
1344
1345 fun remote_branch_name branch = our_remote ^ "/" ^ branch_name branch
1323 1346
1324 fun checkout context (libname, source, branch) = 1347 fun checkout context (libname, source, branch) =
1325 let val url = remote_for context (libname, source) 1348 let val url = remote_for context (libname, source)
1326 in 1349 in
1327 (* make the lib dir rather than just the ext dir, since 1350 (* make the lib dir rather than just the ext dir, since
1328 the lib dir might be nested and git will happily check 1351 the lib dir might be nested and git will happily check
1329 out into an existing empty dir anyway *) 1352 out into an existing empty dir anyway *)
1330 case FileBits.mkpath (FileBits.libpath context libname) of 1353 case FileBits.mkpath (FileBits.libpath context libname) of
1331 OK () => git_command context "" 1354 OK () => git_command context ""
1332 ["clone", "-b", 1355 ["clone", "--origin", our_remote,
1333 branch_name branch, 1356 "--branch", branch_name branch,
1334 url, libname] 1357 url, libname]
1335 | ERROR e => ERROR e 1358 | ERROR e => ERROR e
1359 end
1360
1361 fun add_our_remote context (libname, source) =
1362 (* When we do the checkout ourselves (above), we add the
1363 remote at the same time. But if the repo was cloned by
1364 someone else, we'll need to do it after the fact. Git
1365 doesn't seem to have a means to add a remote or change its
1366 url if it already exists; seems we have to do this: *)
1367 let val url = remote_for context (libname, source)
1368 in
1369 case git_command context libname
1370 ["remote", "set-url", our_remote, url] of
1371 OK () => OK ()
1372 | ERROR e => git_command context libname
1373 ["remote", "add", "-f", our_remote, url]
1336 end 1374 end
1337 1375
1338 (* NB git rev-parse HEAD shows revision id of current checkout; 1376 (* NB git rev-parse HEAD shows revision id of current checkout;
1339 git rev-list -1 <tag> shows revision id of revision with that tag *) 1377 git rev-list -1 <tag> shows revision id of revision with that tag *)
1340 1378
1341 fun id_of context libname = 1379 fun id_of context libname =
1342 git_command_output context libname ["rev-parse", "HEAD"] 1380 git_command_output context libname ["rev-parse", "HEAD"]
1343 1381
1344 fun is_at context (libname, id_or_tag) = 1382 fun is_at context (libname, id_or_tag) =
1345 case id_of context libname of 1383 case id_of context libname of
1346 ERROR e => ERROR e 1384 ERROR e => OK false (* HEAD nonexistent, expected in empty repo *)
1347 | OK id => 1385 | OK id =>
1348 if String.isPrefix id_or_tag id orelse 1386 if String.isPrefix id_or_tag id orelse
1349 String.isPrefix id id_or_tag 1387 String.isPrefix id id_or_tag
1350 then OK true 1388 then OK true
1351 else 1389 else
1352 case git_command_output context libname 1390 case git_command_output context libname
1353 ["show-ref", 1391 ["show-ref",
1354 "refs/tags/" ^ id_or_tag] of 1392 "refs/tags/" ^ id_or_tag,
1393 "--"] of
1355 OK "" => OK false 1394 OK "" => OK false
1356 | ERROR _ => OK false 1395 | ERROR _ => OK false
1357 | OK s => OK (id = hd (String.tokens (fn c => c = #" ") s)) 1396 | OK s => OK (id = hd (String.tokens (fn c => c = #" ") s))
1358 1397
1359 fun branch_tip context (libname, branch) = 1398 fun branch_tip context (libname, branch) =
1399 (* We don't have access to the source info or the network
1400 here, as this is used by status (e.g. via is_on_branch) as
1401 well as review. It's possible the remote branch won't exist,
1402 e.g. if the repo was checked out by something other than
1403 Vext, and if that's the case, we can't add it here; we'll
1404 just have to fail, since checking against local branches
1405 instead could produce the wrong result. *)
1360 git_command_output context libname 1406 git_command_output context libname
1361 ["rev-list", "-1", 1407 ["rev-list", "-1",
1362 remote_branch_name branch] 1408 remote_branch_name branch, "--"]
1363 1409
1364 fun is_newest_locally context (libname, branch) = 1410 fun is_newest_locally context (libname, branch) =
1365 case branch_tip context (libname, branch) of 1411 case branch_tip context (libname, branch) of
1366 ERROR e => ERROR e 1412 ERROR e => OK false
1367 | OK rev => is_at context (libname, rev) 1413 | OK rev => is_at context (libname, rev)
1368 1414
1369 fun is_on_branch context (libname, branch) = 1415 fun is_on_branch context (libname, branch) =
1370 case branch_tip context (libname, branch) of 1416 case branch_tip context (libname, branch) of
1371 ERROR e => ERROR e 1417 ERROR e => OK false
1372 | OK rev => 1418 | OK rev =>
1373 case is_at context (libname, rev) of 1419 case is_at context (libname, rev) of
1374 ERROR e => ERROR e 1420 ERROR e => ERROR e
1375 | OK true => OK true 1421 | OK true => OK true
1376 | OK false => 1422 | OK false =>
1378 ["merge-base", "--is-ancestor", 1424 ["merge-base", "--is-ancestor",
1379 "HEAD", remote_branch_name branch] of 1425 "HEAD", remote_branch_name branch] of
1380 ERROR e => OK false (* cmd returns non-zero for no *) 1426 ERROR e => OK false (* cmd returns non-zero for no *)
1381 | _ => OK true 1427 | _ => OK true
1382 1428
1383 fun is_newest context (libname, branch) = 1429 fun fetch context (libname, source) =
1384 case is_newest_locally context (libname, branch) of 1430 case add_our_remote context (libname, source) of
1385 ERROR e => ERROR e 1431 ERROR e => ERROR e
1386 | OK false => OK false 1432 | _ => git_command context libname ["fetch", our_remote]
1387 | OK true => 1433
1388 case git_command context libname ["fetch"] of 1434 fun is_newest context (libname, source, branch) =
1435 case add_our_remote context (libname, source) of
1436 ERROR e => ERROR e
1437 | OK () =>
1438 case is_newest_locally context (libname, branch) of
1389 ERROR e => ERROR e 1439 ERROR e => ERROR e
1390 | _ => is_newest_locally context (libname, branch) 1440 | OK false => OK false
1441 | OK true =>
1442 case fetch context (libname, source) of
1443 ERROR e => ERROR e
1444 | _ => is_newest_locally context (libname, branch)
1391 1445
1392 fun is_modified_locally context libname = 1446 fun is_modified_locally context libname =
1393 case git_command_output context libname ["status", "--porcelain"] of 1447 case git_command_output context libname ["status", "--porcelain"] of
1394 ERROR e => ERROR e 1448 ERROR e => ERROR e
1395 | OK "" => OK false 1449 | OK "" => OK false
1400 branch, as that will succeed even if the branch isn't up to 1454 branch, as that will succeed even if the branch isn't up to
1401 date. We could checkout the branch and then fetch and merge, 1455 date. We could checkout the branch and then fetch and merge,
1402 but it's perhaps cleaner not to maintain a local branch at all, 1456 but it's perhaps cleaner not to maintain a local branch at all,
1403 but instead checkout the remote branch as a detached head. *) 1457 but instead checkout the remote branch as a detached head. *)
1404 1458
1405 fun update context (libname, branch) = 1459 fun update context (libname, source, branch) =
1406 case git_command context libname ["fetch"] of 1460 case fetch context (libname, source) of
1407 ERROR e => ERROR e 1461 ERROR e => ERROR e
1408 | _ => 1462 | _ =>
1409 case git_command context libname ["checkout", "--detach", 1463 case git_command context libname ["checkout", "--detach",
1410 remote_branch_name branch] of 1464 remote_branch_name branch] of
1411 ERROR e => ERROR e 1465 ERROR e => ERROR e
1417 attempt the fetch first, though, purely in order to avoid ugly 1471 attempt the fetch first, though, purely in order to avoid ugly
1418 error messages in the common case where we're being asked to 1472 error messages in the common case where we're being asked to
1419 update to a new pin (from the lock file) that hasn't been 1473 update to a new pin (from the lock file) that hasn't been
1420 fetched yet. *) 1474 fetched yet. *)
1421 1475
1422 fun update_to context (libname, "") = 1476 fun update_to context (libname, _, "") =
1423 ERROR "Non-empty id (tag or revision id) required for update_to" 1477 ERROR "Non-empty id (tag or revision id) required for update_to"
1424 | update_to context (libname, id) = 1478 | update_to context (libname, source, id) =
1425 let val fetch_result = git_command context libname ["fetch"] 1479 let val fetch_result = fetch context (libname, source)
1426 in 1480 in
1427 case git_command context libname ["checkout", "--detach", id] of 1481 case git_command context libname ["checkout", "--detach", id] of
1428 OK _ => id_of context libname 1482 OK _ => id_of context libname
1429 | ERROR e => 1483 | ERROR e =>
1430 case fetch_result of 1484 case fetch_result of