• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ocaml / dune / 29266

12 Dec 2024 12:43PM UTC coverage: 6.906%. First build
29266

Pull #11197

github

web-flow
Merge 5b9b8ab75 into 046fe80a7
Pull Request #11197: Better error message for old versions of Git

0 of 14 new or added lines in 1 file covered. (0.0%)

2951 of 42729 relevant lines covered (6.91%)

26660.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

4.82
/src/dune_pkg/rev_store.ml
1
open Stdune
2
open Dune_vcs
3
module Process = Dune_engine.Process
4
module Display = Dune_engine.Display
5
module Scheduler = Dune_engine.Scheduler
6
module Re = Dune_re
7
module Flock = Dune_util.Flock
8
open Fiber.O
9

10
module Object = struct
11
  type t = Sha1 of string
12

13
  let compare (Sha1 x) (Sha1 y) = String.compare x y
×
14
  let to_string (Sha1 s) = s
×
15
  let equal (Sha1 x) (Sha1 y) = String.equal x y
×
16
  let to_dyn (Sha1 s) = Dyn.string s
×
17
  let hash (Sha1 s) = String.hash s
×
18

19
  type resolved = t
20

21
  let of_sha1 s =
22
    if String.length s = 40
×
23
       && String.for_all s ~f:(function
×
24
         | '0' .. '9' | 'a' .. 'f' | 'A' .. 'F' -> true
×
25
         | _ -> false)
×
26
    then Some (Sha1 (String.lowercase_ascii s))
×
27
    else None
×
28
  ;;
29
end
30

31
module Remote = struct
32
  type nonrec t =
33
    { url : string
34
    ; default_branch : Object.resolved option Fiber.t
35
    ; refs : Object.resolved String.Map.t Fiber.t
36
    }
37

38
  let default_branch t = t.default_branch
×
39
end
40

41
type t =
42
  { dir : Path.t
43
  ; remotes : (string, Remote.t) Table.t
44
  ; (* The mutex that needs to be acquired before touching [present_objects] *)
45
    object_mutexes : (Object.t, Fiber.Mutex.t) Table.t
46
  ; present_objects : (Object.t, unit) Table.t
47
  }
48

49
let with_mutex t obj ~f =
50
  let* () = Fiber.return () in
×
51
  let mutex =
×
52
    (* ideally, we'd like to clear this table if there's nobody queued for
53
       mutex, but it's not safe to do without tracking the number of fiber
54
       awaiting. *)
55
    Table.find_or_add t.object_mutexes obj ~f:(fun _ -> Fiber.Mutex.create ())
×
56
  in
57
  Fiber.Mutex.with_lock mutex ~f
×
58
;;
59

60
let lock_path { dir; _ } =
61
  let parent = dir |> Path.parent_exn in
×
62
  Path.relative parent "rev-store.lock"
×
63
;;
64

65
let rec attempt_to_lock flock lock ~max_retries =
66
  let sleep_duration = 0.1 in
×
67
  match Flock.lock_non_block flock lock with
68
  | Error e -> Fiber.return @@ Error e
×
69
  | Ok `Success -> Fiber.return (Ok `Success)
×
70
  | Ok `Failure ->
×
71
    if max_retries > 0
72
    then
73
      let* () = Scheduler.sleep ~seconds:sleep_duration in
×
74
      attempt_to_lock flock lock ~max_retries:(max_retries - 1)
×
75
    else Fiber.return (Ok `Failure)
×
76
;;
77

78
let with_flock lock_path ~f =
79
  let open Fiber.O in
×
80
  let parent = Path.parent_exn lock_path in
81
  Path.mkdir_p parent;
×
82
  let fd =
×
83
    Unix.openfile
84
      (Path.to_string lock_path)
×
85
      [ Unix.O_CREAT; O_WRONLY; O_SHARE_DELETE; Unix.O_CLOEXEC ]
86
      0o600
87
  in
88
  let flock = Flock.create fd in
×
89
  let max_retries = 49 in
×
90
  Fiber.finalize
91
    ~finally:(fun () ->
92
      let+ () = Fiber.return () in
×
93
      Unix.close fd)
