//! Forks (hub spec ยง4): making one, listing them, and proposing from one //! back to the repository it came from. use super::settings::{csrf, require}; use super::{filters, lookup, Error, Page, RepoShell, Shell}; use crate::auth::{roles, Viewer}; use crate::db::{self, accounts}; use crate::repos::RepoName; use crate::Shared; use askama::Template; use axum::extract::{Form, Path, State}; use axum::response::{IntoResponse, Redirect, Response}; use serde::Deserialize; #[derive(Template)] #[template(path = "fork_new.html")] pub struct ForkPage { shell: Shell, repo: RepoShell, owners: Vec, name: String, error: Option, } pub async fn form(State(app): State, viewer: Viewer, Path((owner, name)): Path<(String, String)>) -> Result, Error> { let shell = Shell::new(&app, &format!("/{owner}/{name}/fork"), &viewer); let me = require(&viewer, &shell)?; let (_, row, role) = lookup(&app, &shell, &viewer, &owner, &name).await?; let owners = super::repo_settings::owners_for(&app, &me.account).await; Ok(Page(ForkPage { repo: RepoShell::new(&app, &row, "code").with_role(role), shell, owners, name, error: None })) } #[derive(Deserialize)] pub struct ForkForm { #[serde(rename = "_csrf")] csrf_token: String, owner: String, name: String, } /// Anyone who can read a repository may fork it under an owner they may /// create repositories for; the fork keeps the source's visibility. pub async fn create( State(app): State, viewer: Viewer, Path((owner, name)): Path<(String, String)>, Form(f): Form, ) -> Result { let shell = Shell::new(&app, &format!("/{owner}/{name}/fork"), &viewer); let me = require(&viewer, &shell)?; csrf(&viewer, &f.csrf_token, &shell)?; let (source, row, role) = lookup(&app, &shell, &viewer, &owner, &name).await?; let owners = super::repo_settings::owners_for(&app, &me.account).await; let page = |msg: &str| { Page(ForkPage { shell: shell.clone(), repo: RepoShell::new(&app, &row, "code").with_role(role), owners: owners.clone(), name: f.name.clone(), error: Some(msg.into()), }) .into_response() }; let Some(dest) = RepoName::new(f.owner.trim(), f.name.trim()) else { return Ok(page("A repository name is letters, digits, dots, dashes and underscores.")); }; if dest == source { return Ok(page("Pick another owner or name: a repository cannot be its own fork.")); } if !roles::may_create_under(&app.db, &me.account, &dest.owner).await.map_err(|e| Error::from_db(shell.clone(), e))? { return Ok(page("You cannot create repositories under that owner.")); } if db::repos::get(&app.db, &dest.owner, &dest.name).await.map_err(|e| Error::from_db(shell.clone(), e))?.is_some() || app.repos.exists(&dest) { return Ok(page("That repository exists.")); } app.repos.fork(&source, &dest).await.map_err(|e| Error::from_repo(shell.clone(), e))?; let created = db::repos::create(&app.db, &dest.owner, &dest.name, &row.description, row.visibility()) .await .map_err(|e| Error::from_db(shell.clone(), e))?; db::repos::set_forked_from(&app.db, created.id, row.id).await.map_err(|e| Error::from_db(shell.clone(), e))?; accounts::audit(&app.db, Some(&me.account), "repo.forked", &dest.to_string(), &source.to_string()).await.ok(); Ok(Redirect::to(&dest.url()).into_response()) } #[derive(Template)] #[template(path = "forks.html")] pub struct ForksPage { shell: Shell, repo: RepoShell, forks: Vec, } pub async fn list( State(app): State, viewer: Viewer, Path((owner, name)): Path<(String, String)>, ) -> Result, Error> { let shell = Shell::new(&app, &format!("/{owner}/{name}/forks"), &viewer); let (_, row, role) = lookup(&app, &shell, &viewer, &owner, &name).await?; let all = db::repos::forks_of(&app.db, row.id).await.map_err(|e| Error::from_db(shell.clone(), e))?; let forks = super::home::visible(&app, &shell, &viewer, all).await?; Ok(Page(ForksPage { repo: RepoShell::new(&app, &row, "code").with_role(role), shell, forks })) } /// What the overview says about forks: where this one came from (when the /// viewer may see it) and how many forks it has. pub struct ForkInfo { pub upstream: Option, pub forks: usize, } pub async fn info(app: &Shared, shell: &Shell, viewer: &Viewer, row: &db::repos::RepoRow) -> ForkInfo { let upstream = match row.forked_from { Some(id) => match db::repos::get_by_id(&app.db, id).await { Ok(Some(up)) => super::home::visible(app, shell, viewer, vec![up]).await.ok().and_then(|mut v| v.pop()), _ => None, }, None => None, }; let forks = match db::repos::forks_of(&app.db, row.id).await { Ok(all) => super::home::visible(app, shell, viewer, all).await.map(|v| v.len()).unwrap_or(0), Err(_) => 0, }; ForkInfo { upstream, forks } }