fix read packed-refs for branch resolution and listing

Store.Ref.list only enumerates loose reference files; repositories that have been garbage-collected or received as bare clones keep all refs exclusively in packed-refs, making them invisible to it. Add read_packed_refs, a shared parser for the packed-refs file, and use it in two places: - resolve_head_hash: when Store.Ref.list returns no branches, also consult packed-refs so that the last-resort fallback can find a resolvable branch. - Reference.refs_by_prefix: always merge loose refs with packed-refs entries (loose wins on duplicates) so that branches and tags appear in the UI regardless of storage format. Fixes HTTP 500 on repositories served only from pack files.

Commit
f074253ea5d03d517b16fc42650efaac0cf1c64c
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
lib/resolvers.ml
index dbf31324..c66bd020 100644..100644
@@ -208,6 +208,46 @@
208 208 let fallback_branch_candidates config =
209 209 fallback_branch_candidates_for config.Config.default_branch
210 210
211 Added: (** Parse the [packed-refs] file and return all entries as [(hex, refname)]
212 Added: pairs. This is the shared primitive used both by [resolve_head_hash] (which
213 Added: needs branch references for its fallback) and by [Reference.refs_by_prefix]
214 Added: (which enumerates branches and tags for the UI).
215 Added:
216 Added: [Store.Ref.list] only walks the filesystem for loose reference files; when a
217 Added: repository has been garbage-collected or was received as a pack, all refs
218 Added: live exclusively in [packed-refs] and are invisible to it. *)
219 Added: let read_packed_refs store =
220 Added: let dotgit = Fpath.to_string (Store.dotgit store) in
221 Added: let path = Filename.concat dotgit "packed-refs" in
222 Added: try
223 Added: In_channel.with_open_text path @@ fun ic ->
224 Added: let rec collect acc =
225 Added: match In_channel.input_line ic with
226 Added: | None -> List.rev acc
227 Added: | Some line when String.length line = 0 -> collect acc
228 Added: | Some line when line.[0] = '#' || line.[0] = '^' -> collect acc
229 Added: | Some line -> (
230 Added: match String.index_opt line ' ' with
231 Added: | None -> collect acc
232 Added: | Some i ->
233 Added: let hex = String.sub line 0 i in
234 Added: let refname =
235 Added: String.sub line (i + 1) (String.length line - i - 1)
236 Added: in
237 Added: collect ((hex, refname) :: acc))
238 Added: in
239 Added: collect []
240 Added: with Sys_error _ -> []
241 Added:
242 Added: (** Branch entries from [packed-refs], returned as [(full_refname, reference)]
243 Added: pairs suitable for [try_references]. *)
244 Added: let packed_refs_branches store =
245 Added: read_packed_refs store
246 Added: |> List.filter_map (fun (_hex, refname) ->
247 Added: if String.starts_with ~prefix:"refs/heads/" refname then
248 Added: Some (refname, Git.Reference.v refname)
249 Added: else None)
250 Added:
211 251 let resolve_head_hash repository =
212 252 let resolve reference =
213 253 map_store (Store.Ref.resolve repository.store reference)
@@ -223,8 +263,18 @@
223 263 if String.starts_with ~prefix:"refs/heads/" name then
224 264 Some (name, reference)
225 265 else None)
226 Removed: |> List.sort (fun (left, _) (right, _) -> String.compare left right)
227 266 in
267 Added: (* When the store lists no loose branches, also consult packed-refs
268 Added: so that repositories with only packed references are handled. *)
269 Added: let branches =
270 Added: if branches = [] then packed_refs_branches repository.store
271 Added: else branches
272 Added: in
273 Added: let branches =
274 Added: List.sort
275 Added: (fun (left, _) (right, _) -> String.compare left right)
276 Added: branches
277 Added: in
228 278 let rec try_branches = function
229 279 | [] ->
230 280 Lwt_result.fail
@@ -376,14 +426,33 @@
376 426 let tag_name name = drop_prefix ~prefix:"refs/tags/" name
377 427 let to_t_with_name name (_, hash) = { name; hash = Store.Hash.to_hex hash }
378 428
429 Added: (** Enumerate references matching a given prefix, combining both loose
430 Added: references (from [Store.Ref.list]) and packed references read directly
431 Added: from the [packed-refs] file. Duplicates are removed by preferring the
432 Added: loose entry (which takes precedence in git's resolution order). *)
379 433 let refs_by_prefix repository name_of_reference =
380 434 let open Lwt.Syntax in
381 435 let* references = Store.Ref.list repository.store in
382 Removed: references
383 Removed: |> List.filter_map (fun ((reference, _) as raw) ->
384 Removed: Git.Reference.to_string reference
385 Removed: |> name_of_reference
386 Removed: |> Option.map (fun name -> to_t_with_name name raw))
436 Added: let from_loose =
437 Added: references
438 Added: |> List.filter_map (fun ((reference, _) as raw) ->
439 Added: Git.Reference.to_string reference
440 Added: |> name_of_reference
441 Added: |> Option.map (fun name -> to_t_with_name name raw))
442 Added: in
443 Added: let from_packed =
444 Added: read_packed_refs repository.store
445 Added: |> List.filter_map (fun (hex, refname) ->
446 Added: name_of_reference refname
447 Added: |> Option.map (fun name -> { name; hash = hex }))
448 Added: in
449 Added: (* Merge: loose refs win over packed refs for the same name *)
450 Added: let loose_names = List.map (fun r -> r.name) from_loose in
451 Added: let merged =
452 Added: from_loose
453 Added: @ List.filter (fun r -> not (List.mem r.name loose_names)) from_packed
454 Added: in
455 Added: merged
387 456 |> List.sort (fun left right -> String.compare left.name right.name)
388 457 |> Lwt_result.return
389 458