×
94
    (fun () ->
95
      attempt_to_lock flock Flock.Exclusive ~max_retries
×
96
      >>= function
97
      | Ok `Success ->
×
98
        Fiber.finalize
99
          (fun () ->
100
            Dune_util.Global_lock.write_pid fd;
×
101
            f ())
×
102
          ~finally:(fun () ->
103
            let+ () = Fiber.return () in
×
104
            Path.unlink_no_err lock_path;
×
105
            match Flock.unlock flock with
×
106
            | Ok () -> ()
×
107
            | Error ue ->
×
108
              Unix_error.Detailed.create ue ~syscall:"flock" ~arg:"unlock"
109
              |> Unix_error.Detailed.raise)
×
110
      | Ok `Failure ->
×
111
        let pid = Io.read_file lock_path in
112
        User_error.raise
×
113
          ~hints:
114
            [ Pp.textf
×
115
                "Another dune instance (pid %s) has locked the revision store. If this \
116
                 is happening in error, make sure to terminate that instance and re-run \
117
                 the command."
118
                pid
119
            ]
120
          [ Pp.textf "Couldn't acquire revision store lock after %d attempts" max_retries
×
121
          ]
122
      | Error error ->
×
123
        User_error.raise
124
          [ Pp.textf
×
125
              "Failed to get a lock for the revision store at %s: %s"
126
              (Path.to_string_maybe_quoted lock_path)
×
127
              (Unix.error_message error)
×
128
          ])
129
;;
130

131
let failure_mode = Process.Failure_mode.Return
132
let output_limit = Sys.max_string_length
133
let make_stdout () = Process.Io.make_stdout ~output_on_success:Swallow ~output_limit
×
134
let make_stderr () = Process.Io.make_stderr ~output_on_success:Swallow ~output_limit
×
135

136
let env =
137
  (* to avoid Git translating its CLI *)
138
  Env.add Env.initial ~var:"LC_ALL" ~value:"C"
139
  (* to avoid prmompting for passwords *)
140
  |> Env.add ~var:"GIT_TERMINAL_PROMPT" ~value:"0"
39✔
141
;;
142

143
module Git_error = struct
144
  type t =
145
    { dir : Path.t
146
    ; args : string list
147
    ; exit_code : int
148
    ; output : string list
149
    }
150

151
  let raise_code_error { dir; args; exit_code; output } =
152
    let git = Lazy.force Vcs.git in
×
153
    Code_error.raise
×
154
      "git returned non-zero exit code"
155
      [ "exit code", Dyn.int exit_code
×
156
      ; "dir", Path.to_dyn dir
×
157
      ; "git", Path.to_dyn git
×
158
      ; "args", Dyn.list Dyn.string args
×
159
      ; "output", Dyn.list Dyn.string output
×
160
      ]
161
  ;;
162

163
  let result_get_or_code_error = function
164
    | Ok x -> x
×
165
    | Error t -> raise_code_error t
×
166
  ;;
167
end
168

169
let run_with_exit_code { dir; _ } ~allow_codes ~display args =
170
  let stdout_to = make_stdout () in
×
171
  let git = Lazy.force Vcs.git in
×
NEW
172
  let+ stderr, exit_code =
×
173
    Fiber_util.Temp.with_temp_file
174
      ~f:(function
NEW
175
        | Error exn -> raise exn
×
NEW
176
        | Ok path ->
×
177
          let stderr_to = Process.Io.file path Out in
NEW
178
          let+ (), exit_code =
×
NEW
179
            Process.run ~dir ~display ~stdout_to ~stderr_to ~env failure_mode git args
×
180
          in
NEW
181
          Stdune.Io.read_file path, exit_code)
×
182
      ~prefix:"dune"
183
      ~suffix:"run_with_exit_code"
NEW
184
      ~dir:(Path.of_string (Filename.get_temp_dir_name ()))
×
185
  in
186
  if allow_codes exit_code
×
187
  then Ok exit_code
×
NEW
188
  else (
×
189
    match exit_code with
NEW
190
    | 129
×
NEW
191
      when String.is_prefix ~prefix:"error: unknown option `no-write-fetch-head'" stderr
×
192
      ->
NEW
193
      User_error.raise
×
NEW
194
        [ Pp.text "Your git version doesn't support the '--no-write-fetch-head' flag." ]
×
NEW
195
        ~hints:[ Pp.text "Please update your git version." ]
×
NEW
196
    | _ ->
×
197
      ();
198
      Error { Git_error.dir; args; exit_code; output = [] })
199
;;
200

201
let run t ~display args =
202
  run_with_exit_code t ~allow_codes:(Int.equal 0) ~display args
×
203
  >>| Result.map ~f:(ignore : int -> unit)
204
;;
205

206
let run_capture_lines { dir; _ } ~display args =
207
  let git = Lazy.force Vcs.git in
×
208
  let+ output, exit_code =
