gitscope
← rgit / 0.1.1 / src/database/indexer.rs
https://dev.kronoz.win/r/rgit.git
src/database/indexer.rs · Rust
Blame Raw
  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}