1use std::{
2 collections::HashSet,
3 path::{Path, PathBuf},
4};
5
6use git2::Sort;
7use time::OffsetDateTime;
8use tracing::{info, info_span};
9
10use crate::database::schema::{
11 commit::Commit,
12 repository::{Repository, RepositoryId},
13 tag::Tag,
14};
15
16pub fn run(scan_path: &Path, db: &sled::Db) {
17 let span = info_span!("index_update");
18 let _entered = span.enter();
19
20 info!("Starting index update");
21
22 update_repository_metadata(scan_path, db);
23 update_repository_reflog(scan_path, db);
24 update_repository_tags(scan_path, db);
25
26 info!("Flushing to disk");
27
28 db.flush().unwrap();
29
30 info!("Finished index update");
31}
32
33fn update_repository_metadata(scan_path: &Path, db: &sled::Db) {
34 let mut discovered = Vec::new();
35 discover_repositories(scan_path, &mut discovered);
36
37 for repository in discovered {
38 let relative = get_relative_path(scan_path, &repository);
39
40 let id = Repository::open(db, relative)
41 .unwrap()
42 .map_or_else(|| RepositoryId::new(db), |v| v.get().id);
43 let name = relative.file_name().unwrap().to_string_lossy();
44 let description = std::fs::read(repository.join("description")).unwrap_or_default();
45 let description = Some(String::from_utf8_lossy(&description)).filter(|v| !v.is_empty());
46
47 let git_repository = git2::Repository::open(scan_path.join(relative)).unwrap();
48
49 Repository {
50 id,
51 name,
52 description,
53 owner: None, // TODO read this from config
54 last_modified: find_last_committed_time(&git_repository)
55 .unwrap_or(OffsetDateTime::UNIX_EPOCH),
56 }
57 .insert(db, relative);
58 }
59}
60
61fn find_last_committed_time(repo: &git2::Repository) -> Result<OffsetDateTime, git2::Error> {
62 let mut timestamp = OffsetDateTime::UNIX_EPOCH;
63
64 for reference in repo.references()? {
65 let Ok(commit) = reference?.peel_to_commit() else {
66 continue;
67 };
68
69 let committed_time = commit.committer().when().seconds();
70 let committed_time = OffsetDateTime::from_unix_timestamp(committed_time)
71 .unwrap_or(OffsetDateTime::UNIX_EPOCH);
72
73 if committed_time > timestamp {
74 timestamp = committed_time;
75 }
76 }
77
78 Ok(timestamp)
79}
80
81fn update_repository_reflog(scan_path: &Path, db: &sled::Db) {
82 for (relative_path, db_repository) in Repository::fetch_all(db).unwrap() {
83 let git_repository = git2::Repository::open(scan_path.join(&relative_path)).unwrap();
84
85 for reference in git_repository.references().unwrap() {
86 let reference = reference.unwrap();
87
88 let reference_name = String::from_utf8_lossy(reference.name_bytes());
89 if !reference_name.starts_with("refs/heads/") {
90 continue;
91 }
92
93 let span = info_span!(
94 "branch_index_update",
95 reference = reference_name.as_ref(),
96 repository = relative_path
97 );
98 let _entered = span.enter();
99
100 info!("Refreshing indexes");
101
102 let commit_tree = db_repository
103 .get()
104 .commit_tree(db, &reference_name)
105 .unwrap();
106
107 if let (Some(latest_indexed), Ok(latest_commit)) =
108 (commit_tree.fetch_latest_one(), reference.peel_to_commit())
109 {
110 if latest_commit.id().as_bytes() == &*latest_indexed.get().hash {
111 info!("No commits since last index");
112 continue;
113 }
114 }
115
116 // TODO: only scan revs from the last time we looked
117 let mut revwalk = git_repository.revwalk().unwrap();
118 revwalk.set_sorting(Sort::REVERSE).unwrap();
119 revwalk.push_ref(&reference_name).unwrap();
120
121 let mut i = 0;
122 for rev in revwalk {
123 let commit = git_repository.find_commit(rev.unwrap()).unwrap();
124 let author = commit.author();
125 let committer = commit.committer();
126
127 Commit::new(&commit, &author, &committer).insert(&commit_tree, i);
128 i += 1;
129 }
130
131 // a complete and utter hack to remove potentially dropped commits from our tree,
132 // we'll need to add `clear()` to sled's tx api to remove this
133 for to_remove in (i + 1)..(i + 100) {
134 commit_tree.remove(to_remove.to_be_bytes()).unwrap();
135 }
136 }
137 }
138}
139
140fn update_repository_tags(scan_path: &Path, db: &sled::Db) {
141 for (relative_path, db_repository) in Repository::fetch_all(db).unwrap() {
142 let git_repository = git2::Repository::open(scan_path.join(&relative_path)).unwrap();
143
144 let tag_tree = db_repository.get().tag_tree(db).unwrap();
145
146 let git_tags: HashSet<_> = git_repository
147 .references()
148 .unwrap()
149 .filter_map(Result::ok)
150 .filter(|v| v.name_bytes().starts_with(b"refs/tags/"))
151 .map(|v| String::from_utf8_lossy(v.name_bytes()).into_owned())
152 .collect();
153 let indexed_tags: HashSet<String> = tag_tree.list().into_iter().collect();
154
155 // insert any git tags that are missing from the index
156 for tag_name in git_tags.difference(&indexed_tags) {
157 let span = info_span!(
158 "tag_index_update",
159 reference = tag_name,
160 repository = relative_path
161 );
162 let _entered = span.enter();
163
164 let reference = git_repository.find_reference(tag_name).unwrap();
165
166 if let Ok(tag) = reference.peel_to_tag() {
167 info!("Inserting newly discovered tag to index");
168
169 Tag::new(tag.tagger().as_ref()).insert(&tag_tree, tag_name);
170 }
171 }
172
173 // remove any extra tags that the index has
174 // TODO: this also needs to check peel_to_tag
175 for tag_name in indexed_tags.difference(&git_tags) {
176 let span = info_span!(
177 "tag_index_update",
178 reference = tag_name,
179 repository = relative_path
180 );
181 let _entered = span.enter();
182
183 info!("Removing stale tag from index");
184
185 tag_tree.remove(tag_name);
186 }
187 }
188}
189
190fn get_relative_path<'a>(relative_to: &Path, full_path: &'a Path) -> &'a Path {
191 full_path.strip_prefix(relative_to).unwrap()
192}
193
194fn discover_repositories(current: &Path, discovered_repos: &mut Vec<PathBuf>) {
195 let dirs = std::fs::read_dir(current)
196 .unwrap()
197 .map(|v| v.unwrap().path())
198 .filter(|path| path.is_dir());
199
200 for dir in dirs {
201 if dir.join("packed-refs").is_file() {
202 // we've hit what looks like a bare git repo, lets take it
203 discovered_repos.push(dir);
204 } else {
205 // probably not a bare git repo, lets recurse deeper
206 discover_repositories(&dir, discovered_repos);
207 }
208 }
209}