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