OXYGENHUB

Coverage / crates/hub/src/web/forks.rs 86.7%

72 of 83 lines run, 11 of 34 functions · run #496 on main · run missed

1
//! Forks (hub spec §4): making one, listing them, and proposing from one
2
//! back to the repository it came from.
3
4
use super::settings::{csrf, require};
5
use super::{filters, lookup, Error, Page, RepoShell, Shell};
6
use crate::auth::{roles, Viewer};
7
use crate::db::{self, accounts};
8
use crate::repos::RepoName;
9
use crate::Shared;
10
use askama::Template;
11
use axum::extract::{Form, Path, State};
12
use axum::response::{IntoResponse, Redirect, Response};
13
use serde::Deserialize;
14
15
#[derive(Template)]
16
#[template(path = "fork_new.html")]
17
pub struct ForkPage {
18
    shell: Shell,
19
    repo: RepoShell,
20
    owners: Vec<String>,
21
    name: String,
22
    error: Option<String>,
23
}
24
251×
pub async fn form(State(app): State<Shared>, viewer: Viewer, Path((owner, name)): Path<(String, String)>) -> Result<Page<ForkPage>, Error> {
261×
    let shell = Shell::new(&app, &format!("/{owner}/{name}/fork"), &viewer);
271×
    let me = require(&viewer, &shell)?;
281×
    let (_, row, role) = lookup(&app, &shell, &viewer, &owner, &name).await?;
291×
    let owners = super::repo_settings::owners_for(&app, &me.account).await;
301×
    Ok(Page(ForkPage { repo: RepoShell::new(&app, &row, "code").with_role(role), shell, owners, name, error: None }))
311×
}
32
33
#[derive(Deserialize)]
34
pub struct ForkForm {
35
    #[serde(rename = "_csrf")]
36
    csrf_token: String,
37
    owner: String,
38
    name: String,
39
}
40
41
/// Anyone who can read a repository may fork it under an owner they may
42
/// create repositories for; the fork keeps the source's visibility.
432×
pub async fn create(
442×
    State(app): State<Shared>,
452×
    viewer: Viewer,
462×
    Path((owner, name)): Path<(String, String)>,
472×
    Form(f): Form<ForkForm>,
482×
) -> Result<Response, Error> {
492×
    let shell = Shell::new(&app, &format!("/{owner}/{name}/fork"), &viewer);
502×
    let me = require(&viewer, &shell)?;
512×
    csrf(&viewer, &f.csrf_token, &shell)?;
522×
    let (source, row, role) = lookup(&app, &shell, &viewer, &owner, &name).await?;
532×
    let owners = super::repo_settings::owners_for(&app, &me.account).await;
542×
    let page = |msg: &str| {
551×
        Page(ForkPage {
561×
            shell: shell.clone(),
571×
            repo: RepoShell::new(&app, &row, "code").with_role(role),
581×
            owners: owners.clone(),
591×
            name: f.name.clone(),
601×
            error: Some(msg.into()),
611×
        })
621×
        .into_response()
631×
    };
642×
    let Some(dest) = RepoName::new(f.owner.trim(), f.name.trim()) else {
650×
        return Ok(page("A repository name is letters, digits, dots, dashes and underscores."));
66
    };
672×
    if dest == source {
680×
        return Ok(page("Pick another owner or name: a repository cannot be its own fork."));
692×
    }
702×
    if !roles::may_create_under(&app.db, &me.account, &dest.owner).await.map_err(|e| Error::from_db(shell.clone(), e))? {
711×
        return Ok(page("You cannot create repositories under that owner."));
721×
    }
731×
    if db::repos::get(&app.db, &dest.owner, &dest.name).await.map_err(|e| Error::from_db(shell.clone(), e))?.is_some()
741×
        || app.repos.exists(&dest)
75
    {
760×
        return Ok(page("That repository exists."));
771×
    }
781×
    app.repos.fork(&source, &dest).await.map_err(|e| Error::from_repo(shell.clone(), e))?;
791×
    let created = db::repos::create(&app.db, &dest.owner, &dest.name, &row.description, row.visibility())
801×
        .await
811×
        .map_err(|e| Error::from_db(shell.clone(), e))?;
821×
    db::repos::set_forked_from(&app.db, created.id, row.id).await.map_err(|e| Error::from_db(shell.clone(), e))?;
831×
    accounts::audit(&app.db, Some(&me.account), "repo.forked", &dest.to_string(), &source.to_string()).await.ok();
841×
    Ok(Redirect::to(&dest.url()).into_response())
852×
}
86
87
#[derive(Template)]
88
#[template(path = "forks.html")]
89
pub struct ForksPage {
90
    shell: Shell,
91
    repo: RepoShell,
92
    forks: Vec<db::repos::RepoRow>,
93
}
94
951×
pub async fn list(
961×
    State(app): State<Shared>,
971×
    viewer: Viewer,
981×
    Path((owner, name)): Path<(String, String)>,
991×
) -> Result<Page<ForksPage>, Error> {
1001×
    let shell = Shell::new(&app, &format!("/{owner}/{name}/forks"), &viewer);
1011×
    let (_, row, role) = lookup(&app, &shell, &viewer, &owner, &name).await?;
1021×
    let all = db::repos::forks_of(&app.db, row.id).await.map_err(|e| Error::from_db(shell.clone(), e))?;
1031×
    let forks = super::home::visible(&app, &shell, &viewer, all).await?;
1041×
    Ok(Page(ForksPage { repo: RepoShell::new(&app, &row, "code").with_role(role), shell, forks }))
1051×
}
106
107
/// What the overview says about forks: where this one came from (when the
108
/// viewer may see it) and how many forks it has.
109
pub struct ForkInfo {
110
    pub upstream: Option<db::repos::RepoRow>,
111
    pub forks: usize,
112
}
113
11451×
pub async fn info(app: &Shared, shell: &Shell, viewer: &Viewer, row: &db::repos::RepoRow) -> ForkInfo {
11551×
    let upstream = match row.forked_from {
1161×
        Some(id) => match db::repos::get_by_id(&app.db, id).await {
1171×
            Ok(Some(up)) => super::home::visible(app, shell, viewer, vec![up]).await.ok().and_then(|mut v| v.pop()),
1180×
            _ => None,
119
        },
12050×
        None => None,
121
    };
12251×
    let forks = match db::repos::forks_of(&app.db, row.id).await {
12351×
        Ok(all) => super::home::visible(app, shell, viewer, all).await.map(|v| v.len()).unwrap_or(0),
1240×
        Err(_) => 0,
125
    };
12651×
    ForkInfo { upstream, forks }
12751×
}