aboutsummaryrefslogtreecommitdiff
path: root/src/main.rs
blob: c15fe5875ddf7624f83fcf2deca461c03145d720 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
use std::{ env, io, path::PathBuf };
use structopt::StructOpt;
use sqlx::SqlitePool;
use tracing::info;
use tracing_subscriber::{
    fmt, filter,
    layer::SubscriberExt,
    util::SubscriberInitExt
};

use anyhow::{ anyhow, Result, Context };

pub mod query;
pub mod generate;

#[derive(StructOpt)]
#[structopt(name = "github-label-feed")]
struct Opt {
    #[structopt(subcommand)]
    mode: OptMode,
}

#[derive(StructOpt)]
pub struct GenerateOpts {
    /// Repository to generate feeds for
    repo: String,
    /// Root directory of output
    out_path: PathBuf,
    /// Labels for which to generate feeds. Leave empty to select all labels
    labels: Vec<String>,
    /// Exclude open issues from the feeds
    #[structopt(long)]
    without_open: bool,
    /// Exclude closed issues from the feeds
    #[structopt(long)]
    without_closed: bool,

    /// Generate an RSS feed to rss.xml
    #[structopt(long)]
    rss: bool,
    /// Generate an Atom feed to atom.xml
    #[structopt(long)]
    atom: bool
}

#[derive(StructOpt)]
enum OptMode {
    /// List repositories currently stored in database
    List,
    /// Synchronise <repo> updates, starting from most recent issue update time
    Sync {
        repo: String,
        #[structopt(long = "github-api-token", env = "GITHUB_TOKEN", hide_env_values = true)]
        github_api_token: String
    },
    /// Generate Atom feeds for <repo>
    Generate(GenerateOpts)
}


pub type Conn = sqlx::SqliteConnection;

async fn init_db(conn: &mut Conn) {
    // Naive init, all data is re-fetch-able, so no support for migrations
    sqlx::query(r#"
        PRAGMA foreign_keys = ON;
        PRAGMA synchronous = OFF;

        CREATE TABLE IF NOT EXISTS repositories(
            id integer PRIMARY KEY,
            owner text, name text,
            UNIQUE (owner, name)
        );

        CREATE TABLE IF NOT EXISTS issues(
            repo integer REFERENCES repositories,
            number integer,
            state integer, title text, body text,
            user_login text,
            html_url text,
            updated_at integer,
            PRIMARY KEY (repo, number)
        );
        CREATE INDEX IF NOT EXISTS issues_state ON issues (repo, number, state);

        CREATE TABLE IF NOT EXISTS labels(
            id integer PRIMARY KEY,
            repo integer REFERENCES repositories,
            name text,
            UNIQUE (repo, name)
        );

        CREATE TABLE IF NOT EXISTS is_labeled(
            repo integer, issue integer,
            label integer RFERENCES labels,
            PRIMARY KEY (issue, label),
            FOREIGN KEY (repo, issue) REFERENCES issues
        );
    "#).execute(conn)
       .await
       .expect("Failed to init database");
}

pub fn parse_repo(combined: &str) -> Result<(String, String)> {
    let mut parts = combined
        .split('/')
        .map(str::trim)
        .map(str::to_owned);

    match (parts.next(), parts.next()) {
        (Some(r), Some(n)) => Ok((r, n)),
        _ => Err(anyhow!("invalid repo format, expected owner/name: '{}'", combined))
    }
}

fn main() -> Result<()> {
    let env_spec = env::var("RUST_LOG")
        .unwrap_or_else(|_| String::from("info"));
    tracing_subscriber::registry()
        .with(fmt::layer()
              .without_time()
              .with_writer(io::stderr))
        .with(filter::EnvFilter::new(env_spec))
        .init();

    let opt = Opt::from_args();

    smol::run(async {
        let pool = SqlitePool::new("sqlite:./issues.sqlite").await?;
        init_db(&mut *pool.acquire().await?).await;

        match opt.mode {
            OptMode::List => {
                let repos = query::list_repositories(&mut *pool.acquire().await?).await?;
                for query::RepositoryInfo { owner, name, label_count, issue_count, .. } in repos {
                    println!("{}/{} ({} labels, {} issues)", owner, name, label_count, issue_count);
                }
                Ok(())
            },
            OptMode::Sync { repo, github_api_token } => {
                info!("sync");
                let repo = parse_repo(&repo)?;
                let mut tx = pool.begin().await?;
                query::labels::update(&mut tx, &github_api_token, repo.clone())
                    .await
                    .context("Failed to update labels")?;
                query::issues::update(&mut tx, &github_api_token, repo)
                    .await
                    .context("Failed to update issues")?;
                tx.commit().await?;
                Ok(())
            },
            OptMode::Generate(opts) => generate::run(&mut *pool.acquire().await?, opts).await
        }
    })
}