Skip to content
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
96 changes: 70 additions & 26 deletions rook/src/git/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::env;
use std::fs;
use std::path::PathBuf;

const REGEX_URL: &str = r"https?://(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)";

/// Generate a unique directory name using repo owner and name
fn generate_dir_name(repo_url: &str, branch: Option<String>) -> String {
fn generate_dir_name(repo_url: &str, branch: Option<&str>) -> String {
let parts: Vec<&str> = repo_url
.trim_start_matches("https://github.com/")
.split('/')
Expand All @@ -18,7 +19,7 @@ fn generate_dir_name(repo_url: &str, branch: Option<String>) -> String {
"queensac_temp_repo/{}/{}/{}",
user_name,
repo_name,
branch.unwrap_or("default".to_string())
branch.unwrap_or("default")
)
}

Expand Down Expand Up @@ -49,42 +50,33 @@ impl std::hash::Hash for LinkInfo {

/// Checkout a specific branch in the repository
fn checkout_branch(repo: &Repository, branch_name: &str) -> Result<(), git2::Error> {
let remote_branch_name = format!("origin/{}", branch_name);
let mut remote = repo.find_remote("origin")?;

// 특정 브랜치만 fetch
let refspec = format!(
"refs/heads/{}:refs/remotes/origin/{}",
branch_name, branch_name
);
remote.fetch(&[&refspec], None, None)?;

let remote_ref = format!("refs/remotes/{}", remote_branch_name);
let remote_branch = format!("origin/{}", branch_name);
let reference = repo
.find_reference(&remote_ref)
.find_reference(&format!("refs/remotes/{}", remote_branch))
.map_err(|_| git2::Error::from_str(&format!("Branch not found: {}", branch_name)))?;

// Create a local branch tracking the remote branch
let commit = reference.peel_to_commit()?;
let branch = repo.branch(branch_name, &commit, false)?;
repo.set_head(branch.get().name().unwrap())?;
repo.checkout_head(None)?;

repo.checkout_tree(commit.as_object(), None)?;
repo.set_head(&format!("refs/heads/{}", branch_name))?;
Ok(())
}

pub fn extract_links_from_repo_url(
repo_url: &str,
branch: Option<String>,
) -> Result<HashSet<LinkInfo>, git2::Error> {
let temp_dir = env::temp_dir().join(generate_dir_name(repo_url, branch.clone()));
let temp_dir = env::temp_dir().join(generate_dir_name(repo_url, branch.as_deref()));
let _temp_dir_guard = TempDirGuard::new(temp_dir.clone()).map_err(|e| {
git2::Error::from_str(&format!("Failed to create temporary directory: {}", e))
})?;

// Clone repository
let repo = Repository::clone(repo_url, &temp_dir)?;

// 체크아웃 브랜치
// 브랜치가 지정된 경우에만 fetch와 체크아웃 수행
if let Some(branch_name) = branch {
let mut remote = repo.find_remote("origin")?;
remote.fetch(&["refs/heads/*:refs/remotes/origin/*"], None, None)?;
checkout_branch(&repo, &branch_name)?;
}

Expand Down Expand Up @@ -118,9 +110,7 @@ pub fn extract_links_from_repo_url(
}

fn find_link_in_content(content: &str, file_path: String) -> HashSet<LinkInfo> {
// TODO 정규표현식 캐싱
let url_regex = Regex::new(REGEX_URL).unwrap();

let mut result = HashSet::new();

for (line_num, line) in content.lines().enumerate() {
Expand All @@ -133,19 +123,19 @@ fn find_link_in_content(content: &str, file_path: String) -> HashSet<LinkInfo> {
result.insert(LinkInfo {
url,
file_path: file_path.clone(),
line_number: line_num + 1, // 1-based line number
line_number: line_num + 1,
});
}
}
result
}

struct TempDirGuard {
path: std::path::PathBuf,
path: PathBuf,
}

impl TempDirGuard {
fn new(path: std::path::PathBuf) -> std::io::Result<Self> {
fn new(path: PathBuf) -> std::io::Result<Self> {
if path.exists() {
fs::remove_dir_all(&path)?;
}
Expand Down Expand Up @@ -284,4 +274,58 @@ mod tests {
);
}
}

#[test]
#[serial]
fn test_checkout_branch_with_valid_branch() {
let repo_url = "https://github.com/reddevilmidzy/woowalog";
let temp_dir = env::temp_dir().join("test_checkout_branch");
let _temp_dir_guard = TempDirGuard::new(temp_dir.clone()).unwrap();

// Clone repository
let repo = Repository::clone(repo_url, &temp_dir).unwrap();

// Fetch all branches
let mut remote = repo.find_remote("origin").unwrap();
remote
.fetch(&["refs/heads/*:refs/remotes/origin/*"], None, None)
.unwrap();

// Test checkout with a known existing branch
let result = checkout_branch(&repo, "main");
assert!(
result.is_ok(),
"Should successfully checkout existing branch"
);
}

#[test]
#[serial]
fn test_checkout_branch_with_invalid_branch() {
let repo_url = "https://github.com/reddevilmidzy/woowalog";
let temp_dir = env::temp_dir().join("test_checkout_invalid_branch");
let _temp_dir_guard = TempDirGuard::new(temp_dir.clone()).unwrap();

// Clone repository
let repo = Repository::clone(repo_url, &temp_dir).unwrap();

// Fetch all branches
let mut remote = repo.find_remote("origin").unwrap();
remote
.fetch(&["refs/heads/*:refs/remotes/origin/*"], None, None)
.unwrap();

// Test checkout with a non-existent branch
let result = checkout_branch(&repo, "non-existent-branch");
assert!(
result.is_err(),
"Should fail to checkout non-existent branch"
);
if let Err(e) = result {
assert!(
e.message().contains("non-existent-branch"),
"Error message should contain the branch name"
);
}
}
}
58 changes: 19 additions & 39 deletions rook/src/schedule/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use tracing::{error, info, instrument, warn};
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
struct RepoKey {
repo_url: String,
branch: String,
branch: Option<String>,
}

static REPO_TASKS: Lazy<Mutex<HashMap<RepoKey, CancellationToken>>> =
Expand All @@ -22,7 +22,6 @@ pub async fn check_repository_links(
branch: Option<String>,
interval_duration: Duration,
) -> Result<(), String> {
let branch = branch.unwrap_or_else(|| "default".to_string());
let repo_key = RepoKey {
repo_url: repo_url.to_string(),
branch: branch.clone(),
Expand All @@ -33,7 +32,7 @@ pub async fn check_repository_links(
let mut map = REPO_TASKS.lock().unwrap();
if map.contains_key(&repo_key) {
return Err(format!(
"Repository {} (branch: {}) is already being monitored",
"Repository {} (branch: {:?}) is already being monitored",
repo_url, branch
));
}
Expand All @@ -43,17 +42,21 @@ pub async fn check_repository_links(
};

info!(
"Starting repository link checker for {} (branch: {})",
"Starting repository link checker for {} (branch: {:?})",
repo_url, branch
);

let mut interval = tokio::time::interval(interval_duration);
loop {
tokio::select! {
_ = interval.tick() => {
info!("Checking links for repository: {} (branch: {})", repo_url, branch);
info!(
"Checking links for repository: {} (branch: {:?})",
repo_url,
branch
);

match git::extract_links_from_repo_url(repo_url, Some(branch.clone())) {
match git::extract_links_from_repo_url(repo_url, branch.clone()) {
Ok(links) => {
info!("Found {} links to check", links.len());
let mut handles = Vec::new();
Expand All @@ -78,12 +81,17 @@ pub async fn check_repository_links(
}
Err(e) => error!("Error processing repository: {}", e),
}
info!("Link check completed for {} (branch: {}). Waiting for next interval...", repo_url, branch);
info!(
"Link check completed for {} (branch: {:?}). Waiting for next interval...",
repo_url,
branch
);
},
_ = token.cancelled() => {
info!(
"Repository checker cancelled for: {} (branch: {})",
repo_url, branch
"Repository checker cancelled for: {} (branch: {:?})",
repo_url,
branch
);
break;
}
Expand All @@ -98,7 +106,6 @@ pub async fn cancel_repository_checker(
repo_url: &str,
branch: Option<String>,
) -> Result<(), String> {
let branch = branch.unwrap_or_else(|| "default".to_string());
let repo_key = RepoKey {
repo_url: repo_url.to_string(),
branch: branch.clone(),
Expand All @@ -111,13 +118,13 @@ pub async fn cancel_repository_checker(
if let Some(token) = token {
token.cancel();
info!(
"Cancellation requested for repository: {} (branch: {})",
"Cancellation requested for repository: {} (branch: {:?})",
repo_url, branch
);
Ok(())
} else {
Err(format!(
"No active checker found for repository: {} (branch: {})",
"No active checker found for repository: {} (branch: {:?})",
repo_url, branch
))
}
Expand All @@ -130,33 +137,6 @@ mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::time::timeout;

// #[tokio::test]
// FIXME: 미션 mock 사용하여 테스트를 가능케하라.
// async fn test_duplicate_repository() {
// let repo_url = "https://github.com/reddevilmidzy/woowalog";
// let interval = Duration::from_millis(100);

// // First call should succeed
// let result1 = check_repository_links(repo_url, None, interval).await;
// assert!(result1.is_ok(), "First call should succeed");

// // Second call should fail (same repo, default branch)
// let result2 = check_repository_links(repo_url, None, interval).await;
// assert!(result2.is_err(), "Second call should fail");
// assert!(result2.unwrap_err().contains("already being monitored"));

// // Call with different branch should succeed
// let result3 = check_repository_links(repo_url, Some("main".to_string()), interval).await;
// assert!(result3.is_ok(), "Call with different branch should succeed");

// // Cancel the first checker
// cancel_repository_checker(repo_url, None).await.unwrap();

// // Third call should succeed again
// let result4 = check_repository_links(repo_url, None, interval).await;
// assert!(result4.is_ok(), "Third call should succeed after cancellation");
// }

#[tokio::test]
async fn test_scheduled_execution() {
let counter = Arc::new(AtomicUsize::new(0));
Expand Down