Skip to content

Commit

Permalink
Create pull request from fork
Browse files Browse the repository at this point in the history
  • Loading branch information
dima74 committed Feb 4, 2024
1 parent 34da896 commit 489b254
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 28 deletions.
22 changes: 12 additions & 10 deletions src/git_util.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
use std::io;
use std::io::Write;
use std::path::Path;
use std::process::Command;

use log::warn;
use log::{error, warn};

use crate::github::GITHUB_BRANCH_NAME;
use crate::github::{GITHUB_BRANCH_NAME, GITHUB_USER_NAME};

pub fn clone(url: &str, path: &Path) {
execute_git_command(
Expand All @@ -16,7 +14,7 @@ pub fn clone(url: &str, path: &Path) {
url,
".", // clone to current directory
],
true
true,
)
}

Expand All @@ -43,14 +41,16 @@ pub fn commit(path: &Path) {
}

pub fn push(path: &Path) {
execute_git_command(&path, &["push"], false)
execute_git_command(&path, &["push"], false);
}

pub fn push_to_crowdin_branch(path: &Path) {
execute_git_command(&path, &["push", "-d", "origin", GITHUB_BRANCH_NAME], false);
pub fn push_to_my_fork(path: &Path, repo: &str) {
let personal_token = dotenv::var("GITHUB_PERSONAL_ACCESS_TOKEN").unwrap();
let url = format!("https://x-access-token:{}@github.com/{}/{}.git", personal_token, GITHUB_USER_NAME, repo);
execute_git_command(&path, &["remote", "add", "my", &url], true);

let refspec = format!("HEAD:{}", GITHUB_BRANCH_NAME);
execute_git_command(&path, &["push", "origin", &refspec], true);
execute_git_command(&path, &["push", "my", &refspec, "--force"], true);
}

fn has_changes(path: &Path) -> bool {
Expand All @@ -70,11 +70,13 @@ fn execute_git_command(path: &Path, args: &[&str], panic_if_fail: bool) {
.output()
.expect("Failed to execute git command");
if !result.status.success() {
io::stderr().write_all(&result.stderr).unwrap();
let stderr = String::from_utf8_lossy(&result.stderr);
let message = format!("Failed to execute `git {}`", args.join(" "));
if panic_if_fail {
error!("{}", stderr);
panic!("{}", message);
} else {
warn!("{}", stderr);
warn!("{}", message);
}
}
Expand Down
68 changes: 57 additions & 11 deletions src/github.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
use std::path::Path;
use std::time::Duration;

use jsonwebtoken::EncodingKey;
use log::info;
use octocrab::{Error, Octocrab, Page};
use octocrab::models::{AppId, Installation, InstallationId, Repository};
use octocrab::models::pulls::PullRequest;
use rocket::serde::Deserialize;
use sentry::Level;
use serde::de::DeserializeOwned;
use tokio::time::sleep;

use crate::{git_util, util};
use crate::mod_directory::ModDirectory;
use crate::util::EmptyBody;

pub const GITHUB_USER_NAME: &str = "factorio-mods-helper";
pub const GITHUB_BRANCH_NAME: &str = "crowdin-fml";
const MAX_PER_PAGE: u8 = 100;

Expand Down Expand Up @@ -146,13 +150,14 @@ async fn clone_repository_to(full_name: &str, installation_id: InstallationId, p
git_util::clone(&url, path);
}

pub async fn create_pull_request(installation_api: &Octocrab, full_name: &str, default_branch: &str) {
pub async fn create_pull_request(personal_api: &Octocrab, full_name: &str, default_branch: &str) {
let (owner, repo) = full_name.split_once('/').unwrap();
let title = "Update translations from Crowdin";
let body = "See https://github.com/dima74/factorio-mods-localization for details";
let result = installation_api
let head_branch = format!("{}:{}", GITHUB_USER_NAME, GITHUB_BRANCH_NAME);
let result = personal_api
.pulls(owner, repo)
.create(title, GITHUB_BRANCH_NAME, default_branch)
.create(title, head_branch, default_branch)
.body(body)
.maintainer_can_modify(true)
.send().await;
Expand All @@ -161,17 +166,22 @@ pub async fn create_pull_request(installation_api: &Octocrab, full_name: &str, d

fn check_create_pull_request_response(result: octocrab::Result<PullRequest>, full_name: &str) {
let Err(err) = result else { return; };
if let Error::GitHub { source, .. } = &err {
if source.message.starts_with("A pull request already exists for") {
return; // PR exists - no need to reopen, force push is enough
}
if source.message.starts_with("Resource not accessible by integration") {
return; // Means that user not yet accepted new "pull requests" permission for the app
}
if is_error_pull_request_already_exists(&err) {
// PR exists - no need to reopen, force push is enough
return;
}
panic!("[{}] Can't create pull request: {}", full_name, err);
}

fn is_error_pull_request_already_exists(error: &Error) -> bool {
let Error::GitHub { source, .. } = &error else { return false; };
if source.message != "Validation Failed" { return false; };
let Some(&[ref error, ..]) = source.errors.as_deref() else { return false; };
let serde_json::Value::Object(error) = error else { return false; };
let Some(serde_json::Value::String(message)) = error.get("message") else { return false; };
message.starts_with("A pull request already exists for")
}

pub async fn get_default_branch(installation_api: &Octocrab, full_name: &str) -> String {
#[derive(Deserialize)]
struct Response { default_branch: String }
Expand All @@ -185,7 +195,7 @@ pub async fn is_branch_protected(installation_api: &Octocrab, full_name: &str, b
struct Response { protected: bool }
let url = format!("/repos/{}/branches/{}", full_name, branch);
let result: Response = installation_api.get(&url, None::<&()>).await.unwrap();
return result.protected
result.protected
}

pub fn as_personal_account() -> Octocrab {
Expand All @@ -196,6 +206,42 @@ pub fn as_personal_account() -> Octocrab {
.unwrap()
}

pub async fn fork_repository(personal_api: &Octocrab, owner: &str, repo: &str) -> bool {
if let Some(result) = check_fork_exists(&personal_api, owner, repo).await {
return result;
}

info!("[update-github-from-crowdin] [{}/{}] forking repository...", owner, repo);
personal_api
.repos(owner, repo)
.create_fork()
.send().await.unwrap();
sleep(Duration::from_secs(120)).await;
true
}

async fn check_fork_exists(api: &Octocrab, owner: &str, repo: &str) -> Option<bool> {
let forks = api
.repos(owner, repo)
.list_forks()
.send().await.unwrap()
.all_pages(&api).await.unwrap();
for fork in forks {
let fork_full_name = fork.full_name.unwrap();
let (fork_owner, fork_repo) = fork_full_name.split_once('/').unwrap();
if fork_owner == GITHUB_USER_NAME {
return if fork_repo == repo {
Some(true) // fork already exists
} else {
let message = format!("Fork name {} doesn't match repository {}/{}", fork_repo, owner, repo);
sentry::capture_message(&message, Level::Error);
Some(false)
};
}
}
None
}

pub async fn star_repository(api: &Octocrab, full_name: &str) {
let _response: octocrab::Result<EmptyBody> = api
.put(format!("/user/starred/{}", full_name), None::<&()>)
Expand Down
25 changes: 18 additions & 7 deletions src/server/trigger_update.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
use std::fs;
use std::path::Path;
use std::sync::LazyLock;
use std::time::Duration;

use log::{info, warn};
use log::info;
use octocrab::models::InstallationId;
use rocket::get;
use tempfile::TempDir;
use tokio::time::sleep;

use crate::{crowdin, git_util, github, util};
use crate::crowdin::{get_crowdin_directory_name, normalize_language_code, replace_ini_to_cfg};
use crate::github::as_personal_account;
use crate::mod_directory::ModDirectory;

#[get("/triggerUpdate?<repo>&<secret>")]
Expand Down Expand Up @@ -46,7 +49,7 @@ async fn push_repository_crowdin_changes_to_github(full_name: &str) -> &'static
let repositories = vec![(full_name.to_owned(), installation_id)];
let success = push_crowdin_changes_to_github(repositories).await;
if !success {
return "Can't find mod directory on crowdin"
return "Can't find mod directory on crowdin";
}
info!("[update-github-from-crowdin] [{}] success", full_name);
"Ok"
Expand Down Expand Up @@ -87,11 +90,7 @@ async fn push_repository_crowdin_changes_to_github_impl(
let default_branch = github::get_default_branch(&installation_api, &full_name).await;
let is_protected = github::is_branch_protected(&installation_api, &full_name, &default_branch).await;
if is_protected {
// todo fork repository and push to fork
warn!("[update-github-from-crowdin] [{}] can't push because branch is protected", full_name);
// git_util::push_to_crowdin_branch(path);
// github::create_pull_request(&installation_api, &full_name, &default_branch).await;
// info!("[update-github-from-crowdin] [{}] pushed to crowdin-fml branch and created PR", full_name);
push_changes_using_pull_request(path, &full_name, &default_branch).await;
} else {
git_util::push(path);
info!("[update-github-from-crowdin] [{}] pushed", full_name);
Expand All @@ -101,6 +100,18 @@ async fn push_repository_crowdin_changes_to_github_impl(
}
}

async fn push_changes_using_pull_request(path: &Path, full_name: &str, default_branch: &str) {
let personal_api = as_personal_account();
let (owner, repo) = full_name.split_once('/').unwrap();
if !github::fork_repository(&personal_api, owner, repo).await {
return;
}
git_util::push_to_my_fork(path, repo);
sleep(Duration::from_secs(30)).await;
github::create_pull_request(&personal_api, &full_name, &default_branch).await;
info!("[update-github-from-crowdin] [{}] pushed to crowdin-fml branch and created PR", full_name);
}

async fn move_translated_files_to_repository(mod_directory: &ModDirectory, translation_directory: &Path) {
for (language_path, language) in util::read_dir(translation_directory) {
let language_path_crowdin = language_path.join(get_crowdin_directory_name(&mod_directory.github_full_name));
Expand Down

0 comments on commit 489b254

Please sign in to comment.