|
| 1 | +open Stdbob |
| 2 | + |
| 3 | +let src = Logs.Src.create "bob.git" |
| 4 | + |
| 5 | +module Log = (val Logs.src_log src : Logs.LOG) |
| 6 | + |
| 7 | +module SHA1 = struct |
| 8 | + include Digestif.SHA1 |
| 9 | + |
| 10 | + let hash x = Hashtbl.hash x |
| 11 | + let length = digest_size |
| 12 | + let feed = feed_bigstring |
| 13 | + let null = digest_string "" |
| 14 | + let compare a b = String.compare (to_raw_string a) (to_raw_string b) |
| 15 | + |
| 16 | + let sink_bigstring ?(ctx = empty) () = |
| 17 | + Stream.Sink.make ~init:(Fiber.always ctx) |
| 18 | + ~push:(fun ctx bstr -> Fiber.return (feed_bigstring ctx bstr)) |
| 19 | + ~stop:(Fiber.return <.> get) () |
| 20 | + |
| 21 | + let sink_string ?(ctx = empty) () = |
| 22 | + Stream.Sink.make ~init:(Fiber.always ctx) |
| 23 | + ~push:(fun ctx str -> Fiber.return (feed_string ctx str)) |
| 24 | + ~stop:(Fiber.return <.> get) () |
| 25 | +end |
| 26 | + |
| 27 | +let v_space = Astring.String.Sub.of_string " " |
| 28 | +let v_null = Astring.String.Sub.of_string "\x00" |
| 29 | + |
| 30 | +let tree_of_string ?path str = |
| 31 | + let path_with ~name = |
| 32 | + match path with |
| 33 | + | Some path -> Bob_fpath.(path / name) |
| 34 | + | None -> Bob_fpath.v name |
| 35 | + in |
| 36 | + let one str = |
| 37 | + let open Astring.String.Sub in |
| 38 | + let ( >>= ) = Option.bind in |
| 39 | + cut ~sep:v_space str >>= fun (perm, str) -> |
| 40 | + cut ~sep:v_null str >>= fun (name, str) -> |
| 41 | + (try Some (with_range ~len:SHA1.length str) with _ -> None) |
| 42 | + >>= fun hash -> |
| 43 | + let str = with_range ~first:SHA1.length str in |
| 44 | + let hash = SHA1.of_raw_string (to_string hash) in |
| 45 | + match to_string perm with |
| 46 | + | "40000" -> Some (`Dir (path_with ~name:(to_string name), hash), str) |
| 47 | + | "100644" -> Some (`Reg (path_with ~name:(to_string name), hash), str) |
| 48 | + | _ -> failwith "Invalid kind of entry into a tree" |
| 49 | + in |
| 50 | + let rec go acc str = |
| 51 | + match one str with |
| 52 | + | Some (entry, str) -> go (entry :: acc) str |
| 53 | + | None -> List.rev acc |
| 54 | + in |
| 55 | + go [] (Astring.String.Sub.of_string str) |
| 56 | + |
| 57 | +let v_space = Cstruct.string " " |
| 58 | +let v_null = Cstruct.string "\x00" |
| 59 | + |
| 60 | +let tree_of_cstruct ?path contents = |
| 61 | + let path_with ~name = |
| 62 | + match path with |
| 63 | + | Some path -> Bob_fpath.(path / name) |
| 64 | + | None -> Bob_fpath.v name |
| 65 | + in |
| 66 | + let init () = Fiber.return (Cstruct.of_bigarray contents) in |
| 67 | + let pull contents = |
| 68 | + let ( >>= ) = Option.bind in |
| 69 | + Cstruct.cut ~sep:v_space contents >>= fun (perm, contents) -> |
| 70 | + Cstruct.cut ~sep:v_null contents >>= fun (name, contents) -> |
| 71 | + (try Some (Cstruct.sub contents 0 SHA1.length) with _ -> None) |
| 72 | + >>= fun hash -> |
| 73 | + let contents = Cstruct.shift contents SHA1.length in |
| 74 | + let hash = SHA1.of_raw_string (Cstruct.to_string hash) in |
| 75 | + match Cstruct.to_string perm with |
| 76 | + | "40000" -> |
| 77 | + let path = path_with ~name:(Cstruct.to_string name) in |
| 78 | + Some (`Dir (path, hash), contents) |
| 79 | + | "100644" -> |
| 80 | + let path = path_with ~name:(Cstruct.to_string name) in |
| 81 | + Some (`Reg (path, hash), contents) |
| 82 | + | _ -> failwith "Invalid kind of entry into a tree" |
| 83 | + in |
| 84 | + let pull = Fiber.return <.> pull in |
| 85 | + let stop = Fiber.ignore in |
| 86 | + Stream.Source { init; pull; stop } |
| 87 | + |
| 88 | +let digest ~kind ?(off = 0) ?len buf = |
| 89 | + let len = |
| 90 | + match len with Some len -> len | None -> Bigarray.Array1.dim buf - off |
| 91 | + in |
| 92 | + let ctx = SHA1.empty in |
| 93 | + let ctx = |
| 94 | + match kind with |
| 95 | + | `A -> SHA1.feed_string ctx (Fmt.str "commit %d\000" len) |
| 96 | + | `B -> SHA1.feed_string ctx (Fmt.str "tree %d\000" len) |
| 97 | + | `C -> SHA1.feed_string ctx (Fmt.str "blob %d\000" len) |
| 98 | + | `D -> SHA1.feed_string ctx (Fmt.str "mesg %d\000" len) |
| 99 | + in |
| 100 | + let ctx = SHA1.feed_bigstring ctx ~off ~len buf in |
| 101 | + SHA1.get ctx |
| 102 | + |
| 103 | +let hash_of_root ~real_length ~root hash = |
| 104 | + let str = |
| 105 | + Fmt.str "%s\000%s%d" (Bob_fpath.basename root) (SHA1.to_raw_string hash) |
| 106 | + real_length |
| 107 | + in |
| 108 | + let hdr = Fmt.str "commit %d\000" (String.length str) in |
| 109 | + SHA1.digest_string (hdr ^ str) |
| 110 | + |
| 111 | +module Filesystem = struct |
| 112 | + let readdir = |
| 113 | + let readdir d = |
| 114 | + try Sys.readdir (Bob_fpath.to_string d) with _exn -> [||] |
| 115 | + in |
| 116 | + Array.to_list <.> readdir |
| 117 | + |
| 118 | + let rec traverse ~get ~add visited stack ~f acc = |
| 119 | + match stack with |
| 120 | + | [] -> Fiber.return acc |
| 121 | + | x :: r -> |
| 122 | + if List.exists (Bob_fpath.equal x) visited then |
| 123 | + traverse ~get ~add visited r ~f acc |
| 124 | + else |
| 125 | + let open Fiber in |
| 126 | + let contents = get x in |
| 127 | + traverse ~get ~add (x :: visited) (add contents stack) ~f acc >>= f x |
| 128 | + |
| 129 | + let fold ?(dotfiles = false) ~f acc paths = |
| 130 | + let dir_child d acc bname = |
| 131 | + if (not dotfiles) && bname.[0] = '.' then acc |
| 132 | + else Bob_fpath.(d / bname) :: acc |
| 133 | + in |
| 134 | + let add stack vs = vs @ stack in |
| 135 | + let get path = |
| 136 | + let entries = readdir path in |
| 137 | + List.fold_left (dir_child path) [] entries |
| 138 | + in |
| 139 | + traverse ~get ~add [] paths ~f acc |
| 140 | + |
| 141 | + let fold ?dotfiles ~f acc d = fold ?dotfiles ~f acc [ d ] |
| 142 | +end |
| 143 | + |
| 144 | +let serialize_directory entries = |
| 145 | + let entries = |
| 146 | + List.sort (fun (a, _) (b, _) -> Bob_fpath.compare a b) entries |
| 147 | + in |
| 148 | + let open Stream in |
| 149 | + let open Stream in |
| 150 | + Stream.of_list entries >>= fun (p, hash) -> |
| 151 | + match Bob_fpath.is_dir_path p with |
| 152 | + | true -> |
| 153 | + Stream.of_list |
| 154 | + [ |
| 155 | + "40000 "; |
| 156 | + Bob_fpath.(to_string (rem_empty_seg p)); |
| 157 | + "\x00"; |
| 158 | + SHA1.to_raw_string hash; |
| 159 | + ] |
| 160 | + |> Fiber.return |
| 161 | + | false -> |
| 162 | + Stream.of_list |
| 163 | + [ "100644 "; Bob_fpath.to_string p; "\x00"; SHA1.to_raw_string hash ] |
| 164 | + |> Fiber.return |
| 165 | + |
| 166 | +let hash_of_directory ~root:_ rstore path = |
| 167 | + let entries = Filesystem.readdir path in |
| 168 | + let entries = |
| 169 | + List.filter_map |
| 170 | + (fun entry -> |
| 171 | + let key = Bob_fpath.(path / entry) in |
| 172 | + match Hashtbl.find_opt rstore key with |
| 173 | + | Some (hash, `Dir) -> Some (Bob_fpath.(to_dir_path (v entry)), hash) |
| 174 | + | Some (hash, `Reg) -> Some (Bob_fpath.v entry, hash) |
| 175 | + | Some (_, `Root) -> None |
| 176 | + | None -> None) |
| 177 | + entries |
| 178 | + in |
| 179 | + let open Fiber in |
| 180 | + let open Stream in |
| 181 | + Stream.to_string (serialize_directory entries) >>= fun str -> |
| 182 | + Log.debug (fun m -> m "Serialization of %a:" Bob_fpath.pp path); |
| 183 | + Log.debug (fun m -> m "@[<hov>%a@]" (Hxd_string.pp Hxd.default) str); |
| 184 | + let hdr = Fmt.str "tree %d\000" (String.length str) in |
| 185 | + Stream.(into (SHA1.sink_string ()) (double hdr str)) |
| 186 | + |
| 187 | +let hash_of_filename path = |
| 188 | + let open Fiber in |
| 189 | + let open Stream in |
| 190 | + let len = Unix.(stat (Bob_fpath.to_string path)).Unix.st_size in |
| 191 | + let hdr = Fmt.str "blob %d\000" len in |
| 192 | + let ctx = SHA1.feed_string SHA1.empty hdr in |
| 193 | + Stream.of_file path >>= function |
| 194 | + | Error (`Msg err) -> Fmt.failwith "%s." err |
| 195 | + | Ok stream -> Stream.(into (SHA1.sink_bigstring ~ctx ()) stream) |
0 commit comments