steid

@jamesgill /

5.4 KBCode·Blame·Raw
1//! Reading a repository's contents for display.
2//!
3//! Authorization is not re-implemented here: every entry point goes through
4//! [`view_repo`](super::repo::view_repo), so a repository invisible on its page is
5//! invisible in its file tree, by construction rather than by remembering to check.
6
7use crate::domain::{
8 Actor, CommitSummary, ObjectId, OrgName, RefName, RepoName, RepoPath,
9 repository::{MembershipRepository, OrgRepository, RepoRepository},
10};
11
12use super::{
13 error::Result,
14 port::{Blob, GitQuery},
15 repo::view_repo,
16};
17
18/// The largest file Steid will render.
19///
20/// A page has a person waiting on it, and past a megabyte nobody is reading the file —
21/// they are waiting for a browser to lay out a megabyte of text. Bigger files are
22/// reported by size rather than shown.
23pub const MAX_BLOB_BYTES: u64 = 1024 * 1024;
24
25/// How many commits a log shows. No paging in v1; this is the whole of it.
26pub const LOG_LIMIT: usize = 50;
27
28/// A file, as far as it can be displayed.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct FileView {
31 pub id: ObjectId,
32 pub size: u64,
33 /// The contents, when they are text small enough to show.
34 ///
35 /// `None` covers both "not valid UTF-8" and "too large"; [`too_large`](Self::too_large)
36 /// tells them apart, because the page says something different for each.
37 pub text: Option<String>,
38 pub too_large: bool,
39}
40
41impl FileView {
42 /// Whether the file exists and is simply not displayable as text.
43 pub fn is_binary(&self) -> bool {
44 self.text.is_none() && !self.too_large
45 }
46}
47
48/// What is at a path in a repository.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum Browsed {
51 /// The repository has no commits. Distinct from an empty directory: there is
52 /// nothing to point a revision at, so the page offers push instructions rather than
53 /// an empty listing.
54 Empty,
55 Directory {
56 rev: RefName,
57 path: RepoPath,
58 /// Ordered directories-first, then case-insensitively by name.
59 entries: Vec<crate::domain::TreeEntry>,
60 },
61 File {
62 rev: RefName,
63 path: RepoPath,
64 file: FileView,
65 },
66}
67
68/// Resolves a path in a repository into whatever is there.
69///
70/// `rev` of `None` means the default branch, which is what a bare repository URL asks
71/// for.
72///
73/// `Ok(None)` means the repository is invisible, absent, or has nothing at that path —
74/// all rendered identically as 404, for the reason
75/// [`view_repo`](super::repo::view_repo) gives.
76#[allow(clippy::too_many_arguments)]
77pub async fn browse_repo(
78 handle: &OrgName,
79 name: &RepoName,
80 rev: Option<&RefName>,
81 path: &RepoPath,
82 actor: &Actor,
83 orgs: &impl OrgRepository,
84 memberships: &impl MembershipRepository,
85 repos: &impl RepoRepository,
86 queries: &impl GitQuery,
87) -> Result<Option<Browsed>> {
88 if view_repo(handle, name, actor, orgs, memberships, repos)
89 .await?
90 .is_none()
91 {
92 return Ok(None);
93 }
94
95 let Some(rev) = resolve_revision(handle, name, rev, queries).await? else {
96 return Ok(Some(Browsed::Empty));
97 };
98
99 // A directory first, because that is the common case and the cheaper question.
100 if let Some(mut entries) = queries.list_tree(handle, name, &rev, path).await? {
101 entries.sort_by_key(crate::domain::TreeEntry::ordering_key);
102
103 return Ok(Some(Browsed::Directory {
104 rev,
105 path: path.clone(),
106 entries,
107 }));
108 }
109
110 let Some(blob) = queries
111 .read_blob(handle, name, &rev, path, MAX_BLOB_BYTES)
112 .await?
113 else {
114 return Ok(None);
115 };
116
117 Ok(Some(Browsed::File {
118 rev,
119 path: path.clone(),
120 file: view_of(blob),
121 }))
122}
123
124/// The commit log for a revision, newest first.
125///
126/// `Ok(None)` on the same terms as [`browse_repo`]. An empty repository logs nothing
127/// rather than failing.
128#[allow(clippy::too_many_arguments)]
129pub async fn repo_log(
130 handle: &OrgName,
131 name: &RepoName,
132 rev: Option<&RefName>,
133 actor: &Actor,
134 orgs: &impl OrgRepository,
135 memberships: &impl MembershipRepository,
136 repos: &impl RepoRepository,
137 queries: &impl GitQuery,
138) -> Result<Option<Vec<CommitSummary>>> {
139 if view_repo(handle, name, actor, orgs, memberships, repos)
140 .await?
141 .is_none()
142 {
143 return Ok(None);
144 }
145
146 let Some(rev) = resolve_revision(handle, name, rev, queries).await? else {
147 return Ok(Some(Vec::new()));
148 };
149
150 Ok(Some(queries.log(handle, name, &rev, LOG_LIMIT).await?))
151}
152
153/// Settles which revision is being asked about.
154///
155/// `None` out means the repository has no commits at all — not that the revision was
156/// wrong, which surfaces later as nothing being found at the path.
157async fn resolve_revision(
158 handle: &OrgName,
159 name: &RepoName,
160 rev: Option<&RefName>,
161 queries: &impl GitQuery,
162) -> Result<Option<RefName>> {
163 match rev {
164 Some(rev) => Ok(Some(rev.clone())),
165 None => Ok(queries.default_branch(handle, name).await?),
166 }
167}
168
169/// Decides what can be done with a blob's bytes.
170///
171/// The port carries bytes and a size; turning those into "text", "binary" or "too big"
172/// is a display decision, so it happens here rather than in the adapter.
173fn view_of(blob: Blob) -> FileView {
174 let too_large = blob.content.is_none();
175 let text = blob.content.and_then(|bytes| String::from_utf8(bytes).ok());
176
177 FileView {
178 id: blob.id,
179 size: blob.size,
180 text,
181 too_large,
182 }
183}