OXYGENHUB

Coverage / crates/hub/src/db/repos.rs 100.0%

366 of 366 lines run, 99 of 188 functions · run #519 on main · run missed

1
//! The `repositories` and `pushes` tables.
2
3
use super::{now_ms, Pool};
4
use hub_api::Visibility;
5
use sqlx::FromRow;
6
7
#[derive(Clone, Debug, Default, FromRow)]
8
pub struct RepoRow {
9
    pub id: i64,
10
    pub owner: String,
11
    pub name: String,
12
    pub description: String,
13
    pub default_bookmark: String,
14
    pub visibility: String,
15
    pub created_ms: i64,
16
    pub updated_ms: i64,
17
    #[sqlx(default)]
18
    pub git_mirror: bool,
19
    #[sqlx(default)]
20
    pub git_mirror_state: String,
21
    /// The repository this one was forked from (migration 0009).
22
    #[sqlx(default)]
23
    pub forked_from: Option<i64>,
24
    /// Space-separated topics (migration 0010).
25
    #[sqlx(default)]
26
    pub topics: String,
27
    /// OxygenFlow: `on`, `off`, or empty to follow hub.toml (migration 0018).
28
    #[sqlx(default)]
29
    pub flow: String,
30
    /// Its jobs' priority: `low`, `normal`, `high`, or empty to follow its
31
    /// organisation's and the site's (oxygenhub#244, migration 0073).
32
    #[sqlx(default)]
33
    pub flow_priority: String,
34
    /// Diffusion: `off`, `green`, `any`, or empty to follow hub.toml
35
    /// (migration 0058, #148).
36
    #[sqlx(default)]
37
    pub diffusion: String,
38
    /// Diffusion auto-land: `auto`, `propose`, or empty for propose
39
    /// (migration 0061).
40
    #[sqlx(default)]
41
    pub diffusion_land: String,
42
    /// Backups (migration 0062): empty with its owner's, `off`, or its own
43
    /// `hourly` or `daily`.
44
    #[sqlx(default)]
45
    pub backup: String,
46
    /// Read-only, with why (migration 0040, #108).
47
    #[sqlx(default)]
48
    pub archived: bool,
49
    #[sqlx(default)]
50
    pub archived_reason: String,
51
    /// Agents' tokens may approve or reject its approval gates (#258).
52
    #[sqlx(default)]
53
    pub agents_decide_gates: bool,
54
    /// Newest run wins (#267): a superseded run already running finishes;
55
    /// only queued ones are cancelled. Off by default.
56
    #[sqlx(default)]
57
    pub flow_keep_running: bool,
58
    /// Where a push may skip its run with `[skip flow]` (#284, migration
59
    /// 0088): `any` bookmark, `others` than the default one, or `never`.
60
    #[sqlx(default)]
61
    pub flow_skip: String,
62
    /// A push run reuses a job's pass on the same state (#265, migration
63
    /// 0090). On by default.
64
    #[sqlx(default)]
65
    pub flow_reuse: bool,
66
    /// Pushes that add possible secrets are refused (#268, migration
67
    /// 0085). On by default.
68
    #[sqlx(default)]
69
    pub push_protection: bool,
70
    /// `repo`, or `drop` for a drop (#279, migration 0087): a small
71
    /// repository named `drops/ID` that no repository list shows.
72
    #[sqlx(default)]
73
    pub kind: String,
74
    /// A drop's title.
75
    #[sqlx(default)]
76
    pub title: String,
77
    /// Counts for the tabs; filled by `get` only.
78
    #[sqlx(default)]
79
    pub open_issues: i64,
80
    #[sqlx(default)]
81
    pub open_proposals: i64,
82
}
83
84
impl RepoRow {
851674×
    pub fn visibility(&self) -> Visibility {
861674×
        Visibility::parse(&self.visibility).unwrap_or(Visibility::Private)
871674×
    }
88
8960×
    pub fn topic_list(&self) -> Vec<&str> {
9060×
        self.topics.split_whitespace().collect()
9160×
    }
92
93389×
    pub fn slug(&self) -> String {
94389×
        format!("{}/{}", self.owner, self.name)
95389×
    }
96
97
    /// A drop (#279), not a repository.
984362×
    pub fn is_drop(&self) -> bool {
994362×
        self.kind == DROP
1004362×
    }
101
}
102
103
/// `repositories.kind` of a drop (#279).
104
pub const DROP: &str = "drop";
105
106
#[derive(Clone, Debug, FromRow)]
107
pub struct PushRow {
108
    pub id: i64,
109
    pub at_ms: i64,
110
    pub reference: String,
111
    pub ok: bool,
112
    pub reason: String,
113
    pub old_state: String,
114
    pub new_state: String,
115
    pub heads: i64,
116
    pub changes: i64,
117
    pub pusher: String,
118
}
119
120
/// One reference's outcome in a push, as the protocol backend reports it.
121
#[derive(Clone, Debug)]
122
pub struct PushOutcome {
123
    pub reference: String,
124
    pub ok: bool,
125
    pub reason: String,
126
    pub old_state: String,
127
    pub new_state: String,
128
    pub heads: i64,
129
    pub changes: i64,
130
}
131
132
/// Every repository, drops left out: what every list, search and count
133
/// of repositories starts from (#279).
134556×
pub async fn list(pool: &Pool) -> sqlx::Result<Vec<RepoRow>> {
135556×
    sqlx::query_as("SELECT * FROM repositories WHERE kind = 'repo' ORDER BY updated_ms DESC, owner, name").fetch_all(pool).await
136556×
}
137
138
/// Every repository and every drop: for upkeep that covers all stored
139
/// repositories (gc, the history scan, `oxygenhub check`), never a list
140
/// people see unless it labels the drops.
14130×
pub async fn list_with_drops(pool: &Pool) -> sqlx::Result<Vec<RepoRow>> {
14230×
    sqlx::query_as("SELECT * FROM repositories ORDER BY updated_ms DESC, owner, name").fetch_all(pool).await
14330×
}
144
145601×
pub async fn get_by_id(pool: &Pool, id: i64) -> sqlx::Result<Option<RepoRow>> {
146601×
    sqlx::query_as("SELECT * FROM repositories WHERE id = $1").bind(id).fetch_optional(pool).await
147601×
}
148
14992×
pub async fn list_by_owner(pool: &Pool, owner: &str) -> sqlx::Result<Vec<RepoRow>> {
15092×
    sqlx::query_as("SELECT * FROM repositories WHERE owner = $1 AND kind = 'repo' ORDER BY updated_ms DESC, name")
15192×
        .bind(owner)
15292×
        .fetch_all(pool)
15392×
        .await
15492×
}
155
156
// ------------------------------------------------------------------ drops
157
158
/// A drop's row (#279): a repository named `drops/ID`, its kind `drop`,
159
/// push protection on and Flow off. `visibility` is `public`, or
160
/// `private` for a secret drop.
1617×
pub async fn create_drop(
1627×
    pool: &Pool,
1637×
    owner: &str,
1647×
    id: &str,
1657×
    title: &str,
1667×
    description: &str,
1677×
    visibility: Visibility,
1687×
) -> sqlx::Result<RepoRow> {
1697×
    let now = now_ms();
1707×
    sqlx::query_as(
1717×
        "INSERT INTO repositories (owner, name, description, visibility, created_ms, updated_ms, kind, title, flow) \
1727×
         VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'off') RETURNING *",
1737×
    )
