Chris@303
|
1 (* This file is automatically generated from the individual
|
Chris@303
|
2 source files in the Vext repository. *)
|
Chris@303
|
3
|
Chris@303
|
4 (*
|
Chris@303
|
5 Vext
|
Chris@303
|
6
|
Chris@303
|
7 A simple manager for third-party source code dependencies
|
Chris@303
|
8
|
Chris@303
|
9 Copyright 2017 Chris Cannam.
|
Chris@303
|
10
|
Chris@303
|
11 Permission is hereby granted, free of charge, to any person
|
Chris@303
|
12 obtaining a copy of this software and associated documentation
|
Chris@303
|
13 files (the "Software"), to deal in the Software without
|
Chris@303
|
14 restriction, including without limitation the rights to use, copy,
|
Chris@303
|
15 modify, merge, publish, distribute, sublicense, and/or sell copies
|
Chris@303
|
16 of the Software, and to permit persons to whom the Software is
|
Chris@303
|
17 furnished to do so, subject to the following conditions:
|
Chris@303
|
18
|
Chris@303
|
19 The above copyright notice and this permission notice shall be
|
Chris@303
|
20 included in all copies or substantial portions of the Software.
|
Chris@303
|
21
|
Chris@303
|
22 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
Chris@303
|
23 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
Chris@303
|
24 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
Chris@303
|
25 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
|
Chris@303
|
26 ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
Chris@303
|
27 CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
Chris@303
|
28 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
Chris@303
|
29
|
Chris@303
|
30 Except as contained in this notice, the names of Chris Cannam and
|
Chris@303
|
31 Particular Programs Ltd shall not be used in advertising or
|
Chris@303
|
32 otherwise to promote the sale, use or other dealings in this
|
Chris@303
|
33 Software without prior written authorization.
|
Chris@303
|
34 *)
|
Chris@303
|
35
|
Chris@303
|
36 val vext_version = "0.9.4"
|
Chris@303
|
37
|
Chris@303
|
38
|
Chris@303
|
39 datatype vcs =
|
Chris@303
|
40 HG |
|
Chris@303
|
41 GIT
|
Chris@303
|
42
|
Chris@303
|
43 datatype source =
|
Chris@303
|
44 URL_SOURCE of string |
|
Chris@303
|
45 SERVICE_SOURCE of {
|
Chris@303
|
46 service : string,
|
Chris@303
|
47 owner : string option,
|
Chris@303
|
48 repo : string option
|
Chris@303
|
49 }
|
Chris@303
|
50
|
Chris@303
|
51 datatype pin =
|
Chris@303
|
52 UNPINNED |
|
Chris@303
|
53 PINNED of string
|
Chris@303
|
54
|
Chris@303
|
55 datatype libstate =
|
Chris@303
|
56 ABSENT |
|
Chris@303
|
57 CORRECT |
|
Chris@303
|
58 SUPERSEDED |
|
Chris@303
|
59 WRONG
|
Chris@303
|
60
|
Chris@303
|
61 datatype localstate =
|
Chris@303
|
62 MODIFIED |
|
Chris@303
|
63 UNMODIFIED
|
Chris@303
|
64
|
Chris@303
|
65 datatype branch =
|
Chris@303
|
66 BRANCH of string |
|
Chris@303
|
67 DEFAULT_BRANCH
|
Chris@303
|
68
|
Chris@303
|
69 (* If we can recover from an error, for example by reporting failure
|
Chris@303
|
70 for this one thing and going on to the next thing, then the error
|
Chris@303
|
71 should usually be returned through a result type rather than an
|
Chris@303
|
72 exception. *)
|
Chris@303
|
73
|
Chris@303
|
74 datatype 'a result =
|
Chris@303
|
75 OK of 'a |
|
Chris@303
|
76 ERROR of string
|
Chris@303
|
77
|
Chris@303
|
78 type libname = string
|
Chris@303
|
79
|
Chris@303
|
80 type id_or_tag = string
|
Chris@303
|
81
|
Chris@303
|
82 type libspec = {
|
Chris@303
|
83 libname : libname,
|
Chris@303
|
84 vcs : vcs,
|
Chris@303
|
85 source : source,
|
Chris@303
|
86 branch : branch,
|
Chris@303
|
87 pin : pin
|
Chris@303
|
88 }
|
Chris@303
|
89
|
Chris@303
|
90 type lock = {
|
Chris@303
|
91 libname : libname,
|
Chris@303
|
92 id_or_tag : id_or_tag
|
Chris@303
|
93 }
|
Chris@303
|
94
|
Chris@303
|
95 type remote_spec = {
|
Chris@303
|
96 anon : string option,
|
Chris@303
|
97 auth : string option
|
Chris@303
|
98 }
|
Chris@303
|
99
|
Chris@303
|
100 type provider = {
|
Chris@303
|
101 service : string,
|
Chris@303
|
102 supports : vcs list,
|
Chris@303
|
103 remote_spec : remote_spec
|
Chris@303
|
104 }
|
Chris@303
|
105
|
Chris@303
|
106 type account = {
|
Chris@303
|
107 service : string,
|
Chris@303
|
108 login : string
|
Chris@303
|
109 }
|
Chris@303
|
110
|
Chris@303
|
111 type context = {
|
Chris@303
|
112 rootpath : string,
|
Chris@303
|
113 extdir : string,
|
Chris@303
|
114 providers : provider list,
|
Chris@303
|
115 accounts : account list
|
Chris@303
|
116 }
|
Chris@303
|
117
|
Chris@303
|
118 type userconfig = {
|
Chris@303
|
119 providers : provider list,
|
Chris@303
|
120 accounts : account list
|
Chris@303
|
121 }
|
Chris@303
|
122
|
Chris@303
|
123 type project = {
|
Chris@303
|
124 context : context,
|
Chris@303
|
125 libs : libspec list
|
Chris@303
|
126 }
|
Chris@303
|
127
|
Chris@303
|
128 structure VextFilenames = struct
|
Chris@303
|
129 val project_file = "vext-project.json"
|
Chris@303
|
130 val project_lock_file = "vext-lock.json"
|
Chris@303
|
131 val user_config_file = ".vext.json"
|
Chris@303
|
132 end
|
Chris@303
|
133
|
Chris@303
|
134 signature VCS_CONTROL = sig
|
Chris@303
|
135
|
Chris@303
|
136 (** Test whether the library is present locally at all *)
|
Chris@303
|
137 val exists : context -> libname -> bool result
|
Chris@303
|
138
|
Chris@303
|
139 (** Return the id (hash) of the current revision for the library *)
|
Chris@303
|
140 val id_of : context -> libname -> id_or_tag result
|
Chris@303
|
141
|
Chris@303
|
142 (** Test whether the library is at the given id *)
|
Chris@303
|
143 val is_at : context -> libname * id_or_tag -> bool result
|
Chris@303
|
144
|
Chris@303
|
145 (** Test whether the library is on the given branch, i.e. is at
|
Chris@303
|
146 the branch tip or an ancestor of it *)
|
Chris@303
|
147 val is_on_branch : context -> libname * branch -> bool result
|
Chris@303
|
148
|
Chris@303
|
149 (** Test whether the library is at the newest revision for the
|
Chris@303
|
150 given branch. False may indicate that the branch has advanced
|
Chris@303
|
151 or that the library is not on the branch at all. This function
|
Chris@303
|
152 may use the network to check for new revisions *)
|
Chris@303
|
153 val is_newest : context -> libname * branch -> bool result
|
Chris@303
|
154
|
Chris@303
|
155 (** Test whether the library is at the newest revision available
|
Chris@303
|
156 locally for the given branch. False may indicate that the
|
Chris@303
|
157 branch has advanced or that the library is not on the branch
|
Chris@303
|
158 at all. This function must not use the network *)
|
Chris@303
|
159 val is_newest_locally : context -> libname * branch -> bool result
|
Chris@303
|
160
|
Chris@303
|
161 (** Test whether the library has been modified in the local
|
Chris@303
|
162 working copy *)
|
Chris@303
|
163 val is_modified_locally : context -> libname -> bool result
|
Chris@303
|
164
|
Chris@303
|
165 (** Check out, i.e. clone a fresh copy of, the repo for the given
|
Chris@303
|
166 library on the given branch *)
|
Chris@303
|
167 val checkout : context -> libname * source * branch -> unit result
|
Chris@303
|
168
|
Chris@303
|
169 (** Update the library to the given branch tip *)
|
Chris@303
|
170 val update : context -> libname * branch -> id_or_tag result
|
Chris@303
|
171
|
Chris@303
|
172 (** Update the library to the given specific id or tag *)
|
Chris@303
|
173 val update_to : context -> libname * id_or_tag -> id_or_tag result
|
Chris@303
|
174 end
|
Chris@303
|
175
|
Chris@303
|
176 signature LIB_CONTROL = sig
|
Chris@303
|
177 val review : context -> libspec -> (libstate * localstate) result
|
Chris@303
|
178 val status : context -> libspec -> (libstate * localstate) result
|
Chris@303
|
179 val update : context -> libspec -> id_or_tag result
|
Chris@303
|
180 end
|
Chris@303
|
181
|
Chris@303
|
182 structure FileBits :> sig
|
Chris@303
|
183 val extpath : context -> string
|
Chris@303
|
184 val libpath : context -> libname -> string
|
Chris@303
|
185 val subpath : context -> libname -> string -> string
|
Chris@303
|
186 val command_output : context -> libname -> string list -> string result
|
Chris@303
|
187 val command : context -> libname -> string list -> unit result
|
Chris@303
|
188 val file_contents : string -> string
|
Chris@303
|
189 val mydir : unit -> string
|
Chris@303
|
190 val homedir : unit -> string
|
Chris@303
|
191 val mkpath : string -> unit result
|
Chris@303
|
192 val project_spec_path : string -> string
|
Chris@303
|
193 val project_lock_path : string -> string
|
Chris@303
|
194 val verbose : unit -> bool
|
Chris@303
|
195 end = struct
|
Chris@303
|
196
|
Chris@303
|
197 fun verbose () =
|
Chris@303
|
198 case OS.Process.getEnv "VEXT_VERBOSE" of
|
Chris@303
|
199 SOME "0" => false
|
Chris@303
|
200 | SOME _ => true
|
Chris@303
|
201 | NONE => false
|
Chris@303
|
202
|
Chris@303
|
203 fun extpath ({ rootpath, extdir, ... } : context) =
|
Chris@303
|
204 let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
|
Chris@303
|
205 in OS.Path.toString {
|
Chris@303
|
206 isAbs = isAbs,
|
Chris@303
|
207 vol = vol,
|
Chris@303
|
208 arcs = arcs @ [ extdir ]
|
Chris@303
|
209 }
|
Chris@303
|
210 end
|
Chris@303
|
211
|
Chris@303
|
212 fun subpath ({ rootpath, extdir, ... } : context) libname remainder =
|
Chris@303
|
213 (* NB libname is allowed to be a path fragment, e.g. foo/bar *)
|
Chris@303
|
214 let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
|
Chris@303
|
215 val split = String.fields (fn c => c = #"/")
|
Chris@303
|
216 in OS.Path.toString {
|
Chris@303
|
217 isAbs = isAbs,
|
Chris@303
|
218 vol = vol,
|
Chris@303
|
219 arcs = arcs @ [ extdir ] @ split libname @ split remainder
|
Chris@303
|
220 }
|
Chris@303
|
221 end
|
Chris@303
|
222
|
Chris@303
|
223 fun libpath context "" =
|
Chris@303
|
224 extpath context
|
Chris@303
|
225 | libpath context libname =
|
Chris@303
|
226 subpath context libname ""
|
Chris@303
|
227
|
Chris@303
|
228 fun project_file_path rootpath filename =
|
Chris@303
|
229 let val { isAbs, vol, arcs } = OS.Path.fromString rootpath
|
Chris@303
|
230 in OS.Path.toString {
|
Chris@303
|
231 isAbs = isAbs,
|
Chris@303
|
232 vol = vol,
|
Chris@303
|
233 arcs = arcs @ [ filename ]
|
Chris@303
|
234 }
|
Chris@303
|
235 end
|
Chris@303
|
236
|
Chris@303
|
237 fun project_spec_path rootpath =
|
Chris@303
|
238 project_file_path rootpath (VextFilenames.project_file)
|
Chris@303
|
239
|
Chris@303
|
240 fun project_lock_path rootpath =
|
Chris@303
|
241 project_file_path rootpath (VextFilenames.project_lock_file)
|
Chris@303
|
242
|
Chris@303
|
243 fun trim str =
|
Chris@303
|
244 hd (String.fields (fn x => x = #"\n" orelse x = #"\r") str)
|
Chris@303
|
245
|
Chris@303
|
246 fun file_contents filename =
|
Chris@303
|
247 let val stream = TextIO.openIn filename
|
Chris@303
|
248 fun read_all str acc =
|
Chris@303
|
249 case TextIO.inputLine str of
|
Chris@303
|
250 SOME line => read_all str (trim line :: acc)
|
Chris@303
|
251 | NONE => rev acc
|
Chris@303
|
252 val contents = read_all stream []
|
Chris@303
|
253 val _ = TextIO.closeIn stream
|
Chris@303
|
254 in
|
Chris@303
|
255 String.concatWith "\n" contents
|
Chris@303
|
256 end
|
Chris@303
|
257
|
Chris@303
|
258 fun expand_commandline cmdlist =
|
Chris@303
|
259 (* We are quite [too] strict about what we accept here, except
|
Chris@303
|
260 for the first element in cmdlist which is assumed to be a
|
Chris@303
|
261 known command location rather than arbitrary user input. NB
|
Chris@303
|
262 only ASCII accepted at this point. *)
|
Chris@303
|
263 let open Char
|
Chris@303
|
264 fun quote arg =
|
Chris@303
|
265 if List.all
|
Chris@303
|
266 (fn c => isAlphaNum c orelse c = #"-" orelse c = #"_")
|
Chris@303
|
267 (explode arg)
|
Chris@303
|
268 then arg
|
Chris@303
|
269 else "\"" ^ arg ^ "\""
|
Chris@303
|
270 fun check arg =
|
Chris@303
|
271 let val valid = explode " /#:;?,._-{}@="
|
Chris@303
|
272 in
|
Chris@303
|
273 app (fn c =>
|
Chris@303
|
274 if isAlphaNum c orelse
|
Chris@303
|
275 List.exists (fn v => v = c) valid
|
Chris@303
|
276 then ()
|
Chris@303
|
277 else raise Fail ("Invalid character '" ^
|
Chris@303
|
278 (Char.toString c) ^
|
Chris@303
|
279 "' in command list"))
|
Chris@303
|
280 (explode arg);
|
Chris@303
|
281 arg
|
Chris@303
|
282 end
|
Chris@303
|
283 in
|
Chris@303
|
284 String.concatWith " "
|
Chris@303
|
285 (map quote
|
Chris@303
|
286 (hd cmdlist :: map check (tl cmdlist)))
|
Chris@303
|
287 end
|
Chris@303
|
288
|
Chris@303
|
289 val tick_cycle = ref 0
|
Chris@303
|
290 val tick_chars = Vector.fromList (map String.str (explode "|/-\\"))
|
Chris@303
|
291
|
Chris@303
|
292 fun tick libname cmdlist =
|
Chris@303
|
293 let val n = Vector.length tick_chars
|
Chris@303
|
294 fun pad_to n str =
|
Chris@303
|
295 if n <= String.size str then str
|
Chris@303
|
296 else pad_to n (str ^ " ")
|
Chris@303
|
297 val name = if libname <> "" then libname
|
Chris@303
|
298 else if cmdlist = nil then ""
|
Chris@303
|
299 else hd (rev cmdlist)
|
Chris@303
|
300 in
|
Chris@303
|
301 print (" " ^
|
Chris@303
|
302 Vector.sub(tick_chars, !tick_cycle) ^ " " ^
|
Chris@303
|
303 pad_to 24 name ^
|
Chris@303
|
304 "\r");
|
Chris@303
|
305 tick_cycle := (if !tick_cycle = n - 1 then 0 else 1 + !tick_cycle)
|
Chris@303
|
306 end
|
Chris@303
|
307
|
Chris@303
|
308 fun run_command context libname cmdlist redirect =
|
Chris@303
|
309 let open OS
|
Chris@303
|
310 val dir = libpath context libname
|
Chris@303
|
311 val cmd = expand_commandline cmdlist
|
Chris@303
|
312 val _ = if verbose ()
|
Chris@303
|
313 then print ("Running: " ^ cmd ^
|
Chris@303
|
314 " (in dir " ^ dir ^ ")...\n")
|
Chris@303
|
315 else tick libname cmdlist
|
Chris@303
|
316 val _ = FileSys.chDir dir
|
Chris@303
|
317 val status = case redirect of
|
Chris@303
|
318 NONE => Process.system cmd
|
Chris@303
|
319 | SOME file => Process.system (cmd ^ ">" ^ file)
|
Chris@303
|
320 in
|
Chris@303
|
321 if Process.isSuccess status
|
Chris@303
|
322 then OK ()
|
Chris@303
|
323 else ERROR ("Command failed: " ^ cmd ^ " (in dir " ^ dir ^ ")")
|
Chris@303
|
324 end
|
Chris@303
|
325 handle ex => ERROR ("Unable to run command: " ^ exnMessage ex)
|
Chris@303
|
326
|
Chris@303
|
327 fun command context libname cmdlist =
|
Chris@303
|
328 run_command context libname cmdlist NONE
|
Chris@303
|
329
|
Chris@303
|
330 fun command_output context libname cmdlist =
|
Chris@303
|
331 let open OS
|
Chris@303
|
332 val tmpFile = FileSys.tmpName ()
|
Chris@303
|
333 val result = run_command context libname cmdlist (SOME tmpFile)
|
Chris@303
|
334 val contents = file_contents tmpFile
|
Chris@303
|
335 in
|
Chris@303
|
336 FileSys.remove tmpFile handle _ => ();
|
Chris@303
|
337 case result of
|
Chris@303
|
338 OK () => OK contents
|
Chris@303
|
339 | ERROR e => ERROR e
|
Chris@303
|
340 end
|
Chris@303
|
341
|
Chris@303
|
342 fun mydir () =
|
Chris@303
|
343 let open OS
|
Chris@303
|
344 val { dir, file } = Path.splitDirFile (CommandLine.name ())
|
Chris@303
|
345 in
|
Chris@303
|
346 FileSys.realPath
|
Chris@303
|
347 (if Path.isAbsolute dir
|
Chris@303
|
348 then dir
|
Chris@303
|
349 else Path.concat (FileSys.getDir (), dir))
|
Chris@303
|
350 end
|
Chris@303
|
351
|
Chris@303
|
352 fun homedir () =
|
Chris@303
|
353 (* Failure is not routine, so we use an exception here *)
|
Chris@303
|
354 case (OS.Process.getEnv "HOME",
|
Chris@303
|
355 OS.Process.getEnv "HOMEPATH") of
|
Chris@303
|
356 (SOME home, _) => home
|
Chris@303
|
357 | (NONE, SOME home) => home
|
Chris@303
|
358 | (NONE, NONE) =>
|
Chris@303
|
359 raise Fail "Failed to look up home directory from environment"
|
Chris@303
|
360
|
Chris@303
|
361 fun mkpath path =
|
Chris@303
|
362 if OS.FileSys.isDir path handle _ => false
|
Chris@303
|
363 then OK ()
|
Chris@303
|
364 else case OS.Path.fromString path of
|
Chris@303
|
365 { arcs = nil, ... } => OK ()
|
Chris@303
|
366 | { isAbs = false, ... } => ERROR "mkpath requires absolute path"
|
Chris@303
|
367 | { isAbs, vol, arcs } =>
|
Chris@303
|
368 case mkpath (OS.Path.toString { (* parent *)
|
Chris@303
|
369 isAbs = isAbs,
|
Chris@303
|
370 vol = vol,
|
Chris@303
|
371 arcs = rev (tl (rev arcs)) }) of
|
Chris@303
|
372 ERROR e => ERROR e
|
Chris@303
|
373 | OK () => ((OS.FileSys.mkDir path; OK ())
|
Chris@303
|
374 handle OS.SysErr (e, _) =>
|
Chris@303
|
375 ERROR ("Directory creation failed: " ^ e))
|
Chris@303
|
376 end
|
Chris@303
|
377
|
Chris@303
|
378 functor LibControlFn (V: VCS_CONTROL) :> LIB_CONTROL = struct
|
Chris@303
|
379
|
Chris@303
|
380 (* Valid states for unpinned libraries:
|
Chris@303
|
381
|
Chris@303
|
382 - CORRECT: We are on the right branch and are up-to-date with
|
Chris@303
|
383 it as far as we can tell. (If not using the network, this
|
Chris@303
|
384 should be reported to user as "Present" rather than "Correct"
|
Chris@303
|
385 as the remote repo may have advanced without us knowing.)
|
Chris@303
|
386
|
Chris@303
|
387 - SUPERSEDED: We are on the right branch but we can see that
|
Chris@303
|
388 there is a newer revision either locally or on the remote (in
|
Chris@303
|
389 Git terms, we are at an ancestor of the desired branch tip).
|
Chris@303
|
390
|
Chris@303
|
391 - WRONG: We are on the wrong branch (in Git terms, we are not
|
Chris@303
|
392 at the desired branch tip or any ancestor of it).
|
Chris@303
|
393
|
Chris@303
|
394 - ABSENT: Repo doesn't exist here at all.
|
Chris@303
|
395
|
Chris@303
|
396 Valid states for pinned libraries:
|
Chris@303
|
397
|
Chris@303
|
398 - CORRECT: We are at the pinned revision.
|
Chris@303
|
399
|
Chris@303
|
400 - WRONG: We are at any revision other than the pinned one.
|
Chris@303
|
401
|
Chris@303
|
402 - ABSENT: Repo doesn't exist here at all.
|
Chris@303
|
403 *)
|
Chris@303
|
404
|
Chris@303
|
405 fun check with_network context ({ libname, branch, pin, ... } : libspec) =
|
Chris@303
|
406 let fun check_unpinned () =
|
Chris@303
|
407 let val is_newest = if with_network
|
Chris@303
|
408 then V.is_newest
|
Chris@303
|
409 else V.is_newest_locally
|
Chris@303
|
410 in
|
Chris@303
|
411 case is_newest context (libname, branch) of
|
Chris@303
|
412 ERROR e => ERROR e
|
Chris@303
|
413 | OK true => OK CORRECT
|
Chris@303
|
414 | OK false =>
|
Chris@303
|
415 case V.is_on_branch context (libname, branch) of
|
Chris@303
|
416 ERROR e => ERROR e
|
Chris@303
|
417 | OK true => OK SUPERSEDED
|
Chris@303
|
418 | OK false => OK WRONG
|
Chris@303
|
419 end
|
Chris@303
|
420 fun check_pinned target =
|
Chris@303
|
421 case V.is_at context (libname, target) of
|
Chris@303
|
422 ERROR e => ERROR e
|
Chris@303
|
423 | OK true => OK CORRECT
|
Chris@303
|
424 | OK false => OK WRONG
|
Chris@303
|
425 fun check' () =
|
Chris@303
|
426 case pin of
|
Chris@303
|
427 UNPINNED => check_unpinned ()
|
Chris@303
|
428 | PINNED target => check_pinned target
|
Chris@303
|
429 in
|
Chris@303
|
430 case V.exists context libname of
|
Chris@303
|
431 ERROR e => ERROR e
|
Chris@303
|
432 | OK false => OK (ABSENT, UNMODIFIED)
|
Chris@303
|
433 | OK true =>
|
Chris@303
|
434 case (check' (), V.is_modified_locally context libname) of
|
Chris@303
|
435 (ERROR e, _) => ERROR e
|
Chris@303
|
436 | (_, ERROR e) => ERROR e
|
Chris@303
|
437 | (OK state, OK true) => OK (state, MODIFIED)
|
Chris@303
|
438 | (OK state, OK false) => OK (state, UNMODIFIED)
|
Chris@303
|
439 end
|
Chris@303
|
440
|
Chris@303
|
441 val review = check true
|
Chris@303
|
442 val status = check false
|
Chris@303
|
443
|
Chris@303
|
444 fun update context ({ libname, source, branch, pin, ... } : libspec) =
|
Chris@303
|
445 let fun update_unpinned () =
|
Chris@303
|
446 case V.is_newest context (libname, branch) of
|
Chris@303
|
447 ERROR e => ERROR e
|
Chris@303
|
448 | OK true => V.id_of context libname
|
Chris@303
|
449 | OK false => V.update context (libname, branch)
|
Chris@303
|
450 fun update_pinned target =
|
Chris@303
|
451 case V.is_at context (libname, target) of
|
Chris@303
|
452 ERROR e => ERROR e
|
Chris@303
|
453 | OK true => OK target
|
Chris@303
|
454 | OK false => V.update_to context (libname, target)
|
Chris@303
|
455 fun update' () =
|
Chris@303
|
456 case pin of
|
Chris@303
|
457 UNPINNED => update_unpinned ()
|
Chris@303
|
458 | PINNED target => update_pinned target
|
Chris@303
|
459 in
|
Chris@303
|
460 case V.exists context libname of
|
Chris@303
|
461 ERROR e => ERROR e
|
Chris@303
|
462 | OK true => update' ()
|
Chris@303
|
463 | OK false =>
|
Chris@303
|
464 case V.checkout context (libname, source, branch) of
|
Chris@303
|
465 ERROR e => ERROR e
|
Chris@303
|
466 | OK () => update' ()
|
Chris@303
|
467 end
|
Chris@303
|
468 end
|
Chris@303
|
469
|
Chris@303
|
470 (* Simple Standard ML JSON parser
|
Chris@303
|
471 ==============================
|
Chris@303
|
472
|
Chris@303
|
473 https://bitbucket.org/cannam/sml-simplejson
|
Chris@303
|
474
|
Chris@303
|
475 An RFC-compliant JSON parser in one SML file with no dependency
|
Chris@303
|
476 on anything outside the Basis library. Also includes a simple
|
Chris@303
|
477 serialiser.
|
Chris@303
|
478
|
Chris@303
|
479 Tested with MLton, Poly/ML, and SML/NJ compilers.
|
Chris@303
|
480
|
Chris@303
|
481 Parser notes:
|
Chris@303
|
482
|
Chris@303
|
483 * Complies with RFC 7159, The JavaScript Object Notation (JSON)
|
Chris@303
|
484 Data Interchange Format
|
Chris@303
|
485
|
Chris@303
|
486 * Passes all of the JSONTestSuite parser accept/reject tests that
|
Chris@303
|
487 exist at the time of writing, as listed in "Parsing JSON is a
|
Chris@303
|
488 Minefield" (http://seriot.ch/parsing_json.php)
|
Chris@303
|
489
|
Chris@303
|
490 * Two-pass parser using naive exploded strings, therefore not
|
Chris@303
|
491 particularly fast and not suitable for large input files
|
Chris@303
|
492
|
Chris@303
|
493 * Only supports UTF-8 input, not UTF-16 or UTF-32. Doesn't check
|
Chris@303
|
494 that JSON strings are valid UTF-8 -- the caller must do that --
|
Chris@303
|
495 but does handle \u escapes
|
Chris@303
|
496
|
Chris@303
|
497 * Converts all numbers to type "real". If that is a 64-bit IEEE
|
Chris@303
|
498 float type (common but not guaranteed in SML) then we're pretty
|
Chris@303
|
499 standard for a JSON parser
|
Chris@303
|
500
|
Chris@303
|
501 Copyright 2017 Chris Cannam.
|
Chris@303
|
502 Parts based on the JSON parser in the Ponyo library by Phil Eaton.
|
Chris@303
|
503
|
Chris@303
|
504 Permission is hereby granted, free of charge, to any person
|
Chris@303
|
505 obtaining a copy of this software and associated documentation
|
Chris@303
|
506 files (the "Software"), to deal in the Software without
|
Chris@303
|
507 restriction, including without limitation the rights to use, copy,
|
Chris@303
|
508 modify, merge, publish, distribute, sublicense, and/or sell copies
|
Chris@303
|
509 of the Software, and to permit persons to whom the Software is
|
Chris@303
|
510 furnished to do so, subject to the following conditions:
|
Chris@303
|
511
|
Chris@303
|
512 The above copyright notice and this permission notice shall be
|
Chris@303
|
513 included in all copies or substantial portions of the Software.
|
Chris@303
|
514
|
Chris@303
|
515 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
Chris@303
|
516 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
Chris@303
|
517 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
Chris@303
|
518 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
|
Chris@303
|
519 ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
Chris@303
|
520 CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
Chris@303
|
521 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
Chris@303
|
522
|
Chris@303
|
523 Except as contained in this notice, the names of Chris Cannam and
|
Chris@303
|
524 Particular Programs Ltd shall not be used in advertising or
|
Chris@303
|
525 otherwise to promote the sale, use or other dealings in this
|
Chris@303
|
526 Software without prior written authorization.
|
Chris@303
|
527 *)
|
Chris@303
|
528
|
Chris@303
|
529 signature JSON = sig
|
Chris@303
|
530
|
Chris@303
|
531 datatype json = OBJECT of (string * json) list
|
Chris@303
|
532 | ARRAY of json list
|
Chris@303
|
533 | NUMBER of real
|
Chris@303
|
534 | STRING of string
|
Chris@303
|
535 | BOOL of bool
|
Chris@303
|
536 | NULL
|
Chris@303
|
537
|
Chris@303
|
538 datatype 'a result = OK of 'a
|
Chris@303
|
539 | ERROR of string
|
Chris@303
|
540
|
Chris@303
|
541 val parse : string -> json result
|
Chris@303
|
542 val serialise : json -> string
|
Chris@303
|
543 val serialiseIndented : json -> string
|
Chris@303
|
544
|
Chris@303
|
545 end
|
Chris@303
|
546
|
Chris@303
|
547 structure Json :> JSON = struct
|
Chris@303
|
548
|
Chris@303
|
549 datatype json = OBJECT of (string * json) list
|
Chris@303
|
550 | ARRAY of json list
|
Chris@303
|
551 | NUMBER of real
|
Chris@303
|
552 | STRING of string
|
Chris@303
|
553 | BOOL of bool
|
Chris@303
|
554 | NULL
|
Chris@303
|
555
|
Chris@303
|
556 datatype 'a result = OK of 'a
|
Chris@303
|
557 | ERROR of string
|
Chris@303
|
558
|
Chris@303
|
559 structure T = struct
|
Chris@303
|
560 datatype token = NUMBER of char list
|
Chris@303
|
561 | STRING of string
|
Chris@303
|
562 | BOOL of bool
|
Chris@303
|
563 | NULL
|
Chris@303
|
564 | CURLY_L
|
Chris@303
|
565 | CURLY_R
|
Chris@303
|
566 | SQUARE_L
|
Chris@303
|
567 | SQUARE_R
|
Chris@303
|
568 | COLON
|
Chris@303
|
569 | COMMA
|
Chris@303
|
570
|
Chris@303
|
571 fun toString t =
|
Chris@303
|
572 case t of NUMBER digits => implode digits
|
Chris@303
|
573 | STRING s => s
|
Chris@303
|
574 | BOOL b => Bool.toString b
|
Chris@303
|
575 | NULL => "null"
|
Chris@303
|
576 | CURLY_L => "{"
|
Chris@303
|
577 | CURLY_R => "}"
|
Chris@303
|
578 | SQUARE_L => "["
|
Chris@303
|
579 | SQUARE_R => "]"
|
Chris@303
|
580 | COLON => ":"
|
Chris@303
|
581 | COMMA => ","
|
Chris@303
|
582 end
|
Chris@303
|
583
|
Chris@303
|
584 fun bmpToUtf8 cp = (* convert a codepoint in Unicode BMP to utf8 bytes *)
|
Chris@303
|
585 let open Word
|
Chris@303
|
586 infix 6 orb andb >>
|
Chris@303
|
587 in
|
Chris@303
|
588 map (Char.chr o toInt)
|
Chris@303
|
589 (if cp < 0wx80 then
|
Chris@303
|
590 [cp]
|
Chris@303
|
591 else if cp < 0wx800 then
|
Chris@303
|
592 [0wxc0 orb (cp >> 0w6), 0wx80 orb (cp andb 0wx3f)]
|
Chris@303
|
593 else if cp < 0wx10000 then
|
Chris@303
|
594 [0wxe0 orb (cp >> 0w12),
|
Chris@303
|
595 0wx80 orb ((cp >> 0w6) andb 0wx3f),
|
Chris@303
|
596 0wx80 orb (cp andb 0wx3f)]
|
Chris@303
|
597 else raise Fail ("Invalid BMP point " ^ (Word.toString cp)))
|
Chris@303
|
598 end
|
Chris@303
|
599
|
Chris@303
|
600 fun error pos text = ERROR (text ^ " at character position " ^
|
Chris@303
|
601 Int.toString (pos - 1))
|
Chris@303
|
602 fun token_error pos = error pos ("Unexpected token")
|
Chris@303
|
603
|
Chris@303
|
604 fun lexNull pos acc (#"u" :: #"l" :: #"l" :: xs) =
|
Chris@303
|
605 lex (pos + 3) (T.NULL :: acc) xs
|
Chris@303
|
606 | lexNull pos acc _ = token_error pos
|
Chris@303
|
607
|
Chris@303
|
608 and lexTrue pos acc (#"r" :: #"u" :: #"e" :: xs) =
|
Chris@303
|
609 lex (pos + 3) (T.BOOL true :: acc) xs
|
Chris@303
|
610 | lexTrue pos acc _ = token_error pos
|
Chris@303
|
611
|
Chris@303
|
612 and lexFalse pos acc (#"a" :: #"l" :: #"s" :: #"e" :: xs) =
|
Chris@303
|
613 lex (pos + 4) (T.BOOL false :: acc) xs
|
Chris@303
|
614 | lexFalse pos acc _ = token_error pos
|
Chris@303
|
615
|
Chris@303
|
616 and lexChar tok pos acc xs =
|
Chris@303
|
617 lex pos (tok :: acc) xs
|
Chris@303
|
618
|
Chris@303
|
619 and lexString pos acc cc =
|
Chris@303
|
620 let datatype escaped = ESCAPED | NORMAL
|
Chris@303
|
621 fun lexString' pos text ESCAPED [] =
|
Chris@303
|
622 error pos "End of input during escape sequence"
|
Chris@303
|
623 | lexString' pos text NORMAL [] =
|
Chris@303
|
624 error pos "End of input during string"
|
Chris@303
|
625 | lexString' pos text ESCAPED (x :: xs) =
|
Chris@303
|
626 let fun esc c = lexString' (pos + 1) (c :: text) NORMAL xs
|
Chris@303
|
627 in case x of
|
Chris@303
|
628 #"\"" => esc x
|
Chris@303
|
629 | #"\\" => esc x
|
Chris@303
|
630 | #"/" => esc x
|
Chris@303
|
631 | #"b" => esc #"\b"
|
Chris@303
|
632 | #"f" => esc #"\f"
|
Chris@303
|
633 | #"n" => esc #"\n"
|
Chris@303
|
634 | #"r" => esc #"\r"
|
Chris@303
|
635 | #"t" => esc #"\t"
|
Chris@303
|
636 | _ => error pos ("Invalid escape \\" ^
|
Chris@303
|
637 Char.toString x)
|
Chris@303
|
638 end
|
Chris@303
|
639 | lexString' pos text NORMAL (#"\\" :: #"u" ::a::b::c::d:: xs) =
|
Chris@303
|
640 if List.all Char.isHexDigit [a,b,c,d]
|
Chris@303
|
641 then case Word.fromString ("0wx" ^ (implode [a,b,c,d])) of
|
Chris@303
|
642 SOME w => (let val utf = rev (bmpToUtf8 w) in
|
Chris@303
|
643 lexString' (pos + 6) (utf @ text)
|
Chris@303
|
644 NORMAL xs
|
Chris@303
|
645 end
|
Chris@303
|
646 handle Fail err => error pos err)
|
Chris@303
|
647 | NONE => error pos "Invalid Unicode BMP escape sequence"
|
Chris@303
|
648 else error pos "Invalid Unicode BMP escape sequence"
|
Chris@303
|
649 | lexString' pos text NORMAL (x :: xs) =
|
Chris@303
|
650 if Char.ord x < 0x20
|
Chris@303
|
651 then error pos "Invalid unescaped control character"
|
Chris@303
|
652 else
|
Chris@303
|
653 case x of
|
Chris@303
|
654 #"\"" => OK (rev text, xs, pos + 1)
|
Chris@303
|
655 | #"\\" => lexString' (pos + 1) text ESCAPED xs
|
Chris@303
|
656 | _ => lexString' (pos + 1) (x :: text) NORMAL xs
|
Chris@303
|
657 in
|
Chris@303
|
658 case lexString' pos [] NORMAL cc of
|
Chris@303
|
659 OK (text, rest, newpos) =>
|
Chris@303
|
660 lex newpos (T.STRING (implode text) :: acc) rest
|
Chris@303
|
661 | ERROR e => ERROR e
|
Chris@303
|
662 end
|
Chris@303
|
663
|
Chris@303
|
664 and lexNumber firstChar pos acc cc =
|
Chris@303
|
665 let val valid = explode ".+-e"
|
Chris@303
|
666 fun lexNumber' pos digits [] = (rev digits, [], pos)
|
Chris@303
|
667 | lexNumber' pos digits (x :: xs) =
|
Chris@303
|
668 if x = #"E" then lexNumber' (pos + 1) (#"e" :: digits) xs
|
Chris@303
|
669 else if Char.isDigit x orelse List.exists (fn c => x = c) valid
|
Chris@303
|
670 then lexNumber' (pos + 1) (x :: digits) xs
|
Chris@303
|
671 else (rev digits, x :: xs, pos)
|
Chris@303
|
672 val (digits, rest, newpos) =
|
Chris@303
|
673 lexNumber' (pos - 1) [] (firstChar :: cc)
|
Chris@303
|
674 in
|
Chris@303
|
675 case digits of
|
Chris@303
|
676 [] => token_error pos
|
Chris@303
|
677 | _ => lex newpos (T.NUMBER digits :: acc) rest
|
Chris@303
|
678 end
|
Chris@303
|
679
|
Chris@303
|
680 and lex pos acc [] = OK (rev acc)
|
Chris@303
|
681 | lex pos acc (x::xs) =
|
Chris@303
|
682 (case x of
|
Chris@303
|
683 #" " => lex
|
Chris@303
|
684 | #"\t" => lex
|
Chris@303
|
685 | #"\n" => lex
|
Chris@303
|
686 | #"\r" => lex
|
Chris@303
|
687 | #"{" => lexChar T.CURLY_L
|
Chris@303
|
688 | #"}" => lexChar T.CURLY_R
|
Chris@303
|
689 | #"[" => lexChar T.SQUARE_L
|
Chris@303
|
690 | #"]" => lexChar T.SQUARE_R
|
Chris@303
|
691 | #":" => lexChar T.COLON
|
Chris@303
|
692 | #"," => lexChar T.COMMA
|
Chris@303
|
693 | #"\"" => lexString
|
Chris@303
|
694 | #"t" => lexTrue
|
Chris@303
|
695 | #"f" => lexFalse
|
Chris@303
|
696 | #"n" => lexNull
|
Chris@303
|
697 | x => lexNumber x) (pos + 1) acc xs
|
Chris@303
|
698
|
Chris@303
|
699 fun show [] = "end of input"
|
Chris@303
|
700 | show (tok :: _) = T.toString tok
|
Chris@303
|
701
|
Chris@303
|
702 fun parseNumber digits =
|
Chris@303
|
703 (* Note lexNumber already case-insensitised the E for us *)
|
Chris@303
|
704 let open Char
|
Chris@303
|
705
|
Chris@303
|
706 fun okExpDigits [] = false
|
Chris@303
|
707 | okExpDigits (c :: []) = isDigit c
|
Chris@303
|
708 | okExpDigits (c :: cs) = isDigit c andalso okExpDigits cs
|
Chris@303
|
709
|
Chris@303
|
710 fun okExponent [] = false
|
Chris@303
|
711 | okExponent (#"+" :: cs) = okExpDigits cs
|
Chris@303
|
712 | okExponent (#"-" :: cs) = okExpDigits cs
|
Chris@303
|
713 | okExponent cc = okExpDigits cc
|
Chris@303
|
714
|
Chris@303
|
715 fun okFracTrailing [] = true
|
Chris@303
|
716 | okFracTrailing (c :: cs) =
|
Chris@303
|
717 (isDigit c andalso okFracTrailing cs) orelse
|
Chris@303
|
718 (c = #"e" andalso okExponent cs)
|
Chris@303
|
719
|
Chris@303
|
720 fun okFraction [] = false
|
Chris@303
|
721 | okFraction (c :: cs) =
|
Chris@303
|
722 isDigit c andalso okFracTrailing cs
|
Chris@303
|
723
|
Chris@303
|
724 fun okPosTrailing [] = true
|
Chris@303
|
725 | okPosTrailing (#"." :: cs) = okFraction cs
|
Chris@303
|
726 | okPosTrailing (#"e" :: cs) = okExponent cs
|
Chris@303
|
727 | okPosTrailing (c :: cs) =
|
Chris@303
|
728 isDigit c andalso okPosTrailing cs
|
Chris@303
|
729
|
Chris@303
|
730 fun okPositive [] = false
|
Chris@303
|
731 | okPositive (#"0" :: []) = true
|
Chris@303
|
732 | okPositive (#"0" :: #"." :: cs) = okFraction cs
|
Chris@303
|
733 | okPositive (#"0" :: #"e" :: cs) = okExponent cs
|
Chris@303
|
734 | okPositive (#"0" :: cs) = false
|
Chris@303
|
735 | okPositive (c :: cs) = isDigit c andalso okPosTrailing cs
|
Chris@303
|
736
|
Chris@303
|
737 fun okNumber (#"-" :: cs) = okPositive cs
|
Chris@303
|
738 | okNumber cc = okPositive cc
|
Chris@303
|
739 in
|
Chris@303
|
740 if okNumber digits
|
Chris@303
|
741 then case Real.fromString (implode digits) of
|
Chris@303
|
742 NONE => ERROR "Number out of range"
|
Chris@303
|
743 | SOME r => OK r
|
Chris@303
|
744 else ERROR ("Invalid number \"" ^ (implode digits) ^ "\"")
|
Chris@303
|
745 end
|
Chris@303
|
746
|
Chris@303
|
747 fun parseObject (T.CURLY_R :: xs) = OK (OBJECT [], xs)
|
Chris@303
|
748 | parseObject tokens =
|
Chris@303
|
749 let fun parsePair (T.STRING key :: T.COLON :: xs) =
|
Chris@303
|
750 (case parseTokens xs of
|
Chris@303
|
751 ERROR e => ERROR e
|
Chris@303
|
752 | OK (j, xs) => OK ((key, j), xs))
|
Chris@303
|
753 | parsePair other =
|
Chris@303
|
754 ERROR ("Object key/value pair expected around \"" ^
|
Chris@303
|
755 show other ^ "\"")
|
Chris@303
|
756 fun parseObject' acc [] = ERROR "End of input during object"
|
Chris@303
|
757 | parseObject' acc tokens =
|
Chris@303
|
758 case parsePair tokens of
|
Chris@303
|
759 ERROR e => ERROR e
|
Chris@303
|
760 | OK (pair, T.COMMA :: xs) =>
|
Chris@303
|
761 parseObject' (pair :: acc) xs
|
Chris@303
|
762 | OK (pair, T.CURLY_R :: xs) =>
|
Chris@303
|
763 OK (OBJECT (rev (pair :: acc)), xs)
|
Chris@303
|
764 | OK (_, _) => ERROR "Expected , or } after object element"
|
Chris@303
|
765 in
|
Chris@303
|
766 parseObject' [] tokens
|
Chris@303
|
767 end
|
Chris@303
|
768
|
Chris@303
|
769 and parseArray (T.SQUARE_R :: xs) = OK (ARRAY [], xs)
|
Chris@303
|
770 | parseArray tokens =
|
Chris@303
|
771 let fun parseArray' acc [] = ERROR "End of input during array"
|
Chris@303
|
772 | parseArray' acc tokens =
|
Chris@303
|
773 case parseTokens tokens of
|
Chris@303
|
774 ERROR e => ERROR e
|
Chris@303
|
775 | OK (j, T.COMMA :: xs) => parseArray' (j :: acc) xs
|
Chris@303
|
776 | OK (j, T.SQUARE_R :: xs) => OK (ARRAY (rev (j :: acc)), xs)
|
Chris@303
|
777 | OK (_, _) => ERROR "Expected , or ] after array element"
|
Chris@303
|
778 in
|
Chris@303
|
779 parseArray' [] tokens
|
Chris@303
|
780 end
|
Chris@303
|
781
|
Chris@303
|
782 and parseTokens [] = ERROR "Value expected"
|
Chris@303
|
783 | parseTokens (tok :: xs) =
|
Chris@303
|
784 (case tok of
|
Chris@303
|
785 T.NUMBER d => (case parseNumber d of
|
Chris@303
|
786 OK r => OK (NUMBER r, xs)
|
Chris@303
|
787 | ERROR e => ERROR e)
|
Chris@303
|
788 | T.STRING s => OK (STRING s, xs)
|
Chris@303
|
789 | T.BOOL b => OK (BOOL b, xs)
|
Chris@303
|
790 | T.NULL => OK (NULL, xs)
|
Chris@303
|
791 | T.CURLY_L => parseObject xs
|
Chris@303
|
792 | T.SQUARE_L => parseArray xs
|
Chris@303
|
793 | _ => ERROR ("Unexpected token " ^ T.toString tok ^
|
Chris@303
|
794 " before " ^ show xs))
|
Chris@303
|
795
|
Chris@303
|
796 fun parse str =
|
Chris@303
|
797 case lex 1 [] (explode str) of
|
Chris@303
|
798 ERROR e => ERROR e
|
Chris@303
|
799 | OK tokens => case parseTokens tokens of
|
Chris@303
|
800 OK (value, []) => OK value
|
Chris@303
|
801 | OK (_, _) => ERROR "Extra data after input"
|
Chris@303
|
802 | ERROR e => ERROR e
|
Chris@303
|
803
|
Chris@303
|
804 fun stringEscape s =
|
Chris@303
|
805 let fun esc x = [x, #"\\"]
|
Chris@303
|
806 fun escape' acc [] = rev acc
|
Chris@303
|
807 | escape' acc (x :: xs) =
|
Chris@303
|
808 escape' (case x of
|
Chris@303
|
809 #"\"" => esc x @ acc
|
Chris@303
|
810 | #"\\" => esc x @ acc
|
Chris@303
|
811 | #"\b" => esc #"b" @ acc
|
Chris@303
|
812 | #"\f" => esc #"f" @ acc
|
Chris@303
|
813 | #"\n" => esc #"n" @ acc
|
Chris@303
|
814 | #"\r" => esc #"r" @ acc
|
Chris@303
|
815 | #"\t" => esc #"t" @ acc
|
Chris@303
|
816 | _ =>
|
Chris@303
|
817 let val c = Char.ord x
|
Chris@303
|
818 in
|
Chris@303
|
819 if c < 0x20
|
Chris@303
|
820 then let val hex = Word.toString (Word.fromInt c)
|
Chris@303
|
821 in (rev o explode) (if c < 0x10
|
Chris@303
|
822 then ("\\u000" ^ hex)
|
Chris@303
|
823 else ("\\u00" ^ hex))
|
Chris@303
|
824 end @ acc
|
Chris@303
|
825 else
|
Chris@303
|
826 x :: acc
|
Chris@303
|
827 end)
|
Chris@303
|
828 xs
|
Chris@303
|
829 in
|
Chris@303
|
830 implode (escape' [] (explode s))
|
Chris@303
|
831 end
|
Chris@303
|
832
|
Chris@303
|
833 fun serialise json =
|
Chris@303
|
834 case json of
|
Chris@303
|
835 OBJECT pp => "{" ^ String.concatWith
|
Chris@303
|
836 "," (map (fn (key, value) =>
|
Chris@303
|
837 serialise (STRING key) ^ ":" ^
|
Chris@303
|
838 serialise value) pp) ^
|
Chris@303
|
839 "}"
|
Chris@303
|
840 | ARRAY arr => "[" ^ String.concatWith "," (map serialise arr) ^ "]"
|
Chris@303
|
841 | NUMBER n => implode (map (fn #"~" => #"-" | c => c)
|
Chris@303
|
842 (explode (Real.toString n)))
|
Chris@303
|
843 | STRING s => "\"" ^ stringEscape s ^ "\""
|
Chris@303
|
844 | BOOL b => Bool.toString b
|
Chris@303
|
845 | NULL => "null"
|
Chris@303
|
846
|
Chris@303
|
847 fun serialiseIndented json =
|
Chris@303
|
848 let fun indent 0 = ""
|
Chris@303
|
849 | indent i = " " ^ indent (i - 1)
|
Chris@303
|
850 fun serialiseIndented' i json =
|
Chris@303
|
851 let val ser = serialiseIndented' (i + 1)
|
Chris@303
|
852 in
|
Chris@303
|
853 case json of
|
Chris@303
|
854 OBJECT [] => "{}"
|
Chris@303
|
855 | ARRAY [] => "[]"
|
Chris@303
|
856 | OBJECT pp => "{\n" ^ indent (i + 1) ^
|
Chris@303
|
857 String.concatWith
|
Chris@303
|
858 (",\n" ^ indent (i + 1))
|
Chris@303
|
859 (map (fn (key, value) =>
|
Chris@303
|
860 ser (STRING key) ^ ": " ^
|
Chris@303
|
861 ser value) pp) ^
|
Chris@303
|
862 "\n" ^ indent i ^ "}"
|
Chris@303
|
863 | ARRAY arr => "[\n" ^ indent (i + 1) ^
|
Chris@303
|
864 String.concatWith
|
Chris@303
|
865 (",\n" ^ indent (i + 1))
|
Chris@303
|
866 (map ser arr) ^
|
Chris@303
|
867 "\n" ^ indent i ^ "]"
|
Chris@303
|
868 | other => serialise other
|
Chris@303
|
869 end
|
Chris@303
|
870 in
|
Chris@303
|
871 serialiseIndented' 0 json ^ "\n"
|
Chris@303
|
872 end
|
Chris@303
|
873
|
Chris@303
|
874 end
|
Chris@303
|
875
|
Chris@303
|
876
|
Chris@303
|
877 structure JsonBits :> sig
|
Chris@303
|
878 val load_json_from : string -> Json.json (* filename -> json *)
|
Chris@303
|
879 val save_json_to : string -> Json.json -> unit
|
Chris@303
|
880 val lookup_optional : Json.json -> string list -> Json.json option
|
Chris@303
|
881 val lookup_optional_string : Json.json -> string list -> string option
|
Chris@303
|
882 val lookup_mandatory : Json.json -> string list -> Json.json
|
Chris@303
|
883 val lookup_mandatory_string : Json.json -> string list -> string
|
Chris@303
|
884 end = struct
|
Chris@303
|
885
|
Chris@303
|
886 fun load_json_from filename =
|
Chris@303
|
887 case Json.parse (FileBits.file_contents filename) of
|
Chris@303
|
888 Json.OK json => json
|
Chris@303
|
889 | Json.ERROR e => raise Fail ("Failed to parse file: " ^ e)
|
Chris@303
|
890
|
Chris@303
|
891 fun save_json_to filename json =
|
Chris@303
|
892 (* using binary I/O to avoid ever writing CR/LF line endings *)
|
Chris@303
|
893 let val jstr = Json.serialiseIndented json
|
Chris@303
|
894 val stream = BinIO.openOut filename
|
Chris@303
|
895 in
|
Chris@303
|
896 BinIO.output (stream, Byte.stringToBytes jstr);
|
Chris@303
|
897 BinIO.closeOut stream
|
Chris@303
|
898 end
|
Chris@303
|
899
|
Chris@303
|
900 fun lookup_optional json kk =
|
Chris@303
|
901 let fun lookup key =
|
Chris@303
|
902 case json of
|
Chris@303
|
903 Json.OBJECT kvs =>
|
Chris@303
|
904 (case List.find (fn (k, v) => k = key) kvs of
|
Chris@303
|
905 SOME (k, v) => SOME v
|
Chris@303
|
906 | NONE => NONE)
|
Chris@303
|
907 | _ => raise Fail "Object expected"
|
Chris@303
|
908 in
|
Chris@303
|
909 case kk of
|
Chris@303
|
910 [] => NONE
|
Chris@303
|
911 | key::[] => lookup key
|
Chris@303
|
912 | key::kk => case lookup key of
|
Chris@303
|
913 NONE => NONE
|
Chris@303
|
914 | SOME j => lookup_optional j kk
|
Chris@303
|
915 end
|
Chris@303
|
916
|
Chris@303
|
917 fun lookup_optional_string json kk =
|
Chris@303
|
918 case lookup_optional json kk of
|
Chris@303
|
919 SOME (Json.STRING s) => SOME s
|
Chris@303
|
920 | SOME _ => raise Fail ("Value (if present) must be string: " ^
|
Chris@303
|
921 (String.concatWith " -> " kk))
|
Chris@303
|
922 | NONE => NONE
|
Chris@303
|
923
|
Chris@303
|
924 fun lookup_mandatory json kk =
|
Chris@303
|
925 case lookup_optional json kk of
|
Chris@303
|
926 SOME v => v
|
Chris@303
|
927 | NONE => raise Fail ("Value is mandatory: " ^
|
Chris@303
|
928 (String.concatWith " -> " kk) ^ " in json: " ^
|
Chris@303
|
929 (Json.serialise json))
|
Chris@303
|
930
|
Chris@303
|
931 fun lookup_mandatory_string json kk =
|
Chris@303
|
932 case lookup_optional json kk of
|
Chris@303
|
933 SOME (Json.STRING s) => s
|
Chris@303
|
934 | _ => raise Fail ("Value must be string: " ^
|
Chris@303
|
935 (String.concatWith " -> " kk))
|
Chris@303
|
936 end
|
Chris@303
|
937
|
Chris@303
|
938 structure Provider :> sig
|
Chris@303
|
939 val load_providers : Json.json -> provider list
|
Chris@303
|
940 val load_more_providers : provider list -> Json.json -> provider list
|
Chris@303
|
941 val remote_url : context -> vcs -> source -> libname -> string
|
Chris@303
|
942 end = struct
|
Chris@303
|
943
|
Chris@303
|
944 val known_providers : provider list =
|
Chris@303
|
945 [ {
|
Chris@303
|
946 service = "bitbucket",
|
Chris@303
|
947 supports = [HG, GIT],
|
Chris@303
|
948 remote_spec = {
|
Chris@303
|
949 anon = SOME "https://bitbucket.org/{owner}/{repository}",
|
Chris@303
|
950 auth = SOME "ssh://{vcs}@bitbucket.org/{owner}/{repository}"
|
Chris@303
|
951 }
|
Chris@303
|
952 },
|
Chris@303
|
953 {
|
Chris@303
|
954 service = "github",
|
Chris@303
|
955 supports = [GIT],
|
Chris@303
|
956 remote_spec = {
|
Chris@303
|
957 anon = SOME "https://github.com/{owner}/{repository}",
|
Chris@303
|
958 auth = SOME "ssh://{vcs}@github.com/{owner}/{repository}"
|
Chris@303
|
959 }
|
Chris@303
|
960 }
|
Chris@303
|
961 ]
|
Chris@303
|
962
|
Chris@303
|
963 fun vcs_name vcs =
|
Chris@303
|
964 case vcs of GIT => "git" |
|
Chris@303
|
965 HG => "hg"
|
Chris@303
|
966
|
Chris@303
|
967 fun vcs_from_name name =
|
Chris@303
|
968 case name of "git" => GIT
|
Chris@303
|
969 | "hg" => HG
|
Chris@303
|
970 | other => raise Fail ("Unknown vcs name \"" ^ name ^ "\"")
|
Chris@303
|
971
|
Chris@303
|
972 fun load_more_providers previously_loaded json =
|
Chris@303
|
973 let open JsonBits
|
Chris@303
|
974 fun load pjson pname : provider =
|
Chris@303
|
975 {
|
Chris@303
|
976 service = pname,
|
Chris@303
|
977 supports =
|
Chris@303
|
978 case lookup_mandatory pjson ["vcs"] of
|
Chris@303
|
979 Json.ARRAY vv =>
|
Chris@303
|
980 map (fn (Json.STRING v) => vcs_from_name v
|
Chris@303
|
981 | _ => raise Fail "Strings expected in vcs array")
|
Chris@303
|
982 vv
|
Chris@303
|
983 | _ => raise Fail "Array expected for vcs",
|
Chris@303
|
984 remote_spec = {
|
Chris@303
|
985 anon = lookup_optional_string pjson ["anonymous"],
|
Chris@303
|
986 auth = lookup_optional_string pjson ["authenticated"]
|
Chris@303
|
987 }
|
Chris@303
|
988 }
|
Chris@303
|
989 val loaded =
|
Chris@303
|
990 case lookup_optional json ["services"] of
|
Chris@303
|
991 NONE => []
|
Chris@303
|
992 | SOME (Json.OBJECT pl) => map (fn (k, v) => load v k) pl
|
Chris@303
|
993 | _ => raise Fail "Object expected for services in config"
|
Chris@303
|
994 val newly_loaded =
|
Chris@303
|
995 List.filter (fn p => not (List.exists (fn pp => #service p =
|
Chris@303
|
996 #service pp)
|
Chris@303
|
997 previously_loaded))
|
Chris@303
|
998 loaded
|
Chris@303
|
999 in
|
Chris@303
|
1000 previously_loaded @ newly_loaded
|
Chris@303
|
1001 end
|
Chris@303
|
1002
|
Chris@303
|
1003 fun load_providers json =
|
Chris@303
|
1004 load_more_providers known_providers json
|
Chris@303
|
1005
|
Chris@303
|
1006 fun expand_spec spec { vcs, service, owner, repo } login =
|
Chris@303
|
1007 (* ugly *)
|
Chris@303
|
1008 let fun replace str =
|
Chris@303
|
1009 case str of
|
Chris@303
|
1010 "vcs" => vcs_name vcs
|
Chris@303
|
1011 | "service" => service
|
Chris@303
|
1012 | "owner" =>
|
Chris@303
|
1013 (case owner of
|
Chris@303
|
1014 SOME ostr => ostr
|
Chris@303
|
1015 | NONE => raise Fail ("Owner not specified for service " ^
|
Chris@303
|
1016 service))
|
Chris@303
|
1017 | "repository" => repo
|
Chris@303
|
1018 | "account" =>
|
Chris@303
|
1019 (case login of
|
Chris@303
|
1020 SOME acc => acc
|
Chris@303
|
1021 | NONE => raise Fail ("Account not given for service " ^
|
Chris@303
|
1022 service))
|
Chris@303
|
1023 | other => raise Fail ("Unknown variable \"" ^ other ^
|
Chris@303
|
1024 "\" in spec for service " ^ service)
|
Chris@303
|
1025 fun expand' acc sstr =
|
Chris@303
|
1026 case Substring.splitl (fn c => c <> #"{") sstr of
|
Chris@303
|
1027 (pfx, sfx) =>
|
Chris@303
|
1028 if Substring.isEmpty sfx
|
Chris@303
|
1029 then rev (pfx :: acc)
|
Chris@303
|
1030 else
|
Chris@303
|
1031 case Substring.splitl (fn c => c <> #"}") sfx of
|
Chris@303
|
1032 (tok, remainder) =>
|
Chris@303
|
1033 if Substring.isEmpty remainder
|
Chris@303
|
1034 then rev (tok :: pfx :: acc)
|
Chris@303
|
1035 else let val replacement =
|
Chris@303
|
1036 replace
|
Chris@303
|
1037 (* tok begins with "{": *)
|
Chris@303
|
1038 (Substring.string
|
Chris@303
|
1039 (Substring.triml 1 tok))
|
Chris@303
|
1040 in
|
Chris@303
|
1041 expand' (Substring.full replacement ::
|
Chris@303
|
1042 pfx :: acc)
|
Chris@303
|
1043 (* remainder begins with "}": *)
|
Chris@303
|
1044 (Substring.triml 1 remainder)
|
Chris@303
|
1045 end
|
Chris@303
|
1046 in
|
Chris@303
|
1047 Substring.concat (expand' [] (Substring.full spec))
|
Chris@303
|
1048 end
|
Chris@303
|
1049
|
Chris@303
|
1050 fun provider_url req login providers =
|
Chris@303
|
1051 case providers of
|
Chris@303
|
1052 [] => raise Fail ("Unknown service \"" ^ (#service req) ^
|
Chris@303
|
1053 "\" for vcs \"" ^ (vcs_name (#vcs req)) ^ "\"")
|
Chris@303
|
1054 | ({ service, supports, remote_spec : remote_spec } :: rest) =>
|
Chris@303
|
1055 if service <> (#service req) orelse
|
Chris@303
|
1056 not (List.exists (fn v => v = (#vcs req)) supports)
|
Chris@303
|
1057 then provider_url req login rest
|
Chris@303
|
1058 else
|
Chris@303
|
1059 case (login, #auth remote_spec, #anon remote_spec) of
|
Chris@303
|
1060 (SOME _, SOME auth, _) => expand_spec auth req login
|
Chris@303
|
1061 | (SOME _, _, SOME anon) => expand_spec anon req NONE
|
Chris@303
|
1062 | (NONE, _, SOME anon) => expand_spec anon req NONE
|
Chris@303
|
1063 | _ => raise Fail ("No suitable anonymous or authenticated " ^
|
Chris@303
|
1064 "URL spec provided for service \"" ^
|
Chris@303
|
1065 service ^ "\"")
|
Chris@303
|
1066
|
Chris@303
|
1067 fun login_for ({ accounts, ... } : context) service =
|
Chris@303
|
1068 case List.find (fn a => service = #service a) accounts of
|
Chris@303
|
1069 SOME { login, ... } => SOME login
|
Chris@303
|
1070 | NONE => NONE
|
Chris@303
|
1071
|
Chris@303
|
1072 fun remote_url (context : context) vcs source libname =
|
Chris@303
|
1073 case source of
|
Chris@303
|
1074 URL_SOURCE u => u
|
Chris@303
|
1075 | SERVICE_SOURCE { service, owner, repo } =>
|
Chris@303
|
1076 provider_url { vcs = vcs,
|
Chris@303
|
1077 service = service,
|
Chris@303
|
1078 owner = owner,
|
Chris@303
|
1079 repo = case repo of
|
Chris@303
|
1080 SOME r => r
|
Chris@303
|
1081 | NONE => libname }
|
Chris@303
|
1082 (login_for context service)
|
Chris@303
|
1083 (#providers context)
|
Chris@303
|
1084 end
|
Chris@303
|
1085
|
Chris@303
|
1086 structure HgControl :> VCS_CONTROL = struct
|
Chris@303
|
1087
|
Chris@303
|
1088 type vcsstate = { id: string, modified: bool,
|
Chris@303
|
1089 branch: string, tags: string list }
|
Chris@303
|
1090
|
Chris@303
|
1091 val hg_args = [ "--config", "ui.interactive=true" ]
|
Chris@303
|
1092
|
Chris@303
|
1093 fun hg_command context libname args =
|
Chris@303
|
1094 FileBits.command context libname ("hg" :: hg_args @ args)
|
Chris@303
|
1095
|
Chris@303
|
1096 fun hg_command_output context libname args =
|
Chris@303
|
1097 FileBits.command_output context libname ("hg" :: hg_args @ args)
|
Chris@303
|
1098
|
Chris@303
|
1099 fun exists context libname =
|
Chris@303
|
1100 OK (OS.FileSys.isDir (FileBits.subpath context libname ".hg"))
|
Chris@303
|
1101 handle _ => OK false
|
Chris@303
|
1102
|
Chris@303
|
1103 fun remote_for context (libname, source) =
|
Chris@303
|
1104 Provider.remote_url context HG source libname
|
Chris@303
|
1105
|
Chris@303
|
1106 fun current_state context libname : vcsstate result =
|
Chris@303
|
1107 let fun is_branch text = text <> "" andalso #"(" = hd (explode text)
|
Chris@303
|
1108 and extract_branch b =
|
Chris@303
|
1109 if is_branch b (* need to remove enclosing parens *)
|
Chris@303
|
1110 then (implode o rev o tl o rev o tl o explode) b
|
Chris@303
|
1111 else "default"
|
Chris@303
|
1112 and is_modified id = id <> "" andalso #"+" = hd (rev (explode id))
|
Chris@303
|
1113 and extract_id id =
|
Chris@303
|
1114 if is_modified id (* need to remove trailing "+" *)
|
Chris@303
|
1115 then (implode o rev o tl o rev o explode) id
|
Chris@303
|
1116 else id
|
Chris@303
|
1117 and split_tags tags = String.tokens (fn c => c = #"/") tags
|
Chris@303
|
1118 and state_for (id, branch, tags) =
|
Chris@303
|
1119 OK { id = extract_id id,
|
Chris@303
|
1120 modified = is_modified id,
|
Chris@303
|
1121 branch = extract_branch branch,
|
Chris@303
|
1122 tags = split_tags tags }
|
Chris@303
|
1123 in
|
Chris@303
|
1124 case hg_command_output context libname ["id"] of
|
Chris@303
|
1125 ERROR e => ERROR e
|
Chris@303
|
1126 | OK out =>
|
Chris@303
|
1127 case String.tokens (fn x => x = #" ") out of
|
Chris@303
|
1128 [id, branch, tags] => state_for (id, branch, tags)
|
Chris@303
|
1129 | [id, other] => if is_branch other
|
Chris@303
|
1130 then state_for (id, other, "")
|
Chris@303
|
1131 else state_for (id, "", other)
|
Chris@303
|
1132 | [id] => state_for (id, "", "")
|
Chris@303
|
1133 | _ => ERROR ("Unexpected output from hg id: " ^ out)
|
Chris@303
|
1134 end
|
Chris@303
|
1135
|
Chris@303
|
1136 fun branch_name branch = case branch of
|
Chris@303
|
1137 DEFAULT_BRANCH => "default"
|
Chris@303
|
1138 | BRANCH "" => "default"
|
Chris@303
|
1139 | BRANCH b => b
|
Chris@303
|
1140
|
Chris@303
|
1141 fun id_of context libname =
|
Chris@303
|
1142 case current_state context libname of
|
Chris@303
|
1143 ERROR e => ERROR e
|
Chris@303
|
1144 | OK { id, ... } => OK id
|
Chris@303
|
1145
|
Chris@303
|
1146 fun is_at context (libname, id_or_tag) =
|
Chris@303
|
1147 case current_state context libname of
|
Chris@303
|
1148 ERROR e => ERROR e
|
Chris@303
|
1149 | OK { id, tags, ... } =>
|
Chris@303
|
1150 OK (String.isPrefix id_or_tag id orelse
|
Chris@303
|
1151 String.isPrefix id id_or_tag orelse
|
Chris@303
|
1152 List.exists (fn t => t = id_or_tag) tags)
|
Chris@303
|
1153
|
Chris@303
|
1154 fun is_on_branch context (libname, b) =
|
Chris@303
|
1155 case current_state context libname of
|
Chris@303
|
1156 ERROR e => ERROR e
|
Chris@303
|
1157 | OK { branch, ... } => OK (branch = branch_name b)
|
Chris@303
|
1158
|
Chris@303
|
1159 fun is_newest_locally context (libname, branch) =
|
Chris@303
|
1160 case hg_command_output context libname
|
Chris@303
|
1161 ["log", "-l1",
|
Chris@303
|
1162 "-b", branch_name branch,
|
Chris@303
|
1163 "--template", "{node}"] of
|
Chris@303
|
1164 ERROR e => ERROR e
|
Chris@303
|
1165 | OK newest_in_repo => is_at context (libname, newest_in_repo)
|
Chris@303
|
1166
|
Chris@303
|
1167 fun pull context libname =
|
Chris@303
|
1168 hg_command context libname
|
Chris@303
|
1169 (if FileBits.verbose ()
|
Chris@303
|
1170 then ["pull"]
|
Chris@303
|
1171 else ["pull", "-q"])
|
Chris@303
|
1172
|
Chris@303
|
1173 fun is_newest context (libname, branch) =
|
Chris@303
|
1174 case is_newest_locally context (libname, branch) of
|
Chris@303
|
1175 ERROR e => ERROR e
|
Chris@303
|
1176 | OK false => OK false
|
Chris@303
|
1177 | OK true =>
|
Chris@303
|
1178 case pull context libname of
|
Chris@303
|
1179 ERROR e => ERROR e
|
Chris@303
|
1180 | _ => is_newest_locally context (libname, branch)
|
Chris@303
|
1181
|
Chris@303
|
1182 fun is_modified_locally context libname =
|
Chris@303
|
1183 case current_state context libname of
|
Chris@303
|
1184 ERROR e => ERROR e
|
Chris@303
|
1185 | OK { modified, ... } => OK modified
|
Chris@303
|
1186
|
Chris@303
|
1187 fun checkout context (libname, source, branch) =
|
Chris@303
|
1188 let val url = remote_for context (libname, source)
|
Chris@303
|
1189 in
|
Chris@303
|
1190 case FileBits.mkpath (FileBits.extpath context) of
|
Chris@303
|
1191 ERROR e => ERROR e
|
Chris@303
|
1192 | _ => hg_command context ""
|
Chris@303
|
1193 ["clone", "-u", branch_name branch,
|
Chris@303
|
1194 url, libname]
|
Chris@303
|
1195 end
|
Chris@303
|
1196
|
Chris@303
|
1197 fun update context (libname, branch) =
|
Chris@303
|
1198 let val pull_result = pull context libname
|
Chris@303
|
1199 in
|
Chris@303
|
1200 case hg_command context libname ["update", branch_name branch] of
|
Chris@303
|
1201 ERROR e => ERROR e
|
Chris@303
|
1202 | _ =>
|
Chris@303
|
1203 case pull_result of
|
Chris@303
|
1204 ERROR e => ERROR e
|
Chris@303
|
1205 | _ => id_of context libname
|
Chris@303
|
1206 end
|
Chris@303
|
1207
|
Chris@303
|
1208 fun update_to context (libname, "") =
|
Chris@303
|
1209 ERROR "Non-empty id (tag or revision id) required for update_to"
|
Chris@303
|
1210 | update_to context (libname, id) =
|
Chris@303
|
1211 let val pull_result = pull context libname
|
Chris@303
|
1212 in
|
Chris@303
|
1213 case hg_command context libname ["update", "-r", id] of
|
Chris@303
|
1214 OK _ => id_of context libname
|
Chris@303
|
1215 | ERROR e =>
|
Chris@303
|
1216 case pull_result of
|
Chris@303
|
1217 ERROR e' => ERROR e' (* this was the ur-error *)
|
Chris@303
|
1218 | _ => ERROR e
|
Chris@303
|
1219 end
|
Chris@303
|
1220
|
Chris@303
|
1221 end
|
Chris@303
|
1222
|
Chris@303
|
1223 structure GitControl :> VCS_CONTROL = struct
|
Chris@303
|
1224
|
Chris@303
|
1225 (* With Git repos we always operate in detached HEAD state. Even
|
Chris@303
|
1226 the master branch is checked out using the remote reference,
|
Chris@303
|
1227 origin/master. *)
|
Chris@303
|
1228
|
Chris@303
|
1229 fun git_command context libname args =
|
Chris@303
|
1230 FileBits.command context libname ("git" :: args)
|
Chris@303
|
1231
|
Chris@303
|
1232 fun git_command_output context libname args =
|
Chris@303
|
1233 FileBits.command_output context libname ("git" :: args)
|
Chris@303
|
1234
|
Chris@303
|
1235 fun exists context libname =
|
Chris@303
|
1236 OK (OS.FileSys.isDir (FileBits.subpath context libname ".git"))
|
Chris@303
|
1237 handle _ => OK false
|
Chris@303
|
1238
|
Chris@303
|
1239 fun remote_for context (libname, source) =
|
Chris@303
|
1240 Provider.remote_url context GIT source libname
|
Chris@303
|
1241
|
Chris@303
|
1242 fun branch_name branch = case branch of
|
Chris@303
|
1243 DEFAULT_BRANCH => "master"
|
Chris@303
|
1244 | BRANCH "" => "master"
|
Chris@303
|
1245 | BRANCH b => b
|
Chris@303
|
1246
|
Chris@303
|
1247 fun remote_branch_name branch = "origin/" ^ branch_name branch
|
Chris@303
|
1248
|
Chris@303
|
1249 fun checkout context (libname, source, branch) =
|
Chris@303
|
1250 let val url = remote_for context (libname, source)
|
Chris@303
|
1251 in
|
Chris@303
|
1252 case FileBits.mkpath (FileBits.extpath context) of
|
Chris@303
|
1253 OK () => git_command context ""
|
Chris@303
|
1254 ["clone", "-b",
|
Chris@303
|
1255 branch_name branch,
|
Chris@303
|
1256 url, libname]
|
Chris@303
|
1257 | ERROR e => ERROR e
|
Chris@303
|
1258 end
|
Chris@303
|
1259
|
Chris@303
|
1260 (* NB git rev-parse HEAD shows revision id of current checkout;
|
Chris@303
|
1261 git rev-list -1 <tag> shows revision id of revision with that tag *)
|
Chris@303
|
1262
|
Chris@303
|
1263 fun id_of context libname =
|
Chris@303
|
1264 git_command_output context libname ["rev-parse", "HEAD"]
|
Chris@303
|
1265
|
Chris@303
|
1266 fun is_at context (libname, id_or_tag) =
|
Chris@303
|
1267 case id_of context libname of
|
Chris@303
|
1268 ERROR e => ERROR e
|
Chris@303
|
1269 | OK id =>
|
Chris@303
|
1270 if String.isPrefix id_or_tag id orelse
|
Chris@303
|
1271 String.isPrefix id id_or_tag
|
Chris@303
|
1272 then OK true
|
Chris@303
|
1273 else
|
Chris@303
|
1274 case git_command_output context libname
|
Chris@303
|
1275 ["show-ref",
|
Chris@303
|
1276 "refs/tags/" ^ id_or_tag] of
|
Chris@303
|
1277 OK "" => OK false
|
Chris@303
|
1278 | ERROR _ => OK false
|
Chris@303
|
1279 | OK s => OK (id = hd (String.tokens (fn c => c = #" ") s))
|
Chris@303
|
1280
|
Chris@303
|
1281 fun branch_tip context (libname, branch) =
|
Chris@303
|
1282 git_command_output context libname
|
Chris@303
|
1283 ["rev-list", "-1",
|
Chris@303
|
1284 remote_branch_name branch]
|
Chris@303
|
1285
|
Chris@303
|
1286 fun is_newest_locally context (libname, branch) =
|
Chris@303
|
1287 case branch_tip context (libname, branch) of
|
Chris@303
|
1288 ERROR e => ERROR e
|
Chris@303
|
1289 | OK rev => is_at context (libname, rev)
|
Chris@303
|
1290
|
Chris@303
|
1291 fun is_on_branch context (libname, branch) =
|
Chris@303
|
1292 case branch_tip context (libname, branch) of
|
Chris@303
|
1293 ERROR e => ERROR e
|
Chris@303
|
1294 | OK rev =>
|
Chris@303
|
1295 case is_at context (libname, rev) of
|
Chris@303
|
1296 ERROR e => ERROR e
|
Chris@303
|
1297 | OK true => OK true
|
Chris@303
|
1298 | OK false =>
|
Chris@303
|
1299 case git_command context libname
|
Chris@303
|
1300 ["merge-base", "--is-ancestor",
|
Chris@303
|
1301 "HEAD", remote_branch_name branch] of
|
Chris@303
|
1302 ERROR e => OK false (* cmd returns non-zero for no *)
|
Chris@303
|
1303 | _ => OK true
|
Chris@303
|
1304
|
Chris@303
|
1305 fun is_newest context (libname, branch) =
|
Chris@303
|
1306 case is_newest_locally context (libname, branch) of
|
Chris@303
|
1307 ERROR e => ERROR e
|
Chris@303
|
1308 | OK false => OK false
|
Chris@303
|
1309 | OK true =>
|
Chris@303
|
1310 case git_command context libname ["fetch"] of
|
Chris@303
|
1311 ERROR e => ERROR e
|
Chris@303
|
1312 | _ => is_newest_locally context (libname, branch)
|
Chris@303
|
1313
|
Chris@303
|
1314 fun is_modified_locally context libname =
|
Chris@303
|
1315 case git_command_output context libname ["status", "--porcelain"] of
|
Chris@303
|
1316 ERROR e => ERROR e
|
Chris@303
|
1317 | OK "" => OK false
|
Chris@303
|
1318 | OK _ => OK true
|
Chris@303
|
1319
|
Chris@303
|
1320 (* This function updates to the latest revision on a branch rather
|
Chris@303
|
1321 than to a specific id or tag. We can't just checkout the given
|
Chris@303
|
1322 branch, as that will succeed even if the branch isn't up to
|
Chris@303
|
1323 date. We could checkout the branch and then fetch and merge,
|
Chris@303
|
1324 but it's perhaps cleaner not to maintain a local branch at all,
|
Chris@303
|
1325 but instead checkout the remote branch as a detached head. *)
|
Chris@303
|
1326
|
Chris@303
|
1327 fun update context (libname, branch) =
|
Chris@303
|
1328 case git_command context libname ["fetch"] of
|
Chris@303
|
1329 ERROR e => ERROR e
|
Chris@303
|
1330 | _ =>
|
Chris@303
|
1331 case git_command context libname ["checkout", "--detach",
|
Chris@303
|
1332 remote_branch_name branch] of
|
Chris@303
|
1333 ERROR e => ERROR e
|
Chris@303
|
1334 | _ => id_of context libname
|
Chris@303
|
1335
|
Chris@303
|
1336 (* This function is dealing with a specific id or tag, so if we
|
Chris@303
|
1337 can successfully check it out (detached) then that's all we
|
Chris@303
|
1338 need to do, regardless of whether fetch succeeded or not. We do
|
Chris@303
|
1339 attempt the fetch first, though, purely in order to avoid ugly
|
Chris@303
|
1340 error messages in the common case where we're being asked to
|
Chris@303
|
1341 update to a new pin (from the lock file) that hasn't been
|
Chris@303
|
1342 fetched yet. *)
|
Chris@303
|
1343
|
Chris@303
|
1344 fun update_to context (libname, "") =
|
Chris@303
|
1345 ERROR "Non-empty id (tag or revision id) required for update_to"
|
Chris@303
|
1346 | update_to context (libname, id) =
|
Chris@303
|
1347 let val fetch_result = git_command context libname ["fetch"]
|
Chris@303
|
1348 in
|
Chris@303
|
1349 case git_command context libname ["checkout", "--detach", id] of
|
Chris@303
|
1350 OK _ => id_of context libname
|
Chris@303
|
1351 | ERROR e =>
|
Chris@303
|
1352 case fetch_result of
|
Chris@303
|
1353 ERROR e' => ERROR e' (* this was the ur-error *)
|
Chris@303
|
1354 | _ => ERROR e
|
Chris@303
|
1355 end
|
Chris@303
|
1356
|
Chris@303
|
1357 end
|
Chris@303
|
1358
|
Chris@303
|
1359 structure AnyLibControl :> LIB_CONTROL = struct
|
Chris@303
|
1360
|
Chris@303
|
1361 structure H = LibControlFn(HgControl)
|
Chris@303
|
1362 structure G = LibControlFn(GitControl)
|
Chris@303
|
1363
|
Chris@303
|
1364 fun review context (spec as { vcs, ... } : libspec) =
|
Chris@303
|
1365 (fn HG => H.review | GIT => G.review) vcs context spec
|
Chris@303
|
1366
|
Chris@303
|
1367 fun status context (spec as { vcs, ... } : libspec) =
|
Chris@303
|
1368 (fn HG => H.status | GIT => G.status) vcs context spec
|
Chris@303
|
1369
|
Chris@303
|
1370 fun update context (spec as { vcs, ... } : libspec) =
|
Chris@303
|
1371 (fn HG => H.update | GIT => G.update) vcs context spec
|
Chris@303
|
1372 end
|
Chris@303
|
1373
|
Chris@303
|
1374 val libobjname = "libraries"
|
Chris@303
|
1375
|
Chris@303
|
1376 fun load_libspec spec_json lock_json libname : libspec =
|
Chris@303
|
1377 let open JsonBits
|
Chris@303
|
1378 val libobj = lookup_mandatory spec_json [libobjname, libname]
|
Chris@303
|
1379 val vcs = lookup_mandatory_string libobj ["vcs"]
|
Chris@303
|
1380 val retrieve = lookup_optional_string libobj
|
Chris@303
|
1381 val service = retrieve ["service"]
|
Chris@303
|
1382 val owner = retrieve ["owner"]
|
Chris@303
|
1383 val repo = retrieve ["repository"]
|
Chris@303
|
1384 val url = retrieve ["url"]
|
Chris@303
|
1385 val branch = retrieve ["branch"]
|
Chris@303
|
1386 val user_pin = retrieve ["pin"]
|
Chris@303
|
1387 val lock_pin = case lookup_optional lock_json [libobjname, libname] of
|
Chris@303
|
1388 SOME ll => lookup_optional_string ll ["pin"]
|
Chris@303
|
1389 | NONE => NONE
|
Chris@303
|
1390 in
|
Chris@303
|
1391 {
|
Chris@303
|
1392 libname = libname,
|
Chris@303
|
1393 vcs = case vcs of
|
Chris@303
|
1394 "hg" => HG
|
Chris@303
|
1395 | "git" => GIT
|
Chris@303
|
1396 | other => raise Fail ("Unknown version-control system \"" ^
|
Chris@303
|
1397 other ^ "\""),
|
Chris@303
|
1398 source = case (url, service, owner, repo) of
|
Chris@303
|
1399 (SOME u, NONE, _, _) => URL_SOURCE u
|
Chris@303
|
1400 | (NONE, SOME ss, owner, repo) =>
|
Chris@303
|
1401 SERVICE_SOURCE { service = ss, owner = owner, repo = repo }
|
Chris@303
|
1402 | _ => raise Fail ("Must have exactly one of service " ^
|
Chris@303
|
1403 "or url string"),
|
Chris@303
|
1404 pin = case lock_pin of
|
Chris@303
|
1405 SOME p => PINNED p
|
Chris@303
|
1406 | NONE =>
|
Chris@303
|
1407 case user_pin of
|
Chris@303
|
1408 SOME p => PINNED p
|
Chris@303
|
1409 | NONE => UNPINNED,
|
Chris@303
|
1410 branch = case branch of
|
Chris@303
|
1411 SOME b => BRANCH b
|
Chris@303
|
1412 | NONE => DEFAULT_BRANCH
|
Chris@303
|
1413 }
|
Chris@303
|
1414 end
|
Chris@303
|
1415
|
Chris@303
|
1416 fun load_userconfig () : userconfig =
|
Chris@303
|
1417 let val home = FileBits.homedir ()
|
Chris@303
|
1418 val conf_json =
|
Chris@303
|
1419 JsonBits.load_json_from
|
Chris@303
|
1420 (OS.Path.joinDirFile {
|
Chris@303
|
1421 dir = home,
|
Chris@303
|
1422 file = VextFilenames.user_config_file })
|
Chris@303
|
1423 handle IO.Io _ => Json.OBJECT []
|
Chris@303
|
1424 in
|
Chris@303
|
1425 {
|
Chris@303
|
1426 accounts = case JsonBits.lookup_optional conf_json ["accounts"] of
|
Chris@303
|
1427 NONE => []
|
Chris@303
|
1428 | SOME (Json.OBJECT aa) =>
|
Chris@303
|
1429 map (fn (k, (Json.STRING v)) =>
|
Chris@303
|
1430 { service = k, login = v }
|
Chris@303
|
1431 | _ => raise Fail
|
Chris@303
|
1432 "String expected for account name")
|
Chris@303
|
1433 aa
|
Chris@303
|
1434 | _ => raise Fail "Array expected for accounts",
|
Chris@303
|
1435 providers = Provider.load_providers conf_json
|
Chris@303
|
1436 }
|
Chris@303
|
1437 end
|
Chris@303
|
1438
|
Chris@303
|
1439 datatype pintype =
|
Chris@303
|
1440 NO_LOCKFILE |
|
Chris@303
|
1441 USE_LOCKFILE
|
Chris@303
|
1442
|
Chris@303
|
1443 fun load_project (userconfig : userconfig) rootpath pintype : project =
|
Chris@303
|
1444 let val spec_file = FileBits.project_spec_path rootpath
|
Chris@303
|
1445 val lock_file = FileBits.project_lock_path rootpath
|
Chris@303
|
1446 val _ = if OS.FileSys.access (spec_file, [OS.FileSys.A_READ])
|
Chris@303
|
1447 handle OS.SysErr _ => false
|
Chris@303
|
1448 then ()
|
Chris@303
|
1449 else raise Fail ("Failed to open project spec file " ^
|
Chris@303
|
1450 (VextFilenames.project_file) ^ " in " ^
|
Chris@303
|
1451 rootpath ^
|
Chris@303
|
1452 ".\nPlease ensure the spec file is in the " ^
|
Chris@303
|
1453 "project root and run this from there.")
|
Chris@303
|
1454 val spec_json = JsonBits.load_json_from spec_file
|
Chris@303
|
1455 val lock_json = if pintype = USE_LOCKFILE
|
Chris@303
|
1456 then JsonBits.load_json_from lock_file
|
Chris@303
|
1457 handle IO.Io _ => Json.OBJECT []
|
Chris@303
|
1458 else Json.OBJECT []
|
Chris@303
|
1459 val extdir = JsonBits.lookup_mandatory_string spec_json
|
Chris@303
|
1460 ["config", "extdir"]
|
Chris@303
|
1461 val spec_libs = JsonBits.lookup_optional spec_json [libobjname]
|
Chris@303
|
1462 val lock_libs = JsonBits.lookup_optional lock_json [libobjname]
|
Chris@303
|
1463 val providers = Provider.load_more_providers
|
Chris@303
|
1464 (#providers userconfig) spec_json
|
Chris@303
|
1465 val libnames = case spec_libs of
|
Chris@303
|
1466 NONE => []
|
Chris@303
|
1467 | SOME (Json.OBJECT ll) => map (fn (k, v) => k) ll
|
Chris@303
|
1468 | _ => raise Fail "Object expected for libs"
|
Chris@303
|
1469 in
|
Chris@303
|
1470 {
|
Chris@303
|
1471 context = {
|
Chris@303
|
1472 rootpath = rootpath,
|
Chris@303
|
1473 extdir = extdir,
|
Chris@303
|
1474 providers = providers,
|
Chris@303
|
1475 accounts = #accounts userconfig
|
Chris@303
|
1476 },
|
Chris@303
|
1477 libs = map (load_libspec spec_json lock_json) libnames
|
Chris@303
|
1478 }
|
Chris@303
|
1479 end
|
Chris@303
|
1480
|
Chris@303
|
1481 fun save_lock_file rootpath locks =
|
Chris@303
|
1482 let val lock_file = FileBits.project_lock_path rootpath
|
Chris@303
|
1483 open Json
|
Chris@303
|
1484 val lock_json =
|
Chris@303
|
1485 OBJECT [
|
Chris@303
|
1486 (libobjname,
|
Chris@303
|
1487 OBJECT (map (fn { libname, id_or_tag } =>
|
Chris@303
|
1488 (libname,
|
Chris@303
|
1489 OBJECT [ ("pin", STRING id_or_tag) ]))
|
Chris@303
|
1490 locks))
|
Chris@303
|
1491 ]
|
Chris@303
|
1492 in
|
Chris@303
|
1493 JsonBits.save_json_to lock_file lock_json
|
Chris@303
|
1494 end
|
Chris@303
|
1495
|
Chris@303
|
1496 fun pad_to n str =
|
Chris@303
|
1497 if n <= String.size str then str
|
Chris@303
|
1498 else pad_to n (str ^ " ")
|
Chris@303
|
1499
|
Chris@303
|
1500 fun hline_to 0 = ""
|
Chris@303
|
1501 | hline_to n = "-" ^ hline_to (n-1)
|
Chris@303
|
1502
|
Chris@303
|
1503 val libname_width = 25
|
Chris@303
|
1504 val libstate_width = 11
|
Chris@303
|
1505 val localstate_width = 9
|
Chris@303
|
1506 val notes_width = 5
|
Chris@303
|
1507 val divider = " | "
|
Chris@303
|
1508
|
Chris@303
|
1509 fun print_status_header () =
|
Chris@303
|
1510 print ("\r" ^ pad_to 80 "" ^ "\n " ^
|
Chris@303
|
1511 pad_to libname_width "Library" ^ divider ^
|
Chris@303
|
1512 pad_to libstate_width "State" ^ divider ^
|
Chris@303
|
1513 pad_to localstate_width "Local" ^ divider ^
|
Chris@303
|
1514 "Notes" ^ "\n " ^
|
Chris@303
|
1515 hline_to libname_width ^ "-+-" ^
|
Chris@303
|
1516 hline_to libstate_width ^ "-+-" ^
|
Chris@303
|
1517 hline_to localstate_width ^ "-+-" ^
|
Chris@303
|
1518 hline_to notes_width ^ "\n")
|
Chris@303
|
1519
|
Chris@303
|
1520 fun print_outcome_header () =
|
Chris@303
|
1521 print ("\r" ^ pad_to 80 "" ^ "\n " ^
|
Chris@303
|
1522 pad_to libname_width "Library" ^ divider ^
|
Chris@303
|
1523 pad_to libstate_width "Outcome" ^ divider ^
|
Chris@303
|
1524 "Notes" ^ "\n " ^
|
Chris@303
|
1525 hline_to libname_width ^ "-+-" ^
|
Chris@303
|
1526 hline_to libstate_width ^ "-+-" ^
|
Chris@303
|
1527 hline_to notes_width ^ "\n")
|
Chris@303
|
1528
|
Chris@303
|
1529 fun print_status with_network (libname, status) =
|
Chris@303
|
1530 let val libstate_str =
|
Chris@303
|
1531 case status of
|
Chris@303
|
1532 OK (ABSENT, _) => "Absent"
|
Chris@303
|
1533 | OK (CORRECT, _) => if with_network then "Correct" else "Present"
|
Chris@303
|
1534 | OK (SUPERSEDED, _) => "Superseded"
|
Chris@303
|
1535 | OK (WRONG, _) => "Wrong"
|
Chris@303
|
1536 | ERROR _ => "Error"
|
Chris@303
|
1537 val localstate_str =
|
Chris@303
|
1538 case status of
|
Chris@303
|
1539 OK (_, MODIFIED) => "Modified"
|
Chris@303
|
1540 | OK (_, UNMODIFIED) => "Clean"
|
Chris@303
|
1541 | _ => ""
|
Chris@303
|
1542 val error_str =
|
Chris@303
|
1543 case status of
|
Chris@303
|
1544 ERROR e => e
|
Chris@303
|
1545 | _ => ""
|
Chris@303
|
1546 in
|
Chris@303
|
1547 print (" " ^
|
Chris@303
|
1548 pad_to libname_width libname ^ divider ^
|
Chris@303
|
1549 pad_to libstate_width libstate_str ^ divider ^
|
Chris@303
|
1550 pad_to localstate_width localstate_str ^ divider ^
|
Chris@303
|
1551 error_str ^ "\n")
|
Chris@303
|
1552 end
|
Chris@303
|
1553
|
Chris@303
|
1554 fun print_update_outcome (libname, outcome) =
|
Chris@303
|
1555 let val outcome_str =
|
Chris@303
|
1556 case outcome of
|
Chris@303
|
1557 OK id => "Ok"
|
Chris@303
|
1558 | ERROR e => "Failed"
|
Chris@303
|
1559 val error_str =
|
Chris@303
|
1560 case outcome of
|
Chris@303
|
1561 ERROR e => e
|
Chris@303
|
1562 | _ => ""
|
Chris@303
|
1563 in
|
Chris@303
|
1564 print (" " ^
|
Chris@303
|
1565 pad_to libname_width libname ^ divider ^
|
Chris@303
|
1566 pad_to libstate_width outcome_str ^ divider ^
|
Chris@303
|
1567 error_str ^ "\n")
|
Chris@303
|
1568 end
|
Chris@303
|
1569
|
Chris@303
|
1570 fun act_and_print action print_header print_line (libs : libspec list) =
|
Chris@303
|
1571 let val lines = map (fn lib => (#libname lib, action lib)) libs
|
Chris@303
|
1572 val _ = print_header ()
|
Chris@303
|
1573 in
|
Chris@303
|
1574 app print_line lines;
|
Chris@303
|
1575 lines
|
Chris@303
|
1576 end
|
Chris@303
|
1577
|
Chris@303
|
1578 fun return_code_for outcomes =
|
Chris@303
|
1579 foldl (fn ((_, result), acc) =>
|
Chris@303
|
1580 case result of
|
Chris@303
|
1581 ERROR _ => OS.Process.failure
|
Chris@303
|
1582 | _ => acc)
|
Chris@303
|
1583 OS.Process.success
|
Chris@303
|
1584 outcomes
|
Chris@303
|
1585
|
Chris@303
|
1586 fun status_of_project ({ context, libs } : project) =
|
Chris@303
|
1587 return_code_for (act_and_print (AnyLibControl.status context)
|
Chris@303
|
1588 print_status_header (print_status false)
|
Chris@303
|
1589 libs)
|
Chris@303
|
1590
|
Chris@303
|
1591 fun review_project ({ context, libs } : project) =
|
Chris@303
|
1592 return_code_for (act_and_print (AnyLibControl.review context)
|
Chris@303
|
1593 print_status_header (print_status true)
|
Chris@303
|
1594 libs)
|
Chris@303
|
1595
|
Chris@303
|
1596 fun update_project ({ context, libs } : project) =
|
Chris@303
|
1597 let val outcomes = act_and_print
|
Chris@303
|
1598 (AnyLibControl.update context)
|
Chris@303
|
1599 print_outcome_header print_update_outcome libs
|
Chris@303
|
1600 val locks =
|
Chris@303
|
1601 List.concat
|
Chris@303
|
1602 (map (fn (libname, result) =>
|
Chris@303
|
1603 case result of
|
Chris@303
|
1604 ERROR _ => []
|
Chris@303
|
1605 | OK id => [{ libname = libname, id_or_tag = id }])
|
Chris@303
|
1606 outcomes)
|
Chris@303
|
1607 val return_code = return_code_for outcomes
|
Chris@303
|
1608 in
|
Chris@303
|
1609 if OS.Process.isSuccess return_code
|
Chris@303
|
1610 then save_lock_file (#rootpath context) locks
|
Chris@303
|
1611 else ();
|
Chris@303
|
1612 return_code
|
Chris@303
|
1613 end
|
Chris@303
|
1614
|
Chris@303
|
1615 fun load_local_project pintype =
|
Chris@303
|
1616 let val userconfig = load_userconfig ()
|
Chris@303
|
1617 val rootpath = OS.FileSys.getDir ()
|
Chris@303
|
1618 in
|
Chris@303
|
1619 load_project userconfig rootpath pintype
|
Chris@303
|
1620 end
|
Chris@303
|
1621
|
Chris@303
|
1622 fun with_local_project pintype f =
|
Chris@303
|
1623 let val return_code = f (load_local_project pintype)
|
Chris@303
|
1624 handle e =>
|
Chris@303
|
1625 (print ("Failed with exception: " ^
|
Chris@303
|
1626 (exnMessage e) ^ "\n");
|
Chris@303
|
1627 OS.Process.failure)
|
Chris@303
|
1628 val _ = print "\n";
|
Chris@303
|
1629 in
|
Chris@303
|
1630 return_code
|
Chris@303
|
1631 end
|
Chris@303
|
1632
|
Chris@303
|
1633 fun review () = with_local_project NO_LOCKFILE review_project
|
Chris@303
|
1634 fun status () = with_local_project NO_LOCKFILE status_of_project
|
Chris@303
|
1635 fun update () = with_local_project NO_LOCKFILE update_project
|
Chris@303
|
1636 fun install () = with_local_project USE_LOCKFILE update_project
|
Chris@303
|
1637
|
Chris@303
|
1638 fun version () =
|
Chris@303
|
1639 (print ("v" ^ vext_version ^ "\n");
|
Chris@303
|
1640 OS.Process.success)
|
Chris@303
|
1641
|
Chris@303
|
1642 fun usage () =
|
Chris@303
|
1643 (print "\nVext ";
|
Chris@303
|
1644 version ();
|
Chris@303
|
1645 print ("\nA simple manager for third-party source code dependencies.\n\n"
|
Chris@303
|
1646 ^ "Usage:\n\n"
|
Chris@303
|
1647 ^ " vext <command>\n\n"
|
Chris@303
|
1648 ^ "where <command> is one of:\n\n"
|
Chris@303
|
1649 ^ " status print quick report on local status only, without using network\n"
|
Chris@303
|
1650 ^ " review check configured libraries against their providers, and report\n"
|
Chris@303
|
1651 ^ " install update configured libraries according to project specs and lock file\n"
|
Chris@303
|
1652 ^ " update update configured libraries and lock file according to project specs\n"
|
Chris@303
|
1653 ^ " version print the Vext version number and exit\n\n");
|
Chris@303
|
1654 OS.Process.failure)
|
Chris@303
|
1655
|
Chris@303
|
1656 fun vext args =
|
Chris@303
|
1657 let val return_code =
|
Chris@303
|
1658 case args of
|
Chris@303
|
1659 ["review"] => review ()
|
Chris@303
|
1660 | ["status"] => status ()
|
Chris@303
|
1661 | ["install"] => install ()
|
Chris@303
|
1662 | ["update"] => update ()
|
Chris@303
|
1663 | ["version"] => version ()
|
Chris@303
|
1664 | _ => usage ()
|
Chris@303
|
1665 in
|
Chris@303
|
1666 OS.Process.exit return_code;
|
Chris@303
|
1667 ()
|
Chris@303
|
1668 end
|
Chris@303
|
1669
|
Chris@303
|
1670 fun main () =
|
Chris@303
|
1671 vext (CommandLine.arguments ())
|