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