1747×
    .bind(owner)
1757×
    .bind(format!("{}/{id}", crate::repos::names::DROPS))
1767×
    .bind(description)
1777×
    .bind(visibility.as_str())
1787×
    .bind(now)
1797×
    .bind(now)
1807×
    .bind(DROP)
1817×
    .bind(title)
1827×
    .fetch_one(pool)
1837×
    .await
1847×
}
185
186
/// A comment on a drop (#280).
187
#[derive(Clone, Debug, FromRow)]
188
pub struct DropComment {
189
    pub id: i64,
190
    pub repo_id: i64,
191
    pub author_id: Option<i64>,
192
    pub author: String,
193
    pub body: String,
194
    pub created_ms: i64,
195
}
196
1972×
pub async fn add_drop_comment(pool: &Pool, repo_id: i64, author_id: i64, author: &str, body: &str) -> sqlx::Result<i64> {
1982×
    sqlx::query_scalar("INSERT INTO drop_comments (repo_id, author_id, author, body, created_ms) VALUES ($1, $2, $3, $4, $5) RETURNING id")
1992×
        .bind(repo_id)
2002×
        .bind(author_id)
2012×
        .bind(author)
2022×
        .bind(body)
2032×
        .bind(now_ms())
2042×
        .fetch_one(pool)
2052×
        .await
2062×
}
207
208
/// A drop's comments, oldest first.
20927×
pub async fn drop_comments(pool: &Pool, repo_id: i64) -> sqlx::Result<Vec<DropComment>> {
21027×
    sqlx::query_as("SELECT * FROM drop_comments WHERE repo_id = $1 ORDER BY created_ms, id").bind(repo_id).fetch_all(pool).await
21127×
}
212
2133×
pub async fn drop_comment(pool: &Pool, repo_id: i64, id: i64) -> sqlx::Result<Option<DropComment>> {
2143×
    sqlx::query_as("SELECT * FROM drop_comments WHERE repo_id = $1 AND id = $2").bind(repo_id).bind(id).fetch_optional(pool).await
2153×
}
216
2172×
pub async fn delete_drop_comment(pool: &Pool, repo_id: i64, id: i64) -> sqlx::Result<()> {
2182×
    sqlx::query("DELETE FROM drop_comments WHERE repo_id = $1 AND id = $2").bind(repo_id).bind(id).execute(pool).await?;
2192×
    Ok(())
2202×
}
221
222
/// An owner's drops, newest activity first.
22312×
pub async fn drops_of(pool: &Pool, owner: &str) -> sqlx::Result<Vec<RepoRow>> {
22412×
    sqlx::query_as("SELECT * FROM repositories WHERE kind = 'drop' AND owner = $1 ORDER BY updated_ms DESC, id DESC")
22512×
        .bind(owner)
22612×
        .fetch_all(pool)
22712×
        .await
22812×
}
229
230
/// The drop with this id, whoever owns it: ids are random and made
231
/// unique across the hub.
23224×
pub async fn drop_by_id(pool: &Pool, id: &str) -> sqlx::Result<Option<RepoRow>> {
23324×
    sqlx::query_as("SELECT * FROM repositories WHERE kind = 'drop' AND name = $1")
23424×
        .bind(format!("{}/{id}", crate::repos::names::DROPS))
23524×
        .fetch_optional(pool)
23624×
        .await
23724×
}
238
239
/// A drop's title, description and visibility, and that it changed.
2402×
pub async fn set_drop(pool: &Pool, row_id: i64, title: &str, description: &str, visibility: Visibility) -> sqlx::Result<()> {
2412×
    sqlx::query("UPDATE repositories SET title = $1, description = $2, visibility = $3, updated_ms = $4 WHERE id = $5 AND kind = 'drop'")
2422×
        .bind(title)
2432×
        .bind(description)
2442×
        .bind(visibility.as_str())
2452×
        .bind(now_ms())
2462×
        .bind(row_id)
2472×
        .execute(pool)
2482×
        .await?;
2492×
    Ok(())
2502×
}
251
252
/// One repository, with its open issue and proposal counts for the tabs.
2535417×
pub async fn get(pool: &Pool, owner: &str, name: &str) -> sqlx::Result<Option<RepoRow>> {
2545417×
    sqlx::query_as(
2555417×
        "SELECT r.*, \
2565417×
           (SELECT COUNT(*) FROM issues i WHERE i.repo_id = r.id AND i.state = 'open') AS open_issues, \
2575417×
           (SELECT COUNT(*) FROM proposals p WHERE p.repo_id = r.id AND p.state IN ('open', 'draft')) AS open_proposals \
2585417×
         FROM repositories r WHERE r.owner = $1 AND r.name = $2",
2595417×
    )
2605417×
    .bind(owner)
2615417×
    .bind(name)
2625417×
    .fetch_optional(pool)
2635417×
    .await
2645417×
}
265
266
/// Create a repository row. One under an organisation of two or more
267
/// members requires one sealed approval to land a proposal (oxygenhub#64),
268
/// since someone other than the author can then give it; others require none.
269207×
pub async fn create(pool: &Pool, owner: &str, name: &str, description: &str, visibility: Visibility) -> sqlx::Result<RepoRow> {
270207×
    let now = now_ms();
271207×
    sqlx::query_as(
272207×
        "INSERT INTO repositories (owner, name, description, visibility, created_ms, updated_ms, required_approvals) \
273207×
         VALUES ($1, $2, $3, $4, $5, $6, (SELECT CASE WHEN COUNT(*) >= 2 THEN 1 ELSE 0 END FROM org_members m JOIN organisations o ON o.id = m.org_id WHERE o.name = $7)) RETURNING *",
274207×
    )
275207×
    .bind(owner)
276207×
    .bind(name)
277207×
    .bind(description)
278207×
    .bind(visibility.as_str())
279207×
    .bind(now)
280207×
    .bind(now)
281207×
    .bind(owner)
282207×
    .fetch_one(pool)
283207×
    .await
284207×
}
285
286
/// The repository an old name now points at, after a transfer.
28733×
pub async fn moved_to(pool: &Pool, owner: &str, name: &str) -> sqlx::Result<Option<RepoRow>> {
28833×
    let id: Option<i64> = sqlx::query_scalar("SELECT repo_id FROM repo_redirects WHERE old_owner = $1 AND old_name = $2")
28933×
        .bind(owner)
29033×
        .bind(name)
29133×
        .fetch_optional(pool)
29233×
        .await?;
29333×
    match id {
29432×
        Some(id) => get_by_id(pool, id).await,
2951×
        None => Ok(None),
296
    }
29733×
}
298
299
/// Move a repository to another owner, keeping its name: the row, and a
300
/// redirect from the old name. A redirect that pointed at the new name
301
/// goes, so names never loop.
3021×
pub async fn transfer(pool: &Pool, id: i64, old_owner: &str, name: &str, new_owner: &str) -> sqlx::Result<()> {
3031×
    sqlx::query("UPDATE repositories SET owner = $1, updated_ms = $2 WHERE id = $3")
3041×
        .bind(new_owner)
3051×
        .bind(now_ms())
3061×
        .bind(id)
3071×
        .execute(pool)
3081×
        .await?;
3091×
    sqlx::query("DELETE FROM repo_redirects WHERE old_owner = $1 AND old_name = $2").bind(new_owner).bind(name).execute(pool).await?;
3101×
    sqlx::query("INSERT INTO repo_redirects (old_owner, old_name, repo_id, moved_ms) VALUES ($1, $2, $3, $4) ON CONFLICT (old_owner, old_name) DO UPDATE SET repo_id = excluded.repo_id, moved_ms = excluded.moved_ms")
3111×
        .bind(old_owner)
3121×
        .bind(name)
3131×
        .bind(id)
3141×
        .bind(now_ms())
3151×
        .execute(pool)
3161×
        .await?;
3171×
    Ok(())
3181×
}
319
320
/// Archive (read-only) or bring back a repository.
3212×
pub async fn set_archived(pool: &Pool, id: i64, archived: bool, reason: &str) -> sqlx::Result<()> {
3222×
    sqlx::query("UPDATE repositories SET archived = $1, archived_reason = $2, updated_ms = $3 WHERE id = $4")
3232×
        .bind(archived)
3242×
        .bind(if archived { reason } else { "" })
3252×
        .bind(now_ms())
3262×
        .bind(id)
3272×
        .execute(pool)
3282×
        .await?;
3292×
    Ok(())
3302×
}
331
3326×
pub async fn delete(pool: &Pool, id: i64) -> sqlx::Result<()> {
3336×
    sqlx::query("DELETE FROM repositories WHERE id = $1").bind(id).execute(pool).await?;
3346×
    Ok(())
3356×
}
336
3374×
pub async fn set_description(pool: &Pool, id: i64, description: &str) -> sqlx::Result<()> {
3384×
    sqlx::query("UPDATE repositories SET description = $1 WHERE id = $2").bind(description).bind(id).execute(pool).await?;
3394×
    Ok(())
3404×
}
341
3425×
pub async fn set_default_bookmark(pool: &Pool, id: i64, bookmark: &str) -> sqlx::Result<()> {
3435×
    sqlx::query("UPDATE repositories SET default_bookmark = $1 WHERE id = $2").bind(bookmark).bind(id).execute(pool).await?;
3445×
    Ok(())
3455×
}
346
3474×
pub async fn set_visibility(pool: &Pool, id: i64, visibility: Visibility) -> sqlx::Result<()> {
3484×
    sqlx::query("UPDATE repositories SET visibility = $1 WHERE id = $2").bind(visibility.as_str()).bind(id).execute(pool).await?;
3494×
    Ok(())
3504×
}
351
352
/// Record a push's per-reference results and bump the repository's activity.
35356×
pub async fn record_push(pool: &Pool, id: i64, pusher: Option<(i64, &str)>, results: &[PushOutcome]) -> sqlx::Result<()> {
35456×
    record_push_via(pool, id, pusher, "", results).await
35556×
}
356
357
/// [`record_push`], naming the agent token the push came through (#217).
358321×
pub async fn record_push_via(pool: &Pool, id: i64, pusher: Option<(i64, &str)>, via: &str, results: &[PushOutcome]) -> sqlx::Result<()> {
359321×
    let now = now_ms();
360321×
    let mut tx = super::begin_write(pool).await?;
361341×
    for r in results {
362341×
        sqlx::query("INSERT INTO pushes (repo_id, at_ms, reference, ok, reason, old_state, new_state, heads, changes, account_id, pusher, via) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)")
363341×
            .bind(id)
364341×
            .bind(now)
365341×
            .bind(&r.reference)
366341×
            .bind(r.ok)
367341×
            .bind(&r.reason)
368341×
            .bind(&r.old_state)
369341×
            .bind(&r.new_state)
370341×
            .bind(r.heads)
371341×
            .bind(r.changes)
372341×
            .bind(pusher.map(|p| p.0).filter(|id| *id > 0))
373341×
            .bind(pusher.map(|p| p.1).unwrap_or(""))
374341×
            .bind(via)
375341×
            .execute(&mut *tx)
376341×
            .await?;
377
    }
378321×
    if results.iter().any(|r| r.ok) {
379304×
        sqlx::query("UPDATE repositories SET updated_ms = $1 WHERE id = $2").bind(now).bind(id).execute(&mut *tx).await?;
38017×
    }
381321×
    tx.commit().await
382321×
}
383
38411×
pub async fn pushes(pool: &Pool, id: i64, limit: i64) -> sqlx::Result<Vec<PushRow>> {
38511×
    sqlx::query_as("SELECT id, at_ms, reference, ok, reason, old_state, new_state, heads, changes, pusher FROM pushes WHERE repo_id = $1 ORDER BY at_ms DESC, id DESC LIMIT $2")
38611×
        .bind(id)
38711×
        .bind(limit)
38811×
        .fetch_all(pool)
38911×
        .await
39011×
}
391
392
/// Topics from user input: comma or space separated, lower-cased,
393
/// letters, digits and dashes, at most 35 characters each and 20 in all.
394
/// Returns the stored form or what is wrong.
39511×
pub fn parse_topics(input: &str) -> Result<String, String> {
39611×
    let mut out: Vec<String> = Vec::new();
39738×
    for t in input.split([',', ' ', '\n', '\t']).map(|t| t.trim().to_ascii_lowercase()).filter(|t| !t.is_empty()) {
39897×
        if t.len() > 35 || !t.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') || t.starts_with('-') {
3994×
            return Err(format!("\"{t}\" is not a topic: use letters, digits and dashes, up to 35 characters"));
40027×
        }
40127×
        if !out.contains(&t) {
40226×
            out.push(t);
40326×
        }
404
    }