×
209
    Process.run_capture_lines ~dir ~display ~env failure_mode git args
×
210
  in
211
  if exit_code = 0 then Ok output else Error { Git_error.dir; args; exit_code; output }
×
212
;;
213

214
let run_capture_zero_separated_lines { dir; _ } args =
215
  let git = Lazy.force Vcs.git in
×
216
  let+ output, exit_code =
×
217
    Process.run_capture_zero_separated ~dir ~display:Quiet ~env failure_mode git args
×
218
  in
219
  if exit_code = 0 then Ok output else Error { Git_error.dir; args; exit_code; output }
×
220
;;
221

222
let cat_file { dir; _ } command =
223
  let git = Lazy.force Vcs.git in
×
224
  let failure_mode = Vcs.git_accept () in
×
225
  let stderr_to = make_stderr () in
×
226
  let stdout_to = make_stdout () in
×
227
  "cat-file" :: command
×
228
  |> Process.run ~dir ~display:Quiet ~stdout_to ~stderr_to ~env failure_mode git
×
229
  >>| Result.is_ok
230
;;
231

232
let rev_parse { dir; _ } rev =
233
  let git = Lazy.force Vcs.git in
×
234
  let+ line, code =
×
235
    Process.run_capture_line
×
236
      ~dir
237
      ~display:Quiet
238
      ~env
239
      Return
240
      git
241
      [ "rev-parse"; "--verify"; "--quiet"; sprintf "%s^{commit}" rev ]
×
242
  in
243
  if code = 0 then Some (Option.value_exn (Object.of_sha1 line)) else None
×
244
;;
245

246
let object_exists_no_lock { dir; _ } (Object.Sha1 sha1) =
247
  let git = Lazy.force Vcs.git in
×
248
  let+ (), code =
×
249
    Process.run ~dir ~display:Quiet ~env Return git [ "cat-file"; "-e"; sha1 ]
×
250
  in
251
  code = 0
×
252
;;
253

254
let object_exists ({ present_objects; _ } as t) obj =
255
  let* () = Fiber.return () in
×
256
  match Table.find present_objects obj with
×
257
  | Some () -> Fiber.return true
×
258
  | None ->
×
259
    Table.set present_objects obj ();
260
    let+ res = object_exists_no_lock t obj in
×
261
    (* We clear objects that aren't present, so that they can be re-queried
262
       after fetches *)
263
    if res then Table.set present_objects obj ();
×
264
    res
×
265
;;
266

267
let resolve_object t hash =
268
  with_mutex t hash ~f:(fun () -> object_exists t hash)
×
269
  >>| function
270
  | false -> None
×
271
  | true -> Some hash
×
272
;;
273

274
let mem_path repo (Object.Sha1 sha1) path =
275
  cat_file repo [ "-e"; sprintf "%s:%s" sha1 (Path.Local.to_string path) ]
×
276
;;
277

278
let show =
279
  let show { dir; _ } revs_and_paths =
280
    let git = Lazy.force Vcs.git in
×
281
    let failure_mode = Vcs.git_accept () in
×
282
    let command =
×
283
      "show"
284
      :: List.map revs_and_paths ~f:(function
×
285
        | `Object o -> o
×
286
        | `Path (Object.Sha1 r, path) -> sprintf "%s:%s" r (Path.Local.to_string path))
×
287
    in
288
    let stderr_to = make_stderr () in
289
    Process.run_capture ~dir ~display:Quiet ~stderr_to failure_mode git command
×
290
  in
291
  fun t revs_and_paths ->
292
    let cli_limit =
×
293
      (if Sys.win32 then 8191 else 2097152)
×
294
      - String.length "show"
×
295
      - 1 (* space *)
296
      - String.length (Path.to_string (Lazy.force Vcs.git))
×
297
      - 100 (* some extra safety *)
298
    in
299
    let rec loop acc batch cmd_len_remaining = function
300
      | [] -> List.rev batch :: acc
×
301
      | cmd :: cmds ->
×
302
        let cmd_len =
303
          1
304
          (* space separator *)
305
          +
306
          match cmd with
307
          | `Object o -> String.length o
×
308
          | `Path (Object.Sha1 r, path) ->
×
309
            String.length r + String.length (Path.Local.to_string path) + 1
×
310
        in
311
        let new_remaining = cmd_len_remaining - cmd_len in
312
        if new_remaining >= 0
313
        then loop acc (cmd :: batch) new_remaining cmds
×
314
        else loop (List.rev batch :: acc) [ cmd ] cli_limit cmds
×
315
    in
