>(
repo: &github::Repo,
db: &database::Db,
mirror_root: &str,
base_cgitrc: Option,
max_repo_size_bytes: Option,
) -> anyhow::Result<()> {
if let Some(max_repo_size_bytes) = max_repo_size_bytes {
if is_repo_oversize(repo.size, max_repo_size_bytes) {
return Ok(());
}
}
let id = repo.id;
let path = clone_path(&mirror_root, &repo);
let db_repo = database::Repo::from(repo);
match db.repo_get(id) {
// If we've already seen the repo and it's been updated, fetch the
// latest.
Ok(current_repo) => {
if db.repo_is_updated(&db_repo)? {
update(&path, ¤t_repo, &repo)?;
db.repo_update(&db_repo)?;
}
},
// If the repo doesn't exist, mirror it and store it in the
// database.
Err(database::Error::Db(rusqlite::Error::QueryReturnedNoRows)) => {
mirror(
path,
&repo,
base_cgitrc,
)?;
db.repo_insert(db_repo)?;
},
Err(e) => anyhow::bail!(e),
}
Ok(())
}
/// Return `true` if `size_kilobytes` is larger than `max_repo_size_bytes`.
fn is_repo_oversize(
size_kilobytes: u64,
max_repo_size_bytes: u64,
) -> bool {
let size_bytes = size_kilobytes * 1000;
if size_bytes > max_repo_size_bytes {
return true;
}
false
}
/// Get the clone path for a repository.
///
/// If `repo` is a fork, add `/fork/` to `base_path`.
fn clone_path>(base_path: P, repo: &github::Repo) -> PathBuf {
let git_dir = format!("{}.git", repo.name);
if repo.fork {
base_path
.as_ref()
.join("fork")
.join(git_dir)
} else {
base_path
.as_ref()
.join(git_dir)
}
}
/// Mirror a repository.
fn mirror(
clone_path: P1,
repo: &github::Repo,
base_cgitrc: Option,
) -> anyhow::Result<()>
where
P1: AsRef,
P2: AsRef,
{
git::mirror(
&repo.clone_url,
&clone_path,
repo.description(),
&repo.default_branch,
)?;
// Copy the base cgitrc file into the newly-cloned repository.
if let Some(base_cgitrc) = base_cgitrc {
let cgitrc_path = clone_path.as_ref().join("cgitrc");
fs::copy(&base_cgitrc, &cgitrc_path)
.with_context(|| format!(
"unable to copy '{}' to '{}'",
"./cgitrc",
&cgitrc_path.display(),
))?;
}
if repo.default_branch != "master" {
repo_cgitrc_set_defbranch(&clone_path, &repo.default_branch)?;
}
update_mtime(&clone_path, &repo)?;
Ok(())
}
/// Update a previously-mirrored repository.
fn update>(
repo_path: P,
current_repo: &database::Repo,
updated_repo: &github::Repo,
) -> anyhow::Result<()> {
git::update(&repo_path)?;
let remote_description = updated_repo.description();
if current_repo.description() != remote_description {
git::update_description(&repo_path, remote_description)?;
}
if let Some(default_branch) = ¤t_repo.default_branch {
if default_branch != &updated_repo.default_branch {
git::change_current_branch(
&repo_path,
&updated_repo.default_branch,
)?;
repo_cgitrc_set_defbranch(&repo_path, &updated_repo.default_branch)?;
}
}
update_mtime(&repo_path, &updated_repo)?;
Ok(())
}
/// Set the mtime of the repository to GitHub's `pushed_at` time.
///
/// Used for CGit "age" sorting.
fn update_mtime>(
repo_path: P,
repo: &github::Repo,
) -> anyhow::Result<()> {
let update_time = filetime::FileTime::from_system_time(
DateTime::parse_from_rfc3339(&repo.pushed_at)
.with_context(|| format!(
"unable to parse update time from '{}'",
&repo.pushed_at,
))?
.into()
);
let default_branch_ref = repo_path
.as_ref()
.join("refs/heads")
.join(&repo.default_branch);
// Try updating times on the default ref.
match filetime::set_file_times(
&default_branch_ref,
update_time,
update_time,
) {
Ok(_) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => {
// If the default ref file doesn't exist, update times on the
// 'packed-refs' file.
let packed_refs_path = repo_path
.as_ref()
.join("packed-refs");
match filetime::set_file_times(
&packed_refs_path,
update_time,
update_time,
) {
Ok(_) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => {
// In the absence of a 'packed-refs' file, create a CGit
// agefile and add the update time to it.
Ok(set_agefile_time(&repo_path, &repo.pushed_at)?)
},
Err(e) => Err(e),
}
.with_context(|| format!(
"unable to set mtime on '{}'",
&packed_refs_path.display(),
))?;
Ok(())
},
Err(e) => Err(e),
}
.with_context(|| format!(
"unable to set mtime on '{}'",
&default_branch_ref.display(),
))?;
Ok(())
}
/// Write `update_time` into the repo's `info/web/last-modified` file.
fn set_agefile_time>(
repo_path: P,
update_time: &str,
) -> anyhow::Result<()> {
let agefile_dir = repo_path.as_ref().join("info/web");
fs::DirBuilder::new()
.create(&agefile_dir)
.with_context(|| format!(
"unable to create directory '{}'",
&agefile_dir.display(),
))?;
let agefile_path = agefile_dir.join("last-modified");
let mut agefile = fs::OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(&agefile_path)
.with_context(|| format!(
"unable to open '{}'",
&agefile_path.display(),
))?;
writeln!(agefile, "{}", &update_time)
.with_context(|| format!(
"unable to write to '{}'",
&agefile_path.display(),
))?;
Ok(())
}
/// Set the default CGit branch in the repository's "cgitrc" file.
fn repo_cgitrc_set_defbranch>(
repo_path: P,
default_branch: &str,
) -> anyhow::Result<()> {
repo_cgitrc_append(
&repo_path,
&format!("defbranch={}", default_branch),
)?;
Ok(())
}
/// Append `config` to the repo-local "cgitrc" file.
fn repo_cgitrc_append>(
repo_path: P,
config: &str,
) -> anyhow::Result<()> {
let cgitrc_path = repo_path
.as_ref()
.join("cgitrc");
let mut cgitrc_file = fs::OpenOptions::new()
.append(true)
.create(true)
.open(&cgitrc_path)
.with_context(|| format!(
"unable to open '{}'",
&cgitrc_path.display(),
))?;
writeln!(cgitrc_file, "{}", config)
.with_context(|| format!(
"unable to write to '{}'",
&cgitrc_path.display(),
))?;
Ok(())
}