Initial import
This commit is contained in:
commit
707f6a7a62
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
/configs
|
2139
Cargo.lock
generated
Normal file
2139
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
Cargo.toml
Normal file
19
Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "bebot"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
constant_time_eq = "0.3"
|
||||
env_logger = "0.10"
|
||||
http = "0.2"
|
||||
log = { version = "0.4", features = ["std"] }
|
||||
matrix-sdk = { version = "0.6", features = ["anyhow", "markdown", "rustls-tls"], default-features = false }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "macros"] }
|
||||
warp = "0.3"
|
2
rustfmt.toml
Normal file
2
rustfmt.toml
Normal file
@ -0,0 +1,2 @@
|
||||
edition = "2021"
|
||||
max_width = 120
|
129
src/config.rs
Normal file
129
src/config.rs
Normal file
@ -0,0 +1,129 @@
|
||||
use std::{collections::HashMap, fmt, fs::File, io::BufReader};
|
||||
|
||||
use anyhow::Context;
|
||||
use matrix_sdk::ruma::{OwnedRoomOrAliasId, OwnedUserId, RoomOrAliasId, UserId};
|
||||
use serde::de;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RepoConfig {
|
||||
pub token: String,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "deser_optional_room_or_alias_id")]
|
||||
pub room: Option<OwnedRoomOrAliasId>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
pub bind_address: Option<String>,
|
||||
pub bind_port: Option<u16>,
|
||||
#[serde(deserialize_with = "deser_user_id")]
|
||||
pub user_id: OwnedUserId,
|
||||
pub password: String,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "deser_optional_room_or_alias_id")]
|
||||
pub default_room: Option<OwnedRoomOrAliasId>,
|
||||
pub repo_configs: HashMap<String, RepoConfig>, // key is repo url without scheme; e.g.
|
||||
// gitlab.xfce.org/xfce/xfdesktop
|
||||
}
|
||||
|
||||
fn deser_user_id<'de, D>(deserializer: D) -> Result<OwnedUserId, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
{
|
||||
struct UserIdVisitor;
|
||||
|
||||
impl<'de> de::Visitor<'de> for UserIdVisitor {
|
||||
type Value = OwnedUserId;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a matrix user ID")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
UserId::parse(v).map_err(E::custom)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(UserIdVisitor)
|
||||
}
|
||||
|
||||
fn deser_room_or_alias_id<'de, D>(deserializer: D) -> Result<OwnedRoomOrAliasId, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
{
|
||||
struct RoomOrAliasIdVisitor;
|
||||
|
||||
impl<'de> de::Visitor<'de> for RoomOrAliasIdVisitor {
|
||||
type Value = OwnedRoomOrAliasId;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a matrix room ID")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
RoomOrAliasId::parse(v).map_err(E::custom)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(RoomOrAliasIdVisitor)
|
||||
}
|
||||
|
||||
fn deser_optional_room_or_alias_id<'de, D>(deserializer: D) -> Result<Option<OwnedRoomOrAliasId>, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
{
|
||||
struct OptionalRoomOrAliasIdVisitor;
|
||||
|
||||
impl<'de> de::Visitor<'de> for OptionalRoomOrAliasIdVisitor {
|
||||
type Value = Option<OwnedRoomOrAliasId>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("null or matrix room ID")
|
||||
}
|
||||
|
||||
fn visit_none<E>(self) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
Ok(Some(deser_room_or_alias_id(deserializer)?))
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
RoomOrAliasId::parse(v).map(Some).map_err(E::custom)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(OptionalRoomOrAliasIdVisitor)
|
||||
}
|
||||
|
||||
fn load_blocking(path: &String) -> anyhow::Result<Config> {
|
||||
let f = File::open(path)?;
|
||||
let r = BufReader::new(f);
|
||||
let config: Config = serde_yaml::from_reader(r)?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub async fn load<S: AsRef<str>>(path: S) -> anyhow::Result<Config> {
|
||||
let p = String::from(path.as_ref());
|
||||
let config = tokio::task::spawn_blocking(move || {
|
||||
load_blocking(&p).with_context(|| format!("Failed to load config from {}", p))
|
||||
})
|
||||
.await??;
|
||||
Ok(config)
|
||||
}
|
224
src/event.rs
Normal file
224
src/event.rs
Normal file
@ -0,0 +1,224 @@
|
||||
pub trait GitlabEventExt {
|
||||
fn project(&self) -> &Project;
|
||||
fn r#ref(&self) -> &str;
|
||||
fn user(&self) -> &str;
|
||||
fn url(&self) -> String;
|
||||
fn title(&self) -> String;
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Project {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub web_url: String,
|
||||
pub avatar_url: Option<String>,
|
||||
pub git_ssh_url: String,
|
||||
pub git_http_url: String,
|
||||
pub namespace: String,
|
||||
pub visibility_level: u32,
|
||||
pub path_with_namespace: String,
|
||||
pub default_branch: String,
|
||||
pub homepage: String,
|
||||
pub url: String,
|
||||
pub ssh_url: String,
|
||||
pub http_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Repository {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub description: String,
|
||||
pub homepage: String,
|
||||
pub git_http_url: String,
|
||||
pub git_ssh_url: String,
|
||||
pub visibility_level: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Commit {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "object_kind")]
|
||||
pub enum GitlabEvent {
|
||||
#[serde(rename = "push")]
|
||||
Push {
|
||||
event_name: String,
|
||||
before: String,
|
||||
after: String,
|
||||
r#ref: String,
|
||||
ref_protected: bool,
|
||||
checkout_sha: String,
|
||||
user_id: u64,
|
||||
user_name: String,
|
||||
user_username: String,
|
||||
user_email: String,
|
||||
user_avatar: Option<String>,
|
||||
project_id: u64,
|
||||
project: Project,
|
||||
repository: Repository,
|
||||
commits: Vec<Commit>,
|
||||
total_commits_count: u64,
|
||||
},
|
||||
#[serde(rename = "tag_push")]
|
||||
TagPush {
|
||||
event_name: String,
|
||||
before: String,
|
||||
after: String,
|
||||
r#ref: String,
|
||||
ref_protected: bool,
|
||||
checkout_sha: String,
|
||||
user_id: u64,
|
||||
user_name: String,
|
||||
user_avatar: Option<String>,
|
||||
project_id: u64,
|
||||
project: Project,
|
||||
repository: Repository,
|
||||
commits: Vec<Commit>,
|
||||
total_commits_count: u64,
|
||||
},
|
||||
}
|
||||
|
||||
impl GitlabEventExt for GitlabEvent {
|
||||
fn project(&self) -> &Project {
|
||||
match self {
|
||||
GitlabEvent::Push { project, .. } => &project,
|
||||
GitlabEvent::TagPush { project, .. } => &project,
|
||||
}
|
||||
}
|
||||
|
||||
fn r#ref(&self) -> &str {
|
||||
match self {
|
||||
GitlabEvent::Push { r#ref, .. } => &r#ref,
|
||||
GitlabEvent::TagPush { r#ref, .. } => &r#ref,
|
||||
}
|
||||
}
|
||||
|
||||
fn user(&self) -> &str {
|
||||
match self {
|
||||
GitlabEvent::Push { user_name, .. } => &user_name,
|
||||
GitlabEvent::TagPush { user_name, .. } => &user_name,
|
||||
}
|
||||
}
|
||||
|
||||
fn url(&self) -> String {
|
||||
let url = match self {
|
||||
GitlabEvent::Push { after, project, .. } => format!("{}/-/commits/{}", project.web_url, after),
|
||||
GitlabEvent::TagPush {
|
||||
r#ref,
|
||||
checkout_sha,
|
||||
project,
|
||||
..
|
||||
} => {
|
||||
let refname = r#ref.split('/').into_iter().last().unwrap_or(checkout_sha);
|
||||
format!("{}/-/tags/{}", project.web_url, refname)
|
||||
}
|
||||
};
|
||||
|
||||
url.replace("http://", "https://").to_string()
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
fn find_commit<'a>(commits: &'a Vec<Commit>, sha: &str) -> Option<&'a Commit> {
|
||||
commits.iter().find(|commit| commit.id == sha)
|
||||
}
|
||||
|
||||
match self {
|
||||
GitlabEvent::Push { after, commits, .. } => find_commit(commits, &after)
|
||||
.map(|commit| commit.title.clone())
|
||||
.unwrap_or_else(|| "New commit(s) pushed".to_string()),
|
||||
GitlabEvent::TagPush {
|
||||
checkout_sha, commits, ..
|
||||
} => find_commit(commits, &checkout_sha)
|
||||
.map(|commit| commit.title.clone())
|
||||
.unwrap_or_else(|| "New tag pushed".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use std::{fs::File, io::BufReader};
|
||||
|
||||
fn load_test_data(name: &str) -> anyhow::Result<GitlabEvent> {
|
||||
let f = File::open(&format!("{}/test-data/{}.json", env!("CARGO_MANIFEST_DIR"), name))?;
|
||||
let r = BufReader::new(f);
|
||||
let event: GitlabEvent = serde_json::from_reader(r)?;
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn parse_push_event() -> anyhow::Result<()> {
|
||||
let event = load_test_data("push-event")?;
|
||||
|
||||
match event {
|
||||
GitlabEvent::Push {
|
||||
event_name,
|
||||
before,
|
||||
after,
|
||||
r#ref,
|
||||
checkout_sha,
|
||||
user_username,
|
||||
project,
|
||||
repository,
|
||||
total_commits_count,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(event_name, "push");
|
||||
assert_eq!(before, "95790bf891e76fee5e1747ab589903a6a1f80f22");
|
||||
assert_eq!(after, "da1560886d4f094c3e6c9ef40349f7d38b5d27d7");
|
||||
assert_eq!(r#ref, "refs/heads/master");
|
||||
assert_eq!(checkout_sha, "da1560886d4f094c3e6c9ef40349f7d38b5d27d7");
|
||||
assert_eq!(user_username, "jsmith");
|
||||
assert_eq!(project.name, "Diaspora");
|
||||
assert_eq!(project.namespace, "Mike");
|
||||
assert_eq!(repository.name, "Diaspora");
|
||||
assert_eq!(repository.url, "git@example.com:mike/diaspora.git");
|
||||
assert_eq!(total_commits_count, 4);
|
||||
}
|
||||
_ => panic!("not a push event"),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn parse_tag_push_event() -> anyhow::Result<()> {
|
||||
let event = load_test_data("tag-push-event")?;
|
||||
|
||||
match event {
|
||||
GitlabEvent::TagPush {
|
||||
event_name,
|
||||
before,
|
||||
after,
|
||||
r#ref,
|
||||
checkout_sha,
|
||||
user_name,
|
||||
project,
|
||||
repository,
|
||||
total_commits_count,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(event_name, "tag_push");
|
||||
assert_eq!(before, "0000000000000000000000000000000000000000");
|
||||
assert_eq!(after, "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7");
|
||||
assert_eq!(r#ref, "refs/tags/v1.0.0");
|
||||
assert_eq!(checkout_sha, "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7");
|
||||
assert_eq!(user_name, "John Smith");
|
||||
assert_eq!(project.name, "Example");
|
||||
assert_eq!(project.namespace, "Jsmith");
|
||||
assert_eq!(repository.name, "Example");
|
||||
assert_eq!(total_commits_count, 0);
|
||||
}
|
||||
_ => panic!("not a tag push event"),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
205
src/main.rs
Normal file
205
src/main.rs
Normal file
@ -0,0 +1,205 @@
|
||||
#[macro_use(anyhow)]
|
||||
extern crate anyhow;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[macro_use]
|
||||
extern crate serde;
|
||||
|
||||
mod config;
|
||||
mod event;
|
||||
|
||||
use std::{env, net::IpAddr, process::exit, sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::Context;
|
||||
use constant_time_eq::constant_time_eq;
|
||||
use event::{GitlabEvent, GitlabEventExt};
|
||||
use http::StatusCode;
|
||||
use matrix_sdk::{
|
||||
config::SyncSettings,
|
||||
room::Joined,
|
||||
ruma::{events::room::message::RoomMessageEventContent, OwnedRoomOrAliasId},
|
||||
BaseRoom, Client,
|
||||
};
|
||||
use warp::Filter;
|
||||
|
||||
async fn build_sync_settings(matrix_client: &Client) -> SyncSettings {
|
||||
let mut settings = SyncSettings::default().timeout(Duration::from_secs(30));
|
||||
if let Some(token) = matrix_client.sync_token().await {
|
||||
settings = settings.token(token);
|
||||
}
|
||||
settings
|
||||
}
|
||||
|
||||
async fn matrix_connect(config: &config::Config) -> anyhow::Result<Client> {
|
||||
let client = Client::builder()
|
||||
.server_name(config.user_id.server_name())
|
||||
.user_agent(format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")))
|
||||
.build()
|
||||
.await?;
|
||||
client
|
||||
.login_username(&config.user_id, &config.password)
|
||||
.initial_device_display_name("Bebot")
|
||||
.send()
|
||||
.await?;
|
||||
info!("Connected to matrix as {}; waiting for first sync", config.user_id);
|
||||
|
||||
let settings = build_sync_settings(&client).await;
|
||||
client.sync_once(settings).await?;
|
||||
info!("First matrix sync complete");
|
||||
|
||||
let sync_client = client.clone();
|
||||
tokio::spawn(async move {
|
||||
let settings = build_sync_settings(&sync_client).await;
|
||||
if let Err(err) = sync_client.sync(settings).await {
|
||||
error!("Matrix sync failed: {}", err);
|
||||
exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
async fn ensure_matrix_room_joined(matrix_client: &Client, room_id: &OwnedRoomOrAliasId) -> anyhow::Result<Joined> {
|
||||
fn room_matches(a_room: &BaseRoom, our_room: &OwnedRoomOrAliasId) -> bool {
|
||||
let our_room_str = our_room.as_str();
|
||||
a_room.room_id().as_str() == our_room_str
|
||||
|| a_room
|
||||
.canonical_alias()
|
||||
.iter()
|
||||
.find(|alias| alias.as_str() == our_room_str)
|
||||
.is_some()
|
||||
|| a_room
|
||||
.alt_aliases()
|
||||
.iter()
|
||||
.find(|alias| alias.as_str() == our_room_str)
|
||||
.is_some()
|
||||
}
|
||||
|
||||
let mut room = matrix_client
|
||||
.joined_rooms()
|
||||
.iter()
|
||||
.find(|a_room| room_matches(*a_room, room_id))
|
||||
.map(|room| room.clone());
|
||||
if room.is_none() {
|
||||
if let Some(invited) = matrix_client
|
||||
.invited_rooms()
|
||||
.iter()
|
||||
.find(|a_room| room_matches(*a_room, room_id))
|
||||
{
|
||||
invited.accept_invitation().await?;
|
||||
} else {
|
||||
matrix_client.join_room_by_id_or_alias(room_id, &[]).await?;
|
||||
}
|
||||
let settings = build_sync_settings(&matrix_client).await;
|
||||
matrix_client.sync_once(settings).await?;
|
||||
room = matrix_client
|
||||
.joined_rooms()
|
||||
.iter()
|
||||
.find(|a_room| room_matches(*a_room, room_id))
|
||||
.map(|room| room.clone());
|
||||
}
|
||||
|
||||
room.ok_or_else(|| anyhow!("Unable to join room {}", room_id))
|
||||
}
|
||||
|
||||
fn build_gitlab_message(event: &GitlabEvent) -> String {
|
||||
let project = event.project();
|
||||
let refname = event
|
||||
.r#ref()
|
||||
.split('/')
|
||||
.last()
|
||||
.unwrap_or_else(|| event.r#ref())
|
||||
.to_string();
|
||||
format!(
|
||||
"*{}* {} **{}** [{}]({})",
|
||||
project.path_with_namespace,
|
||||
refname,
|
||||
event.user(),
|
||||
event.title(),
|
||||
event.url()
|
||||
)
|
||||
}
|
||||
|
||||
async fn handle_gitlab_event(
|
||||
event: GitlabEvent,
|
||||
room_id: &OwnedRoomOrAliasId,
|
||||
matrix_client: &Client,
|
||||
) -> anyhow::Result<()> {
|
||||
let room = ensure_matrix_room_joined(matrix_client, room_id).await?;
|
||||
let msg = build_gitlab_message(&event);
|
||||
debug!("Sending message to {}: {}", room_id, msg);
|
||||
let msg_content = RoomMessageEventContent::text_markdown(&msg);
|
||||
room.send(msg_content, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run() -> anyhow::Result<()> {
|
||||
let config_path = env::args()
|
||||
.nth(1)
|
||||
.ok_or_else(|| anyhow!("Config file should be passed as only parameter"))?;
|
||||
let config = Arc::new(config::load(config_path).await?);
|
||||
let addr = config
|
||||
.bind_address
|
||||
.as_ref()
|
||||
.map(|ba| ba.clone())
|
||||
.unwrap_or_else(|| "127.0.0.1".to_string())
|
||||
.parse::<IpAddr>()
|
||||
.context("Failed to parse bind_address")?;
|
||||
let port = config.bind_port.unwrap_or(3000);
|
||||
|
||||
let matrix_client = matrix_connect(&config).await.context("Failed to connect to Matrix")?;
|
||||
|
||||
let gitlab = warp::path!("hooks" / "gitlab")
|
||||
.and(warp::path::end())
|
||||
.and(warp::post())
|
||||
.and(warp::header::<String>("x-gitlab-token"))
|
||||
.and(warp::body::json())
|
||||
.then(move |token: String, event: event::GitlabEvent| {
|
||||
let config = Arc::clone(&config);
|
||||
let matrix_client = matrix_client.clone();
|
||||
|
||||
async move {
|
||||
let project = event.project();
|
||||
let config_key = project.homepage.replace("http://", "").replace("https://", "");
|
||||
if let Some(repo_config) = config.repo_configs.get(&config_key) {
|
||||
if !constant_time_eq(token.as_bytes(), repo_config.token.as_bytes()) {
|
||||
warn!("Invalid token for repo '{}'", config_key);
|
||||
warp::reply::with_status("Invalid token", StatusCode::FORBIDDEN)
|
||||
} else {
|
||||
debug!("payload: {:?}", event);
|
||||
if let Some(room) = repo_config.room.as_ref().or(config.default_room.as_ref()) {
|
||||
if let Err(err) = handle_gitlab_event(event, &room, &matrix_client).await {
|
||||
warn!("Failed to handle payload: {}", err);
|
||||
}
|
||||
warp::reply::with_status("OK", StatusCode::OK)
|
||||
} else {
|
||||
info!("Channel not configured for repo '{}'", config_key);
|
||||
warp::reply::with_status("Matrix room not configured for repo", StatusCode::NOT_FOUND)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("Repo '{}' unconfigured", config_key);
|
||||
warp::reply::with_status("Repo not configured", StatusCode::NOT_FOUND)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let routes = gitlab.with(warp::log("bebot"));
|
||||
|
||||
warp::serve(routes).run((addr, port)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let lenv = env_logger::Env::new()
|
||||
.filter("BEBOT_LOG")
|
||||
.write_style("BEBOT_LOG_STYLE");
|
||||
env_logger::init_from_env(lenv);
|
||||
|
||||
if let Err(err) = run().await {
|
||||
error!("{}", err);
|
||||
exit(1);
|
||||
}
|
||||
}
|
72
test-data/push-event.json
Normal file
72
test-data/push-event.json
Normal file
@ -0,0 +1,72 @@
|
||||
{
|
||||
"object_kind": "push",
|
||||
"event_name": "push",
|
||||
"before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
|
||||
"after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
|
||||
"ref": "refs/heads/master",
|
||||
"ref_protected": true,
|
||||
"checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
|
||||
"user_id": 4,
|
||||
"user_name": "John Smith",
|
||||
"user_username": "jsmith",
|
||||
"user_email": "john@example.com",
|
||||
"user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
|
||||
"project_id": 15,
|
||||
"project":{
|
||||
"id": 15,
|
||||
"name":"Diaspora",
|
||||
"description":"",
|
||||
"web_url":"http://example.com/mike/diaspora",
|
||||
"avatar_url":null,
|
||||
"git_ssh_url":"git@example.com:mike/diaspora.git",
|
||||
"git_http_url":"http://example.com/mike/diaspora.git",
|
||||
"namespace":"Mike",
|
||||
"visibility_level":0,
|
||||
"path_with_namespace":"mike/diaspora",
|
||||
"default_branch":"master",
|
||||
"homepage":"http://example.com/mike/diaspora",
|
||||
"url":"git@example.com:mike/diaspora.git",
|
||||
"ssh_url":"git@example.com:mike/diaspora.git",
|
||||
"http_url":"http://example.com/mike/diaspora.git"
|
||||
},
|
||||
"repository":{
|
||||
"name": "Diaspora",
|
||||
"url": "git@example.com:mike/diaspora.git",
|
||||
"description": "",
|
||||
"homepage": "http://example.com/mike/diaspora",
|
||||
"git_http_url":"http://example.com/mike/diaspora.git",
|
||||
"git_ssh_url":"git@example.com:mike/diaspora.git",
|
||||
"visibility_level":0
|
||||
},
|
||||
"commits": [
|
||||
{
|
||||
"id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
|
||||
"message": "Update Catalan translation to e38cb41.\n\nSee https://gitlab.com/gitlab-org/gitlab for more information",
|
||||
"title": "Update Catalan translation to e38cb41.",
|
||||
"timestamp": "2011-12-12T14:27:31+02:00",
|
||||
"url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
|
||||
"author": {
|
||||
"name": "Jordi Mallach",
|
||||
"email": "jordi@softcatala.org"
|
||||
},
|
||||
"added": ["CHANGELOG"],
|
||||
"modified": ["app/controller/application.rb"],
|
||||
"removed": []
|
||||
},
|
||||
{
|
||||
"id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
|
||||
"message": "fixed readme",
|
||||
"title": "fixed readme",
|
||||
"timestamp": "2012-01-03T23:36:29+02:00",
|
||||
"url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
|
||||
"author": {
|
||||
"name": "GitLab dev user",
|
||||
"email": "gitlabdev@dv6700.(none)"
|
||||
},
|
||||
"added": ["CHANGELOG"],
|
||||
"modified": ["app/controller/application.rb"],
|
||||
"removed": []
|
||||
}
|
||||
],
|
||||
"total_commits_count": 4
|
||||
}
|
41
test-data/tag-push-event.json
Normal file
41
test-data/tag-push-event.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"object_kind": "tag_push",
|
||||
"event_name": "tag_push",
|
||||
"before": "0000000000000000000000000000000000000000",
|
||||
"after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
|
||||
"ref": "refs/tags/v1.0.0",
|
||||
"ref_protected": true,
|
||||
"checkout_sha": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
|
||||
"user_id": 1,
|
||||
"user_name": "John Smith",
|
||||
"user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
|
||||
"project_id": 1,
|
||||
"project":{
|
||||
"id": 1,
|
||||
"name":"Example",
|
||||
"description":"",
|
||||
"web_url":"http://example.com/jsmith/example",
|
||||
"avatar_url":null,
|
||||
"git_ssh_url":"git@example.com:jsmith/example.git",
|
||||
"git_http_url":"http://example.com/jsmith/example.git",
|
||||
"namespace":"Jsmith",
|
||||
"visibility_level":0,
|
||||
"path_with_namespace":"jsmith/example",
|
||||
"default_branch":"master",
|
||||
"homepage":"http://example.com/jsmith/example",
|
||||
"url":"git@example.com:jsmith/example.git",
|
||||
"ssh_url":"git@example.com:jsmith/example.git",
|
||||
"http_url":"http://example.com/jsmith/example.git"
|
||||
},
|
||||
"repository":{
|
||||
"name": "Example",
|
||||
"url": "ssh://git@example.com/jsmith/example.git",
|
||||
"description": "",
|
||||
"homepage": "http://example.com/jsmith/example",
|
||||
"git_http_url":"http://example.com/jsmith/example.git",
|
||||
"git_ssh_url":"git@example.com:jsmith/example.git",
|
||||
"visibility_level":0
|
||||
},
|
||||
"commits": [],
|
||||
"total_commits_count": 0
|
||||
}
|
Loading…
Reference in New Issue
Block a user