316
    loop [] [] cli_limit revs_and_paths
317
    |> List.rev
×
318
    |> Fiber.parallel_map ~f:(show t)
×
319
    >>| Result.List.all
×
320
    >>| Result.map ~f:(String.concat ~sep:"")
×
321
    >>| Result.to_option
322
;;
323

324
let load_or_create ~dir =
325
  let t =
×
326
    { dir
327
    ; remotes = Table.create (module String) 4
×
328
    ; present_objects = Table.create (module Object) 16
×
329
    ; object_mutexes = Table.create (module Object) 16
×
330
    }
331
  in
332
  let lock = lock_path t in
333
  let* () = Fiber.return () in
×
334
  let+ () =
×
335
    with_flock lock ~f:(fun () ->
×
336
      match Fpath.mkdir_p (Path.to_string dir) with
×
337
      | Already_exists -> Fiber.return ()
×
338
      | Created ->
×
339
        run t ~display:Quiet [ "init"; "--bare" ]
×
340
        >>| (function
341
         | Ok () -> ()
×
342
         | Error git_error -> Git_error.raise_code_error git_error)
×
343
      | exception Unix.Unix_error (e, x, y) ->
×
344
        User_error.raise
345
          [ Pp.textf "%s isn't a directory" (Path.to_string_maybe_quoted dir)
×
346
          ; Pp.textf "reason: %s" (Unix_error.Detailed.to_string_hum (e, x, y))
×
347
          ]
348
          ~hints:[ Pp.text "delete this file or check its permissions" ])
×
349
  in
350
  t
×
351
;;
352

353
module Commit = struct
354
  module T = struct
355
    type t =
356
      { path : Path.Local.t
357
      ; rev : Object.t
358
      }
359

360
    let compare { path; rev } t =
361
      let open Ordering.O in
×
362
      let= () = Path.Local.compare path t.path in
×
363
      Object.compare rev t.rev
×
364
    ;;
365

366
    let to_dyn { path; rev } =
367
      Dyn.record [ "path", Path.Local.to_dyn path; "rev", Object.to_dyn rev ]
×
368
    ;;
369
  end
370

371
  include T
372
  module C = Comparable.Make (T)
373
  module Set = C.Set
374
end
375

376
module File = struct
377
  module T = struct
378
    type t =
379
      | Redirect of
380
          { path : Path.Local.t
381
          ; to_ : t
382
          }
383
      | Direct of
384
          { path : Path.Local.t
385
          ; size : int
386
          ; hash : string
387
          }
388

389
    let compare = Poly.compare
390

391
    let to_dyn = function
392
      | Redirect _ -> Dyn.opaque ()
×
393
      | Direct { path; size; hash } ->
×
394
        Dyn.record
395
          [ "path", Path.Local.to_dyn path
×
396
          ; "size", Dyn.int size
×
397
          ; "hash", Dyn.string hash
×
398
          ]
399
    ;;
400
  end
401

402
  include T
403

404
  let path = function
405
    | Redirect p -> p.path
×
406
    | Direct p -> p.path
×
407
  ;;
408

409
  let rec size = function
410
    | Direct t -> t.size
×
411
    | Redirect t -> size t.to_
×
412
  ;;
413

414
  let rec hash = function
415
    | Direct t -> t.hash
×
416
    | Redirect t -> hash t.to_
×
417
  ;;
418

419
  module C = Comparable.Make (T)
420
  module Set = C.Set
421
end
422

423
module Entry = struct
424
  module T = struct
425
    type t =
426
      | File of File.t
427
      | Commit of Commit.t
428

429
    let compare a b =
430
      match a, b with
×
431
      | File a, File b -> File.compare a b
×
432
      | Commit a, Commit b -> Commit.compare a b
×
433
      | File _, Commit _ -> Ordering.Lt
×
434
      | Commit _, File _ -> Ordering.Gt
×
435
    ;;
436

437
    let to_dyn = function
438
      | File b -> Dyn.variant "File" [ File.to_dyn b ]
×
439
      | Commit c -> Dyn.variant "Commit" [ Commit.to_dyn c ]
×
440
    ;;
441
  end
442

443
  include T
444
  module C = Comparable.Make (T)
445
  module Set = C.Set
446

447
  let parse =
448
    let re =
449
      let space = Re.(rep1 space) in
39✔
450
      let perm = Re.(rep1 digit) in
39✔
451
      let hash = Re.(rep1 alnum) in
39✔
452
      let type_ = Re.(rep1 alpha) in
