feat Link files by path instead of object id

Tree rows, breadcrumbs, raw links, and image sources now use the File_at and Raw_at routes. Path links survive new commits that touch the file, and the raw handler no longer needs a whole-tree search to recover a filename for MIME detection.

Commit
7877cea42b280f01915325f560bf35b6d6dc8a9a
Author
Claude Fable 5 (high reasoning) <claude-fable-5@agents.anthropic.invalid>
Author date
Committer
Claude Fable 5 (high reasoning) <claude-fable-5@agents.anthropic.invalid>
Committer date
lib/views/repo_view.ml
index ac9b4a78..ab785e0a 100644..100644
@@ -104,9 +104,12 @@
104 104
105 105 let tree_display_limit = 3
106 106
107 Removed: let rec tree_row repo (node : Resolvers.Tree.tree_node) =
107 Added: let rec tree_row repo ~prefix (node : Resolvers.Tree.tree_node) =
108 108 let entry = node.entry in
109 Removed: let route = Routes.File (repo, entry.hash) in
109 Added: (* Entry names may span several segments when single-child directories were
110 Added: collapsed, so appending them to the prefix always yields the full path. *)
111 Added: let path = if prefix = "" then entry.name else prefix ^ "/" ^ entry.name in
112 Added: let route = Routes.File_at (repo, path) in
110 113 (* Dotfiles stay visible but are de-emphasised. *)
111 114 let modifier =
112 115 if String.length entry.name > 0 && entry.name.[0] = '.' then "tree-hidden"
@@ -125,11 +128,13 @@
125 128 if omitted = 0 then [] else [ Components.overflow_row ~route omitted ]
126 129 in
127 130 Components.directory ~modifier ~route ~name:entry.name
128 Removed: (List.map (tree_row repo) shown @ overflow)
131 Added: (List.map (tree_row repo ~prefix:path) shown @ overflow)
129 132
130 133 (** {1 Trails} *)
131 134
132 Removed: (** The path from the repository root to the entry being viewed. *)
135 Added: (** The path from the repository root to the entry being viewed. Each crumb
136 Added: links to the accumulated path, so intermediate directories resolve by name
137 Added: rather than by object id. *)
133 138 let path_trail repo (trail : (string * string) list) =
134 139 let repo_name =
135 140 match List.rev (String.split_on_char '/' repo) with
@@ -138,10 +143,16 @@
138 143 in
139 144 let root = Ui.crumb ~href:(Components.url (Files repo)) repo_name in
140 145 let entries =
141 Removed: List.map
142 Removed: (fun (name, hash) ->
143 Removed: Ui.crumb ~href:(Components.url (File (repo, hash))) name)
144 Removed: trail
146 Added: let rec go prefix acc = function
147 Added: | [] -> List.rev acc
148 Added: | (name, _hash) :: rest ->
149 Added: let path = if prefix = "" then name else prefix ^ "/" ^ name in
150 Added: let crumb =
151 Added: Ui.crumb ~href:(Components.url (File_at (repo, path))) name
152 Added: in
153 Added: go path (crumb :: acc) rest
154 Added: in
155 Added: go "" [] trail
145 156 in
146 157 Ui.breadcrumb ~class_:"path-pill" ~link_class:"path-pill-link"
147 158 ~separator_class:"path-pill-sep" ~separator:"/" (root :: entries)
@@ -235,9 +246,12 @@
235 246 ]
236 247
237 248 let files context trail (entries : Resolvers.Tree.tree_node list) =
249 Added: (* Rows link to their full path, so a listing below the root needs the trail
250 Added: as the path prefix. *)
251 Added: let prefix = String.concat "/" (List.map fst trail) in
238 252 render_page context ~active:Files
239 253 ~toolbar:[ path_trail context.repo trail ]
240 Removed: [ Ui.items_of ~id:"file-tree" (tree_row context.repo) entries ]
254 Added: [ Ui.items_of ~id:"file-tree" (tree_row context.repo ~prefix) entries ]
241 255
242 256 let is_image_filename filename =
243 257 match Filename.extension filename |> String.lowercase_ascii with
@@ -250,21 +264,20 @@
250 264 let filename =
251 265 match List.rev trail with (name, _) :: _ -> Some name | [] -> None
252 266 in
267 Added: let full_path = String.concat "/" (List.map fst trail) in
268 Added: let raw_url =
269 Added: if full_path = "" then None
270 Added: else Some (Components.url (Raw_at (context.repo, full_path)))
271 Added: in
253 272 let raw_link =
254 Removed: match List.rev trail with
255 Removed: | (_, hash) :: _ ->
256 Removed: Ui.paragraph
257 Removed: [ Components.route_link (Raw_file (context.repo, hash)) "View raw" ]
258 Removed: | [] -> Ui.nothing
273 Added: match raw_url with
274 Added: | Some href -> Ui.paragraph [ Ui.text_link ~href "View raw" ]
275 Added: | None -> Ui.nothing
259 276 in
260 277 let body =
261 278 match filename with
262 279 | Some filename when is_image_filename filename ->
263 Removed: let src =
264 Removed: match List.rev trail with
265 Removed: | (_, hash) :: _ -> Components.url (Raw_file (context.repo, hash))
266 Removed: | [] -> ""
267 Removed: in
280 Added: let src = Option.value raw_url ~default:"" in
268 281 Ui.block ~class_:"image-preview"
269 282 [ Ui.image ~class_:"file-image" ~alt:filename ~src () ]
270 283 | Some filename when Prose.Render.is_doc_filename filename ->