4057×
    if out.len() > 20 {
4061×
        return Err("At most 20 topics.".into());
4076×
    }
4086×
    Ok(out.join(" "))
40911×
}
410
4114×
pub async fn set_topics(pool: &Pool, id: i64, topics: &str) -> sqlx::Result<()> {
4124×
    sqlx::query("UPDATE repositories SET topics = $1 WHERE id = $2").bind(topics).bind(id).execute(pool).await?;
4134×
    Ok(())
4144×
}
415
416
/// Star or unstar; returns whether it is now starred.
4178×
pub async fn toggle_star(pool: &Pool, account_id: i64, repo_id: i64) -> sqlx::Result<bool> {
4188×
    let gone = sqlx::query("DELETE FROM stars WHERE account_id = $1 AND repo_id = $2").bind(account_id).bind(repo_id).execute(pool).await?;
4198×
    if gone.rows_affected() > 0 {
4202×
        return Ok(false);
4216×
    }
4226×
    sqlx::query("INSERT INTO stars (account_id, repo_id, at_ms) VALUES ($1, $2, $3)")
4236×
        .bind(account_id)
4246×
        .bind(repo_id)
4256×
        .bind(now_ms())
4266×
        .execute(pool)
4276×
        .await?;
4286×
    Ok(true)
4298×
}
430
43144×
pub async fn starred(pool: &Pool, account_id: i64, repo_id: i64) -> sqlx::Result<bool> {
43244×
    let n: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM stars WHERE account_id = $1 AND repo_id = $2")
43344×
        .bind(account_id)
43444×
        .bind(repo_id)
43544×
        .fetch_one(pool)
43644×
        .await?;
43744×
    Ok(n > 0)
43844×
}
439
440
/// Stars and watchers of one repository.
44179×
pub async fn counts(pool: &Pool, repo_id: i64) -> sqlx::Result<(i64, i64)> {
44279×
    let stars: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM stars WHERE repo_id = $1").bind(repo_id).fetch_one(pool).await?;
44379×
    let watchers = super::notify::watchers(pool, repo_id).await?.len() as i64;
44479×
    Ok((stars, watchers))
44579×
}
446
447
/// Star counts of every starred repository.
44817×
pub async fn star_counts(pool: &Pool) -> sqlx::Result<std::collections::HashMap<i64, i64>> {
44917×
    let rows: Vec<(i64, i64)> = sqlx::query_as("SELECT repo_id, COUNT(*) FROM stars GROUP BY repo_id").fetch_all(pool).await?;
45017×
    Ok(rows.into_iter().collect())
45117×
}
452
453
/// Record that `id` is a fork of `from`.
4544×
pub async fn set_forked_from(pool: &Pool, id: i64, from: i64) -> sqlx::Result<()> {
4554×
    sqlx::query("UPDATE repositories SET forked_from = $1 WHERE id = $2").bind(from).bind(id).execute(pool).await?;
4564×
    Ok(())
4574×
}
458
459
/// The forks of `id`, newest activity first.
46053×
pub async fn forks_of(pool: &Pool, id: i64) -> sqlx::Result<Vec<RepoRow>> {
46153×
    sqlx::query_as("SELECT * FROM repositories WHERE forked_from = $1 AND kind = 'repo' ORDER BY updated_ms DESC")
46253×
        .bind(id)
46353×
        .fetch_all(pool)
46453×
        .await
46553×
}
466
467
/// `on`, `off`, or `` (follow hub.toml's `[flow] repos`).
4688×
pub async fn set_flow(pool: &Pool, id: i64, setting: &str) -> sqlx::Result<()> {
4698×
    sqlx::query("UPDATE repositories SET flow = $1 WHERE id = $2").bind(setting).bind(id).execute(pool).await?;
4708×
    Ok(())
4718×
}
472
4734×
pub async fn set_flow_priority(pool: &Pool, id: i64, priority: &str) -> sqlx::Result<()> {
4744×
    sqlx::query("UPDATE repositories SET flow_priority = $1 WHERE id = $2").bind(priority).bind(id).execute(pool).await?;
4754×
    Ok(())
4764×
}
477
478
/// Whether agents' tokens may decide the repository's approval gates (#258).
4792×
pub async fn set_agents_decide_gates(pool: &Pool, id: i64, on: bool) -> sqlx::Result<()> {
4802×
    sqlx::query("UPDATE repositories SET agents_decide_gates = $1 WHERE id = $2").bind(on).bind(id).execute(pool).await?;
4812×
    Ok(())
4822×
}
483
484
/// Whether a new run leaves the older runs it supersedes that are already
485
/// running to finish (#267), cancelling only the queued ones.
4861×
pub async fn set_flow_reuse(pool: &Pool, id: i64, on: bool) -> sqlx::Result<()> {
4871×
    sqlx::query("UPDATE repositories SET flow_reuse = $1 WHERE id = $2").bind(on).bind(id).execute(pool).await?;
4881×
    Ok(())
4891×
}
490
4913×
pub async fn set_flow_skip(pool: &Pool, id: i64, skip: &str) -> sqlx::Result<()> {
4923×
    sqlx::query("UPDATE repositories SET flow_skip = $1 WHERE id = $2").bind(skip).bind(id).execute(pool).await?;
4933×
    Ok(())
4943×
}
495
4963×
pub async fn set_flow_keep_running(pool: &Pool, id: i64, on: bool) -> sqlx::Result<()> {
4973×
    sqlx::query("UPDATE repositories SET flow_keep_running = $1 WHERE id = $2").bind(on).bind(id).execute(pool).await?;
4983×
    Ok(())
4993×
}
500
5014×
pub async fn set_backup(pool: &Pool, id: i64, setting: &str) -> sqlx::Result<()> {
5024×
    sqlx::query("UPDATE repositories SET backup = $1 WHERE id = $2").bind(setting).bind(id).execute(pool).await?;
5034×
    Ok(())
5044×
}
505
5062×
pub async fn set_diffusion_land(pool: &Pool, id: i64, setting: &str) -> sqlx::Result<()> {
5072×
    sqlx::query("UPDATE repositories SET diffusion_land = $1 WHERE id = $2").bind(setting).bind(id).execute(pool).await?;
5082×
    Ok(())
5092×
}
510
5119×
pub async fn set_diffusion(pool: &Pool, id: i64, setting: &str) -> sqlx::Result<()> {
5129×
    sqlx::query("UPDATE repositories SET diffusion = $1 WHERE id = $2").bind(setting).bind(id).execute(pool).await?;
5139×
    Ok(())
5149×
}
515
5162×
pub async fn set_git_mirror(pool: &Pool, id: i64, on: bool) -> sqlx::Result<()> {
5172×
    sqlx::query("UPDATE repositories SET git_mirror = $1 WHERE id = $2").bind(on).bind(id).execute(pool).await?;
5182×
    Ok(())
5192×
}
520
5212×
pub async fn set_git_mirror_state(pool: &Pool, id: i64, state: &str) -> sqlx::Result<()> {
5222×
    sqlx::query("UPDATE repositories SET git_mirror_state = $1 WHERE id = $2").bind(state).bind(id).execute(pool).await?;
5232×
    Ok(())
5242×
}
525
526
#[cfg(test)]
527
mod topic_tests {
528
    use super::parse_topics;
529
530
    #[test]
5311×
    fn topics_are_normalised_and_checked() {
5321×
        assert_eq!(parse_topics("Rust, vcs  retro-computing,rust").unwrap(), "rust vcs retro-computing");
5331×
        assert_eq!(parse_topics("").unwrap(), "");
5341×
        assert!(parse_topics("c++").is_err());
5351×
        assert!(parse_topics("-lead").is_err());
5361×
        assert!(parse_topics(&"x".repeat(36)).is_err());
53721×
        assert!(parse_topics(&(0..21).map(|i| format!("t{i}")).collect::<Vec<_>>().join(" ")).is_err());
5381×
    }
539
}