39✔
453
      let size = Re.(alt [ rep1 digit; str "-" ]) in
39✔
454
      let path = Re.(rep1 any) in
39✔
455
      [ perm
456
      ; space
457
      ; Re.group type_
39✔
458
      ; space
459
      ; Re.group hash
39✔
460
      ; space
461
      ; Re.group size
39✔
462
      ; space
463
      ; Re.group path
39✔
464
      ]
465
      |> Re.seq
466
      |> Re.compile
39✔
467
    in
468
    fun line ->
469
      Re.exec_opt re line
×
470
      |> Option.bind ~f:(fun m ->
×
471
        match Re.Group.get m 1 with
×
472
        | "blob" ->
×
473
          Some
474
            (File
475
               (Direct
476
                  { hash = Re.Group.get m 2
×
477
                  ; size = Int.of_string_exn @@ Re.Group.get m 3
×
478
                  ; path = Path.Local.of_string @@ Re.Group.get m 4
×
479
                  }))
480
        | "commit" ->
×
481
          Some
482
            (Commit
483
               { rev = Re.Group.get m 2 |> Object.of_sha1 |> Option.value_exn
×
484
               ; path = Path.Local.of_string @@ Re.Group.get m 4
×
485
               })
486
        | _ -> None)
×
487
  ;;
488
end
489

