diff options
| author | Santo Cariotti <santo@dcariotti.me> | 2021-03-18 12:19:25 +0100 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-03-18 12:19:25 +0100 | 
| commit | 77715d93b2112b7d231db541ecce6ddca5c762c7 (patch) | |
| tree | c3718080abcbe0a6cc52735bcc6726952c6494a8 /src | |
| parent | b8e22e372cb947771aa122767a4405afa208226e (diff) | |
| parent | 7bdd6d8f2d18022c34986a6eea4620d8eb6dcb3a (diff) | |
Merge pull request #18 from gico-net/feat/git2
Create commits from a new Git Repository
Diffstat (limited to 'src')
| -rw-r--r-- | src/commit/models.rs | 46 | ||||
| -rw-r--r-- | src/errors.rs | 4 | ||||
| -rw-r--r-- | src/git.rs | 142 | ||||
| -rw-r--r-- | src/main.rs | 2 | ||||
| -rw-r--r-- | src/repository/models.rs | 46 | 
5 files changed, 238 insertions, 2 deletions
diff --git a/src/commit/models.rs b/src/commit/models.rs index 8b81a15..2f1536b 100644 --- a/src/commit/models.rs +++ b/src/commit/models.rs @@ -93,4 +93,50 @@ impl Commit {              }),          }      } + +    /// Create commits from an array +    pub async fn create( +        pool: Pool, +        commits: Vec<Commit>, +    ) -> Result<Vec<Commit>, AppError> { +        let client = get_client(pool.clone()).await.unwrap(); +        let mut raw_query = "INSERT INTO commit VALUES".to_string(); + +        for commit in commits { +            let tree = match commit.tree { +                Some(t) => format!("'{}'", t), +                None => "NULL".to_string(), +            }; +            raw_query += &format!( +                "('{}', {}, '{}', '{}', '{}', '{}', '{}', '{}', '{}'),", +                commit.hash, +                tree, +                commit.text, +                commit.date, +                commit.author_email, +                commit.author_name, +                commit.committer_email, +                commit.committer_name, +                commit.repository_url +            )[..] +        } + +        // Remove the last `,` +        let _ = raw_query.pop(); + +        // TODO: write query with &commits and parameter. Need to implement +        // ToSql trait for `Commit` model +        // let statement = client.prepare(&query[..]).await?; +        // client.query(&statement, &[&commits] + +        let statement = client.prepare(&raw_query[..]).await?; +        let result = client +            .query(&statement, &[]) +            .await? +            .iter() +            .map(|row| Commit::from_row_ref(row).unwrap()) +            .collect::<Vec<Commit>>(); + +        Ok(result) +    }  } diff --git a/src/errors.rs b/src/errors.rs index 8140cfe..1efb082 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -4,11 +4,12 @@ use serde::Serialize;  use std::fmt;  use tokio_postgres::error::Error; -#[derive(Debug)] +#[derive(Debug, Eq, PartialEq)]  pub enum AppErrorType {      DbError,      NotFoundError,      AuthorizationError, +    GitError,  }  #[derive(Debug)] @@ -72,6 +73,7 @@ impl ResponseError for AppError {              AppErrorType::DbError => StatusCode::INTERNAL_SERVER_ERROR,              AppErrorType::NotFoundError => StatusCode::NOT_FOUND,              AppErrorType::AuthorizationError => StatusCode::UNAUTHORIZED, +            AppErrorType::GitError => StatusCode::BAD_REQUEST,          }      } diff --git a/src/git.rs b/src/git.rs new file mode 100644 index 0000000..76f9a4e --- /dev/null +++ b/src/git.rs @@ -0,0 +1,142 @@ +use crate::commit::models::Commit; +use chrono::{DateTime, Local}; +use git2::{BranchType, Error, Repository, Time}; + +use std::fs::remove_dir_all; + +/// Return the temporary folder of the git repository +fn get_tmp_dir(repo_name: &String) -> String { +    format!("/tmp/{}", &repo_name) +} + +/// Clone a repository `repo` in a temporary folder. It uses only GitHub at +/// this moment. +pub fn clone_repo(repo_name: &String) -> Result<Repository, Error> { +    let url: &str = +        &format!("https://github.com/{}", &repo_name).to_owned()[..]; +    let tmp_dir: String = get_tmp_dir(&repo_name); + +    let cloned = Repository::clone(url, tmp_dir)?; + +    Ok(cloned) +} + +/// Check if a `branch` exists inside the repository and then set the head of +/// `repo` to that branch +pub fn get_branch(repo: &Repository, branch: &str) -> Result<(), Error> { +    if let Err(e) = repo.find_branch(&branch, BranchType::Local) { +        return Err(e); +    } + +    repo.set_head(&format!("refs/heads/{}", branch).to_owned()[..]) +} + +/// Get a `git2::Commit` and returns a date in RFC2822 format +pub fn get_date(commit: &git2::Commit) -> String { +    let t: Time = commit.time(); +    let (offset, sign) = match t.offset_minutes() { +        n if n < 0 => (-n, '-'), +        n => (n, '+'), +    }; +    let (hours, minutes) = (offset / 60, offset % 60); +    let ts = +        time::Timespec::new(t.seconds() + (t.offset_minutes() as i64) * 60, 0); +    let time = time::at(ts); + +    let result = format!( +        "{} {}{:02}{:02}", +        time.strftime("%a, %e %b %Y %T").unwrap(), +        sign, +        hours, +        minutes +    ); + +    result +} + +/// Get a `git2::Commit` and returns a valid `Commit` to upload to the database +fn get_commit(gcommit: &git2::Commit, repo_name: &String) -> Commit { +    let hash = gcommit.id().to_string(); +    let tree = match gcommit.parent_id(0) { +        Ok(parent) => Some(parent.to_string()), +        Err(_) => None, +    }; + +    let mut text = "".to_string(); +    for line in String::from_utf8_lossy(gcommit.message_bytes()).lines() { +        text += line; +        text += "\n"; +    } + +    // Remove the last "\n" +    let _ = text.pop(); + +    let date: DateTime<Local> = +        match DateTime::parse_from_rfc2822(&get_date(&gcommit)) { +            Ok(date) => date.into(), +            Err(e) => { +                // This is a real problem! +                // TODO: manage this error +                panic!(e) +            } +        }; +    let author_email = gcommit.author().email().unwrap().to_string(); +    let author_name = gcommit.author().name().unwrap().to_string(); +    let committer_email = gcommit.committer().email().unwrap().to_string(); +    let committer_name = gcommit.committer().name().unwrap().to_string(); + +    Commit { +        hash, +        tree, +        text, +        date, +        author_email, +        author_name, +        committer_email, +        committer_name, +        repository_url: repo_name.clone(), +    } +} + +/// Get all commits from a Git repository. Returns an array of `Commit`. +/// First, clone the repo into a temporary folder. +/// Then, open the repository. +/// Then, get commits +/// Finally, remove the temporary folder +pub fn repo_commits(repo_name: &String) -> Result<Vec<Commit>, Error> { +    // Try to clone the repo. If it returns an error, it's useless to go ahead: +    // raises an error. +    let repo = match clone_repo(&repo_name) { +        Ok(r) => r, +        Err(e) => { +            return Err(e); +        } +    }; + +    if let Err(e) = get_branch(&repo, "main") { +        return Err(e); +    } + +    let mut revwalk = repo.revwalk()?; +    let head = repo.head().unwrap().target().unwrap(); + +    if let Err(e) = revwalk.push(head) { +        return Err(e); +    } + +    let mut commits: Vec<Commit> = vec![]; +    for commit in revwalk { +        let hash = repo.find_commit(commit?)?; +        commits.push(get_commit(&hash, &repo_name)); +    } + +    if let Err(_) = remove_dir_all(get_tmp_dir(&repo_name)) { +        return Err(git2::Error::new( +            git2::ErrorCode::GenericError, +            git2::ErrorClass::Os, +            "Temporary clone not deleted", +        )); +    } + +    Ok(commits) +} diff --git a/src/main.rs b/src/main.rs index 6f20201..2e499f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,8 @@ mod db;  mod errors;  mod helpers; +mod git; +  mod branch;  mod commit;  mod email; diff --git a/src/repository/models.rs b/src/repository/models.rs index e3d8456..de82d45 100644 --- a/src/repository/models.rs +++ b/src/repository/models.rs @@ -1,5 +1,8 @@ +use crate::commit::models::Commit;  use crate::db::get_client; +use crate::email::models::{Email, EmailData};  use crate::errors::{AppError, AppErrorType}; +use crate::git;  use crate::helpers::name_of_git_repository;  use chrono::NaiveDateTime; @@ -9,6 +12,7 @@ use tokio_pg_mapper::FromTokioPostgresRow;  use tokio_pg_mapper_derive::PostgresMapper;  use uuid::Uuid; +use std::collections::HashSet;  use std::net::SocketAddr;  #[derive(Serialize, Deserialize, PostgresMapper)] @@ -185,7 +189,47 @@ impl Repository {              .map(|row| Repository::from_row_ref(&row).unwrap());          match repo { -            Some(repo) => Ok(repo), +            Some(repo) => { +                let commits = match git::repo_commits(&repo_name) { +                    Ok(c) => c, +                    Err(e) => { +                        return Err(AppError { +                            message: Some( +                                format!( +                                    "Repository couldn't be created now: {:?}", +                                    e +                                ) +                                .to_string(), +                            ), +                            cause: Some("Repository clone".to_string()), +                            error_type: AppErrorType::GitError, +                        }) +                    } +                }; + +                let mut emails: HashSet<String> = HashSet::new(); +                for commit in &commits { +                    emails.insert(commit.author_email.clone()); +                    emails.insert(commit.committer_email.clone()); +                } +                for email in emails { +                    if let Err(e) = +                        Email::create(pool.clone(), &EmailData { email }).await +                    { +                        if e.error_type == AppErrorType::DbError { +                            return Err(e); +                        } +                    } +                } + +                let commits_result = +                    Commit::create(pool.clone(), commits).await; +                if let Err(e) = commits_result { +                    return Err(e); +                } + +                Ok(repo) +            }              None => Err(AppError {                  message: Some("Error creating a new repository".to_string()),                  cause: Some("Unknown error".to_string()),  |