Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds github webhook integration #123

Merged
merged 4 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ impl ContentConfig
/// - ```key_path```: optional location of ssh key (ssh connection will be used)
/// - ```user```: user name for authentication
/// - ```passphrase```: passphrase for ssh key or for user-pass auth
/// <div class="warning"><p>If using ssh keys be sure the host is added to ~/ssh/known_hosts for
///the user that runs busser, including root</p>
#[derive(Clone, Serialize, Deserialize)]
pub struct GitAuthConfig
{
Expand All @@ -128,11 +130,14 @@ pub struct GitAuthConfig
/// - ```remote```: the url (public or private)
/// - ```branch```: the tracked branch
/// - ```auth```: if present either ssh key or passphrase will be used
/// - ```checkout_schedule```: schedule for checking for new commits on [GitConfig::branch]
/// - ```remote_webhook_token```: optional webhook token to recieve push events
#[derive(Clone, Serialize, Deserialize)]
pub struct GitConfig
{
pub remote: String,
pub branch: String,
pub remote_webhook_token: Option<String>,
pub checkout_schedule: Option<String>,
pub auth: Option<GitAuthConfig>
}
Expand Down
69 changes: 60 additions & 9 deletions src/integrations/git/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,14 @@ impl From<std::io::Error> for GitError
/// Attempt to clone a remote repo from a [crate::config::GitConfig]
pub fn from_clone(path: &str, config: &GitConfig) -> Result<Repository, GitError>
{
if let GitConfig{auth: Some(_), remote: _, checkout_schedule: _, branch: _} = config
if let GitConfig{auth: Some(_), remote: _, checkout_schedule: _, branch: _, remote_webhook_token: _} = config
{
let auth = config.auth.clone().unwrap();
let result = match &auth.key_path
let callbacks = match &auth.key_path
{
Some(_) =>
{
crate::debug(format!("Attempting ssh key authenticated clone of {}", config.remote), Some("GIT"));
let mut callbacks = RemoteCallbacks::new();
callbacks.credentials(|_url, _username_from_url, _allowed_types|
{
Expand All @@ -65,6 +66,7 @@ pub fn from_clone(path: &str, config: &GitConfig) -> Result<Repository, GitError
},
None =>
{
crate::debug(format!("Attempting passphrase authenticated clone of {}", config.remote), Some("GIT"));
let mut callbacks = RemoteCallbacks::new();
callbacks.credentials(|_url, _username_from_url, _allowed_types|
{
Expand All @@ -78,7 +80,7 @@ pub fn from_clone(path: &str, config: &GitConfig) -> Result<Repository, GitError
};

let mut fo = git2::FetchOptions::new();
fo.remote_callbacks(result);
fo.remote_callbacks(callbacks);
let mut builder = git2::build::RepoBuilder::new();
builder.fetch_options(fo);
builder.branch(&config.branch);
Expand All @@ -87,19 +89,20 @@ pub fn from_clone(path: &str, config: &GitConfig) -> Result<Repository, GitError
Ok(repo) => Ok(repo),
Err(e) =>
{
crate::debug(format!("Error {} while cloning (authenticated) repo at {}", e, config.remote), None);
crate::debug(format!("Error {} while cloning (authenticated) repo at {}", e, config.remote), Some("GIT"));
Err(GitError::from(e))
}
}
}
else
{
crate::debug(format!("Attempting un-authenticated clone of {}", config.remote), Some("GIT"));
match Repository::clone(&config.remote, path)
{
Ok(repo) => Ok(repo),
Err(e) =>
{
crate::debug(format!("Error {} while cloning (pub) repo at {}", e, config.remote), None);
crate::debug(format!("Error {} while cloning (pub) repo at {}", e, config.remote), Some("GIT"));
Err(GitError::from(e))
}
}
Expand All @@ -123,7 +126,15 @@ pub fn remove_repository(dir: &str) -> Result<(), std::io::Error>
/// deleting any file/dir called [crate::config::ContentConfig::path]
pub fn clean_and_clone(dir: &str, config: GitConfig) -> Result<Repository, GitError>
{
remove_repository(dir)?;
let path = Path::new(dir);
if path.is_dir()
{
remove_repository(dir)?;
}
else if path.is_file()
{
std::fs::remove_file(path)?;
}
match from_clone(dir, &config)
{
Ok(repo) =>
Expand All @@ -139,7 +150,7 @@ pub fn clean_and_clone(dir: &str, config: GitConfig) -> Result<Repository, GitEr

/// Fast forward pull from the repository, makes no attempt to resolve
/// if a fast foward is not possible
pub fn fast_forward_pull(repo: Repository, branch: &str) -> Result<(), GitError>
pub fn fast_forward_pull(repo: Repository, branch: &str) -> Result<Option<HeadInfo>, GitError>
{
// modified from https://stackoverflow.com/questions/58768910/how-to-perform-git-pull-with-the-rust-git2-crate
repo.find_remote("origin")?.fetch(&[branch], None, None)?;
Expand All @@ -150,18 +161,58 @@ pub fn fast_forward_pull(repo: Repository, branch: &str) -> Result<(), GitError>

if analysis.is_up_to_date()
{
Ok(())
Ok(None)
}
else if analysis.is_fast_forward()
{
let refname = format!("refs/heads/{}", branch);
let mut reference = repo.find_reference(&refname)?;
reference.set_target(fetch_commit.id(), "Fast-Forward")?;
repo.set_head(&refname)?;
Ok(repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))?)
repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))?;
Ok(head_info(&repo))
}
else
{
Err(GitError{why: "Cannot fastforward".to_owned()})
}
}

Jerboa-app marked this conversation as resolved.
Show resolved Hide resolved
/// Commit hash, author and timestamp for head commit
pub struct HeadInfo
{
pub hash: git2::Oid,
pub author: String,
pub datetime: String
}

/// Get the [HeadInfo] if it exists
pub fn head_info(repo: &Repository) -> Option<HeadInfo>
{
let head = match repo.head()
{
Ok(h) => match h.target()
{
Some(h) => h,
None => return None
},
Err(_) => return None
};

match repo.find_commit(head)
{
Ok(c) =>
{
Some
(
HeadInfo
{
hash: c.id(),
author: c.author().to_string(),
datetime: format!("{:?}", c.time())
}
)
},
Err(_) => None
}
}
84 changes: 69 additions & 15 deletions src/integrations/git/refresh.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
use std::{path::Path, sync::Arc};
use std::{path::Path, sync::Arc, time::SystemTime};

use axum::async_trait;
use chrono::{DateTime, Utc};
use cron::Schedule;
use git2::Repository;

use tokio::sync::Mutex;

use crate::{config::{Config, CONFIG_PATH}, task::{next_job_time, schedule_from_option, Task}};
use crate::{config::{Config, CONFIG_PATH}, integrations::discord::post::try_post, task::{next_job_time, schedule_from_option, Task}};

use super::{clean_and_clone, fast_forward_pull};
use super::{clean_and_clone, fast_forward_pull, HeadInfo};

pub struct GitRefreshTask
{
pub lock: Arc<Mutex<()>>,
pub lock: Arc<Mutex<SystemTime>>,
pub last_run: DateTime<Utc>,
pub next_run: Option<DateTime<Utc>>,
pub schedule: Option<Schedule>
Expand All @@ -22,7 +23,7 @@ impl GitRefreshTask
{
pub fn new
(
lock: Arc<Mutex<()>>,
lock: Arc<Mutex<SystemTime>>,
schedule: Option<Schedule>
) -> GitRefreshTask
{
Expand All @@ -34,18 +35,15 @@ impl GitRefreshTask
schedule
}
}
}

#[async_trait]
impl Task for GitRefreshTask
Jerboa-app marked this conversation as resolved.
Show resolved Hide resolved
{
async fn run(&mut self) -> Result<(), crate::task::TaskError>
/// Attempt a fast forward pull of the repo in [crate::config::ContentConfig::path]
/// clone if it is not there. Does nothing if [crate::config::GitConfig] is none
pub fn pull(config: &Config) -> Option<HeadInfo>
{
let _ = self.lock.lock().await;
let config = Config::load_or_default(CONFIG_PATH);

if config.git.is_some()
{
let git = config.git.unwrap();
let git = config.git.clone().unwrap();
let path = Path::new(&config.content.path);
if path.is_dir()
{
Expand All @@ -54,7 +52,7 @@ impl Task for GitRefreshTask
Ok(repo) => fast_forward_pull(repo, &git.branch),
Err(e) =>
{
crate::debug(format!("{}, {:?} is not a git repo", e, path), None);
crate::debug(format!("{}, {:?} is not a git repo", e, path), Some("GIT"));
match clean_and_clone(&config.content.path, git.clone())
{
Ok(repo) => fast_forward_pull(repo, &git.branch),
Expand All @@ -65,10 +63,66 @@ impl Task for GitRefreshTask

if result.is_err()
{
crate::debug(format!("{:?}", result.err()), None);
crate::debug(format!("{:?}", result.err()), Some("GIT"));
}
else
{
return result.unwrap()
}
}
else
{
let result = match clean_and_clone(&config.content.path, git.clone())
{
Ok(repo) => fast_forward_pull(repo, &git.branch),
Err(e) => Err(e)
};
if result.is_err()
{
crate::debug(format!("{:?}", result.err()), Some("GIT"));
}
}
}
None
}

/// Send a discord message with [HeadInfo] if it is Some
pub async fn notify_pull(info: Option<HeadInfo>, config: &Config)
{
Jerboa-app marked this conversation as resolved.
Show resolved Hide resolved
match info
{
Some(info) =>
{
let msg = format!
(
"Checked out new commit for {}:\n {}\n {}\n {}",
config.domain,
info.hash,
info.author,
info.datetime
);
crate::debug(msg.clone(), Some("GIT"));
try_post
(
config.notification_endpoint.clone(),
&msg
).await;
},
None => {}
}

}
}

#[async_trait]
impl Task for GitRefreshTask
{
async fn run(&mut self) -> Result<(), crate::task::TaskError>
{
let mut time = self.lock.lock().await;
let config = Config::load_or_default(CONFIG_PATH);
GitRefreshTask::notify_pull(GitRefreshTask::pull(&config), &config).await;
*time = SystemTime::now();

self.schedule = schedule_from_option(config.stats.save_schedule.clone());

Expand Down
Loading
Loading