490
let fetch_allow_failure repo ~url obj =
491
  with_mutex repo obj ~f:(fun () ->
×
492
    object_exists repo obj
×
493
    >>= function
494
    | true -> Fiber.return `Fetched
×
495
    | false ->
×
496
      run_with_exit_code
×
497
        ~allow_codes:(fun x -> x = 0 || x = 128)
×
498
        repo
499
        ~display:!Dune_engine.Clflags.display
500
        [ "fetch"; "--no-write-fetch-head"; url; Object.to_string obj ]
×
501
      >>| (function
502
       | Ok 128 -> `Not_found
×
503
       | Ok 0 ->
×
504
         Table.set repo.present_objects obj ();
505
         `Fetched
×
506
       | Error git_error -> Git_error.raise_code_error git_error
×
507
       | _ -> assert false))
508
;;
509

510
let fetch repo ~url obj =
511
  fetch_allow_failure repo ~url obj
×
512
  >>| function
513
  | `Fetched -> ()
×
514
  | `Not_found ->
×
515
    User_error.raise [ Pp.textf "unable to fetch %S from %S" (Object.to_string obj) url ]
×
516
;;
517

518
module At_rev = struct
519
  type repo = t
520

521
  type t =
522
    { repo : repo
523
    ; revision : Object.t
524
    ; files : File.Set.t
525
    }
526

527
  let equal x y = Object.equal x.revision y.revision
×
528
  let rev t = t.revision
×
529

530
  module Config = struct
531
    type bindings = string * string
532

533
    type section =
534
      { name : string
535
      ; arg : string option
536
      ; bindings : bindings list
537
      }
538

539
    type t = section list
540

541
    module KV = struct
542
      module T = struct
543
        type t = string * string option
544

545
        let compare = Tuple.T2.compare String.compare (Option.compare String.compare)
39✔
546
        let to_dyn = Tuple.T2.to_dyn Dyn.string (Dyn.option Dyn.string)
39✔
547
      end
548

549
      include Comparable.Make (T)
550
    end
551

552
    let parse line =
553
      let open Option.O in
×
554
      let* key, value = String.lsplit2 ~on:'=' line in
×
555
      let+ section, key = String.lsplit2 ~on:'.' key in
×
556
      let arg, binding =
×
557
        match String.rsplit2 ~on:'.' key with
558
        | None -> None, key
×
559
        | Some (arg, binding) -> Some arg, binding
×
560
      in
561
      section, arg, binding, value
562
    ;;
563

564
    let config repo (Object.Sha1 rev) path : t Fiber.t =
565
      [ "config"; "--list"; "--blob"; sprintf "%s:%s" rev (Path.Local.to_string path) ]
×
566
      |> run_capture_lines repo ~display:Quiet
×
567
      >>| Git_error.result_get_or_code_error
×
568
      >>| List.fold_left ~init:KV.Map.empty ~f:(fun acc line ->
×
569
        match parse line with
×
570
        | None ->
×
571
          Code_error.raise "Couldn't parse git config line" [ "line", Dyn.string line ]
×
572
        | Some (section, arg, binding, value) ->
×
573
          KV.Map.update acc (section, arg) ~f:(function
574
            | None -> Some [ binding, value ]
×
575
            | Some xs -> Some ((binding, value) :: xs)))
×
576
      >>| KV.Map.foldi ~init:[] ~f:(fun (name, arg) bindings acc ->
577
        let section = { name; arg; bindings } in
×
578
        section :: acc)
579
    ;;
580
  end
581

582
  module Submodule = struct
583
    (* a submodule in [.gitmodules] can also have a [branch] but given we only
584
       need to resolve the commit object, we don't have to care about the
585
       tracking branch *)
586
    type t =
587
      { path : Path.Local.t
588
      ; source : string
589
      }
590

591
    let parse repo revision =
592
      let submodule_path = Path.Local.of_string ".gitmodules" in
×
593
      let* has_submodules = mem_path repo revision submodule_path in
×
594
      match has_submodules with
×
595
      | false -> Fiber.return []
×
596
      | true ->
×
597
        let+ cfg = Config.config repo revision submodule_path in
×
598
        List.filter_map cfg ~f:(function
×
599
          | { Config.name = "submodule"; arg = _; bindings } ->
×
600
            let find_key key (k, v) =
601
              match String.equal k key with
×
602
              | true -> Some v
×
603
              | false -> None
×
604
            in
605
            let path = List.find_map bindings ~f:(find_key "path") in
×
606
            let url = List.find_map bindings ~f:(find_key "url") in
×
607
            (match path, url with
×
608
             | Some path, Some source ->
×
609
               (* CR-rginberg: we need to handle submodule paths that try to escape
610
                  the repo *)
611
               let path = Path.Local.of_string path in
612
               Some { path; source }
×
613
             | _, _ ->
×
614
               (* CR-Leonidas-from-XIV: Loc.t for the .gitmodules? *)
615
               User_error.raise
616
                 ~hints:[ Pp.text "Make sure all git submodules specify path & url" ]
×
617
                 [ Pp.text "Submodule definition missing path or url" ])
×
618
          | _otherwise -> None)
×
619
    ;;
620
  end
621

622
  let files_and_submodules repo (Object.Sha1 rev) =
623
    run_capture_zero_separated_lines repo [ "ls-tree"; "-z"; "--long"; "-r"; rev ]
×
624
    >>| Git_error.result_get_or_code_error
×
625
    >>| List.fold_left
626
          ~init:(File.Set.empty, Commit.Set.empty)
627
          ~f:(fun (files, commits) line ->
628
            match Entry.parse line with
×
629
            | None -> files, commits
×
630
            | Some (File file) -> File.Set.add files file, commits
×
631
            | Some (Commit commit) -> files, Commit.Set.add commits commit)
×
632
  ;;
633

634
  let path_commit_map submodules =
635
    Commit.Set.fold
×
636
      submodules
637
      ~init:Path.Local.Map.empty
638
      ~f:(fun { Commit.path; rev } m ->
639
        match Path.Local.Map.add m path rev with
×
640
        | Ok m -> m
×
641
        | Error (Sha1 existing_rev) ->
×
642
          let (Sha1 found_rev) = rev in
643
          User_error.raise
644
            [ Pp.textf
×
645
                "Path %s specified multiple times as submodule pointing to different \
646
                 commits: %s and %s"
647
                (Path.Local.to_string path)
×
648
                found_rev
649
                existing_rev
650
            ])
651
  ;;
652

653
  let rec of_rev repo ~revision =
654
    let* files, submodules = files_and_submodules repo revision in
×
655
    let+ files =
×
656
      let commit_paths = path_commit_map submodules in
657
      let* submodules = Submodule.parse repo revision in
×
658
      (* It's not safe to do a parallel map because adding a remote
659
         requires getting the lock (which we're now holding) *)
660
      submodules
×
661
      |> Fiber.sequential_map ~f:(fun { Submodule.path; source } ->
×
662
        match Path.Local.Map.find commit_paths path with
×
663
        | None ->
×
664
          User_error.raise
665
            ~hints:
666
              [ Pp.text
×
667
                  "Make sure the submodule is initialized and committed in the source \
668
                   repository"
669
              ]
670
            [ Pp.textf
×
671
                "Submodule definition %s references non-existing path %s in repo"
672
                source
673
                (Path.Local.to_string path)
×
674
            ]
675
        | Some revision ->
×
676
          let* () = fetch repo ~url:source revision in
×
677
          let+ at_rev = of_rev repo ~revision in
×
678
          File.Set.map at_rev.files ~f:(fun file ->
×
679
            let path = Path.Local.append path (File.path file) in
×
680
            File.Redirect { path; to_ = file }))
×
681
      >>| List.cons files
×
682
      >>| File.Set.union_all
683
    in
684
    { repo; revision; files }
×
685
  ;;
686

687
  let content { repo; revision; files = _ } path = show repo [ `Path (revision, path) ]
