OXYGENHUB
# 1553bfc9ae53 oxygenhub / crates / hub / src / db / repos.rs
9.0 KiB · Rust · last changed Stars, topics, and a sortable, filterable Explore 1 month ago source blame history raw
d23464c0 Andy McCall ↶ 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, 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,
3f4b0ea6 Andy McCall ↶ 17
    #[sqlx(default)]
18
    pub git_mirror: bool,
19
    #[sqlx(default)]
20
    pub git_mirror_state: String,
07b1a5bb Andy McCall ↶ 21
    /// The repository this one was forked from (migration 0009).
22
    #[sqlx(default)]
23
    pub forked_from: Option<i64>,
1553bfc9 Andy McCall ↶ 24
    /// Space-separated topics (migration 0010).
25
    #[sqlx(default)]
26
    pub topics: String,
d23464c0 Andy McCall ↶ 27
}
28
29
impl RepoRow {
30
    pub fn visibility(&self) -> Visibility {
31
        Visibility::parse(&self.visibility).unwrap_or(Visibility::Private)
32
    }
33
1553bfc9 Andy McCall ↶ 34
    pub fn topic_list(&self) -> Vec<&str> {
35
        self.topics.split_whitespace().collect()
36
    }
37
d23464c0 Andy McCall ↶ 38
    pub fn slug(&self) -> String {
39
        format!("{}/{}", self.owner, self.name)
40
    }
41
}
42
43
#[derive(Clone, Debug, FromRow)]
44
pub struct PushRow {
45
    pub id: i64,
46
    pub at_ms: i64,
47
    pub reference: String,
48
    pub ok: bool,
49
    pub reason: String,
62d6e969 Andy McCall ↶ 50
    pub old_state: String,
51
    pub new_state: String,
52
    pub heads: i64,
53
    pub changes: i64,
19bb31ff Andy McCall ↶ 54
    pub pusher: String,
62d6e969 Andy McCall ↶ 55
}
56
57
/// One reference's outcome in a push, as the protocol backend reports it.
58
#[derive(Clone, Debug)]
59
pub struct PushOutcome {
60
    pub reference: String,
61
    pub ok: bool,
62
    pub reason: String,
63
    pub old_state: String,
64
    pub new_state: String,
65
    pub heads: i64,
66
    pub changes: i64,
d23464c0 Andy McCall ↶ 67
}
68
69
pub async fn list(pool: &Pool) -> sqlx::Result<Vec<RepoRow>> {
70
    sqlx::query_as("SELECT * FROM repositories ORDER BY updated_ms DESC, owner, name").fetch_all(pool).await
71
}
72
19bb31ff Andy McCall ↶ 73
pub async fn get_by_id(pool: &Pool, id: i64) -> sqlx::Result<Option<RepoRow>> {
74
    sqlx::query_as("SELECT * FROM repositories WHERE id = ?").bind(id).fetch_optional(pool).await
75
}
76
d23464c0 Andy McCall ↶ 77
pub async fn list_by_owner(pool: &Pool, owner: &str) -> sqlx::Result<Vec<RepoRow>> {
78
    sqlx::query_as("SELECT * FROM repositories WHERE owner = ? ORDER BY updated_ms DESC, name").bind(owner).fetch_all(pool).await
79
}
80
81
pub async fn get(pool: &Pool, owner: &str, name: &str) -> sqlx::Result<Option<RepoRow>> {
82
    sqlx::query_as("SELECT * FROM repositories WHERE owner = ? AND name = ?").bind(owner).bind(name).fetch_optional(pool).await
83
}
84
85
pub async fn create(pool: &Pool, owner: &str, name: &str, description: &str, visibility: Visibility) -> sqlx::Result<RepoRow> {
86
    let now = now_ms();
87
    sqlx::query_as(
88
        "INSERT INTO repositories (owner, name, description, visibility, created_ms, updated_ms) VALUES (?, ?, ?, ?, ?, ?) RETURNING *",
89
    )
90
    .bind(owner)
91
    .bind(name)
92
    .bind(description)
93
    .bind(visibility.as_str())
94
    .bind(now)
95
    .bind(now)
96
    .fetch_one(pool)
97
    .await
98
}
99
100
pub async fn delete(pool: &Pool, id: i64) -> sqlx::Result<()> {
101
    sqlx::query("DELETE FROM repositories WHERE id = ?").bind(id).execute(pool).await?;
102
    Ok(())
103
}
104
105
pub async fn set_description(pool: &Pool, id: i64, description: &str) -> sqlx::Result<()> {
106
    sqlx::query("UPDATE repositories SET description = ? WHERE id = ?").bind(description).bind(id).execute(pool).await?;
107
    Ok(())
108
}
109
110
pub async fn set_default_bookmark(pool: &Pool, id: i64, bookmark: &str) -> sqlx::Result<()> {
111
    sqlx::query("UPDATE repositories SET default_bookmark = ? WHERE id = ?").bind(bookmark).bind(id).execute(pool).await?;
112
    Ok(())
113
}
114
115
pub async fn set_visibility(pool: &Pool, id: i64, visibility: Visibility) -> sqlx::Result<()> {
116
    sqlx::query("UPDATE repositories SET visibility = ? WHERE id = ?").bind(visibility.as_str()).bind(id).execute(pool).await?;
117
    Ok(())
118
}
119
120
/// Record a push's per-reference results and bump the repository's activity.
19bb31ff Andy McCall ↶ 121
pub async fn record_push(pool: &Pool, id: i64, pusher: Option<(i64, &str)>, results: &[PushOutcome]) -> sqlx::Result<()> {
d23464c0 Andy McCall ↶ 122
    let now = now_ms();
123
    let mut tx = pool.begin().await?;
62d6e969 Andy McCall ↶ 124
    for r in results {
19bb31ff Andy McCall ↶ 125
        sqlx::query("INSERT INTO pushes (repo_id, at_ms, reference, ok, reason, old_state, new_state, heads, changes, account_id, pusher) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
d23464c0 Andy McCall ↶ 126
            .bind(id)
127
            .bind(now)
62d6e969 Andy McCall ↶ 128
            .bind(&r.reference)
129
            .bind(r.ok)
130
            .bind(&r.reason)
131
            .bind(&r.old_state)
132
            .bind(&r.new_state)
133
            .bind(r.heads)
134
            .bind(r.changes)
19bb31ff Andy McCall ↶ 135
            .bind(pusher.map(|p| p.0).filter(|id| *id > 0))
136
            .bind(pusher.map(|p| p.1).unwrap_or(""))
d23464c0 Andy McCall ↶ 137
            .execute(&mut *tx)
138
            .await?;
139
    }
62d6e969 Andy McCall ↶ 140
    if results.iter().any(|r| r.ok) {
d23464c0 Andy McCall ↶ 141
        sqlx::query("UPDATE repositories SET updated_ms = ? WHERE id = ?").bind(now).bind(id).execute(&mut *tx).await?;
142
    }
143
    tx.commit().await
144
}
145
146
pub async fn pushes(pool: &Pool, id: i64, limit: i64) -> sqlx::Result<Vec<PushRow>> {
19bb31ff Andy McCall ↶ 147
    sqlx::query_as("SELECT id, at_ms, reference, ok, reason, old_state, new_state, heads, changes, pusher FROM pushes WHERE repo_id = ? ORDER BY at_ms DESC, id DESC LIMIT ?")
d23464c0 Andy McCall ↶ 148
        .bind(id)
149
        .bind(limit)
150
        .fetch_all(pool)
151
        .await
152
}
3f4b0ea6 Andy McCall ↶ 153
1553bfc9 Andy McCall ↶ 154
/// Topics from user input: comma or space separated, lower-cased,
155
/// letters, digits and dashes, at most 35 characters each and 20 in all.
156
/// Returns the stored form or what is wrong.
157
pub fn parse_topics(input: &str) -> Result<String, String> {
158
    let mut out: Vec<String> = Vec::new();
159
    for t in input.split([',', ' ', '\n', '\t']).map(|t| t.trim().to_ascii_lowercase()).filter(|t| !t.is_empty()) {
160
        if t.len() > 35 || !t.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') || t.starts_with('-') {
161
            return Err(format!("\"{t}\" is not a topic: use letters, digits and dashes, up to 35 characters"));
162
        }
163
        if !out.contains(&t) {
164
            out.push(t);
165
        }
166
    }
167
    if out.len() > 20 {
168
        return Err("At most 20 topics.".into());
169
    }
170
    Ok(out.join(" "))
171
}
172
173
pub async fn set_topics(pool: &Pool, id: i64, topics: &str) -> sqlx::Result<()> {
174
    sqlx::query("UPDATE repositories SET topics = ? WHERE id = ?").bind(topics).bind(id).execute(pool).await?;
175
    Ok(())
176
}
177
178
/// Star or unstar; returns whether it is now starred.
179
pub async fn toggle_star(pool: &Pool, account_id: i64, repo_id: i64) -> sqlx::Result<bool> {
180
    let gone = sqlx::query("DELETE FROM stars WHERE account_id = ? AND repo_id = ?").bind(account_id).bind(repo_id).execute(pool).await?;
181
    if gone.rows_affected() > 0 {
182
        return Ok(false);
183
    }
184
    sqlx::query("INSERT INTO stars (account_id, repo_id, at_ms) VALUES (?, ?, ?)")
185
        .bind(account_id)
186
        .bind(repo_id)
187
        .bind(now_ms())
188
        .execute(pool)
189
        .await?;
190
    Ok(true)
191
}
192
193
pub async fn starred(pool: &Pool, account_id: i64, repo_id: i64) -> sqlx::Result<bool> {
194
    let n: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM stars WHERE account_id = ? AND repo_id = ?")
195
        .bind(account_id)
196
        .bind(repo_id)
197
        .fetch_one(pool)
198
        .await?;
199
    Ok(n > 0)
200
}
201
202
/// Stars and watchers of one repository.
203
pub async fn counts(pool: &Pool, repo_id: i64) -> sqlx::Result<(i64, i64)> {
204
    let stars: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM stars WHERE repo_id = ?").bind(repo_id).fetch_one(pool).await?;
205
    let watchers: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM watches WHERE repo_id = ?").bind(repo_id).fetch_one(pool).await?;
206
    Ok((stars, watchers))
207
}
208
209
/// Star counts of every starred repository.
210
pub async fn star_counts(pool: &Pool) -> sqlx::Result<std::collections::HashMap<i64, i64>> {
211
    let rows: Vec<(i64, i64)> = sqlx::query_as("SELECT repo_id, COUNT(*) FROM stars GROUP BY repo_id").fetch_all(pool).await?;
212
    Ok(rows.into_iter().collect())
213
}
214
07b1a5bb Andy McCall ↶ 215
/// Record that `id` is a fork of `from`.
216
pub async fn set_forked_from(pool: &Pool, id: i64, from: i64) -> sqlx::Result<()> {
217
    sqlx::query("UPDATE repositories SET forked_from = ? WHERE id = ?").bind(from).bind(id).execute(pool).await?;
218
    Ok(())
219
}
220
221
/// The forks of `id`, newest activity first.
222
pub async fn forks_of(pool: &Pool, id: i64) -> sqlx::Result<Vec<RepoRow>> {
223
    sqlx::query_as("SELECT * FROM repositories WHERE forked_from = ? ORDER BY updated_ms DESC").bind(id).fetch_all(pool).await
224
}
225
3f4b0ea6 Andy McCall ↶ 226
pub async fn set_git_mirror(pool: &Pool, id: i64, on: bool) -> sqlx::Result<()> {
227
    sqlx::query("UPDATE repositories SET git_mirror = ? WHERE id = ?").bind(on).bind(id).execute(pool).await?;
228
    Ok(())
229
}
230
231
pub async fn set_git_mirror_state(pool: &Pool, id: i64, state: &str) -> sqlx::Result<()> {
232
    sqlx::query("UPDATE repositories SET git_mirror_state = ? WHERE id = ?").bind(state).bind(id).execute(pool).await?;
233
    Ok(())
234
}
1553bfc9 Andy McCall ↶ 235
236
#[cfg(test)]
237
mod topic_tests {
238
    use super::parse_topics;
239
240
    #[test]
241
    fn topics_are_normalised_and_checked() {
242
        assert_eq!(parse_topics("Rust, vcs  retro-computing,rust").unwrap(), "rust vcs retro-computing");
243
        assert_eq!(parse_topics("").unwrap(), "");
244
        assert!(parse_topics("c++").is_err());
245
        assert!(parse_topics("-lead").is_err());
246
        assert!(parse_topics(&"x".repeat(36)).is_err());
247
        assert!(parse_topics(&(0..21).map(|i| format!("t{i}")).collect::<Vec<_>>().join(" ")).is_err());
248
    }
249
}