OXYGENHUB
◆ v0.3.1 oxygenhub / crates / hub / src / web / forks.rs
5.0 KiB · Rust · last changed Forks 1 month ago source blame history raw
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
25
pub async fn form(State(app): State<Shared>, viewer: Viewer, Path((owner, name)): Path<(String, String)>) -> Result<Page<ForkPage>, Error> {
26
    let shell = Shell::new(&app, &format!("/{owner}/{name}/fork"), &viewer);
27
    let me = require(&viewer, &shell)?;
28
    let (_, row, role) = lookup(&app, &shell, &viewer, &owner, &name).await?;
29
    let owners = super::repo_settings::owners_for(&app, &me.account).await;
30
    Ok(Page(ForkPage { repo: RepoShell::new(&app, &row, "code").with_role(role), shell, owners, name, error: None }))
31
}
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.
43
pub async fn create(
44
    State(app): State<Shared>,
45
    viewer: Viewer,
46
    Path((owner, name)): Path<(String, String)>,
47
    Form(f): Form<ForkForm>,
48
) -> Result<Response, Error> {
49
    let shell = Shell::new(&app, &format!("/{owner}/{name}/fork"), &viewer);
50
    let me = require(&viewer, &shell)?;
51
    csrf(&viewer, &f.csrf_token, &shell)?;
52
    let (source, row, role) = lookup(&app, &shell, &viewer, &owner, &name).await?;
53
    let owners = super::repo_settings::owners_for(&app, &me.account).await;
54
    let page = |msg: &str| {
55
        Page(ForkPage {
56
            shell: shell.clone(),
57
            repo: RepoShell::new(&app, &row, "code").with_role(role),
58
            owners: owners.clone(),
59
            name: f.name.clone(),
60
            error: Some(msg.into()),
61
        })
62
        .into_response()
63
    };
64
    let Some(dest) = RepoName::new(f.owner.trim(), f.name.trim()) else {
65
        return Ok(page("A repository name is letters, digits, dots, dashes and underscores."));
66
    };
67
    if dest == source {
68
        return Ok(page("Pick another owner or name: a repository cannot be its own fork."));
69
    }
70
    if !roles::may_create_under(&app.db, &me.account, &dest.owner).await.map_err(|e| Error::from_db(shell.clone(), e))? {
71
        return Ok(page("You cannot create repositories under that owner."));
72
    }
73
    if db::repos::get(&app.db, &dest.owner, &dest.name).await.map_err(|e| Error::from_db(shell.clone(), e))?.is_some()
74
        || app.repos.exists(&dest)
75
    {
76
        return Ok(page("That repository exists."));
77
    }
78
    app.repos.fork(&source, &dest).await.map_err(|e| Error::from_repo(shell.clone(), e))?;
79
    let created = db::repos::create(&app.db, &dest.owner, &dest.name, &row.description, row.visibility())
80
        .await
81
        .map_err(|e| Error::from_db(shell.clone(), e))?;
82
    db::repos::set_forked_from(&app.db, created.id, row.id).await.map_err(|e| Error::from_db(shell.clone(), e))?;
83
    accounts::audit(&app.db, Some(&me.account), "repo.forked", &dest.to_string(), &source.to_string()).await.ok();
84
    Ok(Redirect::to(&dest.url()).into_response())
85
}
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
95
pub async fn list(
96
    State(app): State<Shared>,
97
    viewer: Viewer,
98
    Path((owner, name)): Path<(String, String)>,
99
) -> Result<Page<ForksPage>, Error> {
100
    let shell = Shell::new(&app, &format!("/{owner}/{name}/forks"), &viewer);
101
    let (_, row, role) = lookup(&app, &shell, &viewer, &owner, &name).await?;
102
    let all = db::repos::forks_of(&app.db, row.id).await.map_err(|e| Error::from_db(shell.clone(), e))?;
103
    let forks = super::home::visible(&app, &shell, &viewer, all).await?;
104
    Ok(Page(ForksPage { repo: RepoShell::new(&app, &row, "code").with_role(role), shell, forks }))
105
}
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
114
pub async fn info(app: &Shared, shell: &Shell, viewer: &Viewer, row: &db::repos::RepoRow) -> ForkInfo {
115
    let upstream = match row.forked_from {
116
        Some(id) => match db::repos::get_by_id(&app.db, id).await {
117
            Ok(Some(up)) => super::home::visible(app, shell, viewer, vec![up]).await.ok().and_then(|mut v| v.pop()),
118
            _ => None,
119
        },
120
        None => None,
121
    };
122
    let forks = match db::repos::forks_of(&app.db, row.id).await {
123
        Ok(all) => super::home::visible(app, shell, viewer, all).await.map(|v| v.len()).unwrap_or(0),
124
        Err(_) => 0,
125
    };
126
    ForkInfo { upstream, forks }
127
}