×
688

689
  let directory_entries_recursive t path =
690
    (* TODO: there are much better ways of implementing this:
691
       1. using libgit or ocamlgit
692
       2. possibly using [$ git archive] *)
693
    File.Set.to_list t.files
×
694
    |> List.filter_map ~f:(fun (file : File.t) ->
×
695
      let file_path = File.path file in
×
696
      (* [directory_entries "foo"] shouldn't return "foo" as an entry, but
697
         "foo" is indeed a descendant of itself. So we filter it manually. *)
698
      if (not (Path.Local.equal file_path path))
×
699
         && Path.Local.is_descendant file_path ~of_:path
×
700
      then Some file
×
701
      else None)
×
702
    |> File.Set.of_list
×
703
  ;;
704

705
  let directory_entries_immediate t path =
706
    (* TODO: there are much better ways of implementing this:
707
       1. using libgit or ocamlgit
708
       2. possibly using [$ git archive] *)
709
    File.Set.filter t.files ~f:(fun (file : File.t) ->
×
710
      match Path.Local.parent (File.path file) with
×
711
      | None -> false
×
712
      | Some p -> Path.Local.equal p path)
×
713
  ;;
714

715
  let directory_entries t ~recursive path =
716
    (if recursive then directory_entries_recursive else directory_entries_immediate)
×
717
      t
718
      path
719
  ;;
720

721
  let check_out { repo = { dir; _ }; revision = Sha1 rev; files = _ } ~target =
722
    (* TODO iterate over submodules to output sources *)
723
    let git = Lazy.force Vcs.git in
×
724
    let temp_dir = Temp_dir.dir_for_target ~target ~prefix:"rev-store" ~suffix:rev in
×
725
    Fiber.finalize ~finally:(fun () ->
726
      let+ () = Fiber.return () in
×
727
      Temp.destroy Dir temp_dir)
×
728
    @@ fun () ->
729
    let archive = Path.relative temp_dir "archive.tar" in
×
730
    let stdout_to = Process.Io.file archive Process.Io.Out in
×
731
    let stderr_to = make_stderr () in
×
732
    let* () =
×
733
      let args = [ "archive"; "--format=tar"; rev ] in
734
      let+ (), exit_code =
735
        Process.run ~dir ~display:Quiet ~stdout_to ~stderr_to ~env failure_mode git args
×
736
      in
737
      if exit_code <> 0
×
738
      then Git_error.raise_code_error { dir; args; exit_code; output = [] }
×
739
    in
740
    (* We untar things into a temp dir to make sure we don't create garbage
741
       in the build dir until we know can produce the files *)
742
    let target_in_temp_dir = Path.relative temp_dir "dir" in
×
743
    Tar.extract ~archive ~target:target_in_temp_dir
×
744
    >>| function
745
    | Error () -> User_error.raise [ Pp.text "failed to untar archive created by git" ]
×
746
    | Ok () ->
×
747
      Path.mkdir_p (Path.parent_exn target);
×
748
      Path.rename target_in_temp_dir target
×
749
  ;;
750
end
751

752
let remote =
753
  let hash = Re.(rep1 alnum) in
39✔
754
  let head_mark, head = Re.mark (Re.str "HEAD") in
39✔
755
  let ref = Re.(group (seq [ str "refs/"; rep1 any ])) in
39✔
756
  let re = Re.(compile @@ seq [ bol; group hash; rep1 space; alt [ head; ref ] ]) in
39✔
757
  fun t ~url:(url_loc, url) ->
758
    let f url =
×
759
      let command = [ "ls-remote"; url ] in
×
760
      let refs =
761
        Fiber_lazy.create (fun () ->
762
          let+ hits =
×
763
            run_capture_lines t ~display:!Dune_engine.Clflags.display command
×
764
            >>| function
×
765
            | Ok lines -> lines
×
766
            | Error git_error ->
×
767
              (match git_error.exit_code with
768
               | 128 ->
×
769
                 User_error.raise
770
                   ~loc:url_loc
771
                   ~hints:
772
                     [ Pp.textf
×
773
                         "Check that this Git URL in the project configuration is \
774
                          correct: %S"
775
                         url
776
                     ]
777
                   [ Pp.text "Failed to run external command:"
×
778
                   ; User_message.command (sprintf "git ls-remote %S" url)
×
779
                   ]
780
               | _ -> Git_error.raise_code_error git_error)
×
781
          in
782
          let default_branch, refs =
×
783
            List.fold_left hits ~init:(None, []) ~f:(fun (default_branch, refs) line ->
784
              match Re.exec_opt re line with
×
785
              | None -> default_branch, refs
×
786
              | Some group ->
×
787
                let hash = Re.Group.get group 1 |> Object.of_sha1 |> Option.value_exn in
×
788
                if Re.Mark.test group head_mark
×
789
                then Some hash, refs
×
790
                else (
×
791
                  let name = Re.Group.get group 2 in
792
                  let entry = name, hash in
×
793
                  default_branch, entry :: refs))
794
          in
795
          default_branch, String.Map.of_list_exn refs)
×
796
      in
797
      { Remote.url
×
798
      ; default_branch = Fiber_lazy.force refs >>| fst
×
799
      ; refs = Fiber_lazy.force refs >>| snd
×
800
      }
801
    in
802
    Table.find_or_add t.remotes ~f url
803
;;
804

805
let fetch_resolved t (remote : Remote.t) revision =
806
  let* () = fetch t ~url:remote.url revision in
×
807
  At_rev.of_rev t ~revision
×
808
;;
809

810
let resolve_revision t (remote : Remote.t) ~revision =
811
  let* refs = remote.refs in
×
812
  let obj =
×
813
    match String.Map.find refs revision with
814
    | Some _ as obj -> obj
×
815
    | None ->
×
816
      (* revision was not found as-is, try formatting as branch/tag *)
817
      let lookup_in format = String.Map.find refs (sprintf format revision) in
×
818
      let as_branch = lookup_in "refs/heads/%s" in
819
      let as_tag = lookup_in "refs/tags/%s" in
×
820
      (match as_branch, as_tag with
×
821
       | (Some _ as obj), None -> obj
×
822
       | None, (Some _ as obj) -> obj
×
823
       | None, None -> None
×
824
       | Some branch_obj, Some tag_obj ->
×
825
         (match Object.equal branch_obj tag_obj with
826
          | true -> Some branch_obj
×
827
          | false ->
×
828
            let hints =
829
              [ Pp.textf "If you want to specify a tag use refs/tags/%s" revision
×
830
              ; Pp.textf "If you want to specify a branch use refs/branches/%s" revision
×
831
              ]
832
            in
833
            User_error.raise
×
834
              ~hints
835
              [ Pp.textf "Reference %S in remote %S is ambiguous" revision remote.url ]))
×
836
  in
837
  match obj with
838
  | Some obj as s ->
×
839
    let+ () = fetch t ~url:remote.url obj in
×
840
    s
×
841
  | None ->
×
842
    rev_parse t revision
×
843
    >>= (function
844
     | None -> Fiber.return None
×
845
     | Some obj -> resolve_object t obj)
×
846
;;
847

848
let fetch_object t (remote : Remote.t) revision =
849
  fetch_allow_failure t ~url:remote.url revision
×
850
  >>= function
851
  | `Not_found -> Fiber.return None
×
852
  | `Fetched -> At_rev.of_rev t ~revision >>| Option.some
×
853
;;
854

855
let content_of_files t files =
856
  match files with
×
857
  | [] -> Fiber.return []
×
858
  | _ :: _ ->
×
859
    let+ out =
860
      List.map files ~f:(fun file -> `Object (File.hash file))
×
861
      |> show t
×
862
      >>| function
×
863
      | Some s -> s
×
864
      | None ->
×
865
        Code_error.raise
866
          "content_of_files failed"
867
          [ "files", Dyn.(list File.to_dyn) files ]
×
868
    in
869
    let rec loop acc pos = function
×
870
      | [] ->
×
871
        assert (pos = String.length out);
×
872
        acc
873
      | (file : File.t) :: files ->
×
874
        let size = File.size file in
875
        let acc = String.sub out ~pos ~len:size :: acc in
×
876
        loop acc (pos + size) files
877
    in
878
    List.rev (loop [] 0 files)
×
879
;;
880

881
let get =
882
  Fiber_lazy.create (fun () ->
883
    let dir =
×
884
      Path.L.relative
885
        (Path.of_string (Xdg.cache_dir (Lazy.force Dune_util.xdg)))
×
886
        [ "dune"; "git-repo" ]
887
    in
888
    load_or_create ~dir)
×
889
  |> Fiber_lazy.force
39✔
890
;;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc