-
Notifications
You must be signed in to change notification settings - Fork 0
Implement version control and change log for problem files #47
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
Changes from 5 commits
29f1de7
b4eaafc
fa02586
b83b919
5b0f3cb
9f616cb
487c95b
2cc072c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,378 @@ | ||
| #![allow(dead_code)] | ||
|
|
||
| use anyhow::{Context, Result}; | ||
| use git2::{DiffOptions, IndexAddOption, Repository, StatusOptions, Time}; | ||
| use std::path::PathBuf; | ||
| use tokio::fs; | ||
|
|
||
| const UPLOAD_DIR: &str = "uploads"; | ||
|
|
||
| #[derive(Debug)] | ||
| struct GitManager { | ||
| problem_id: u32, | ||
| } | ||
|
|
||
| impl GitManager { | ||
| fn new(problem_id: u32) -> Self { | ||
| Self { problem_id } | ||
| } | ||
|
|
||
| fn git_init(&self) -> Result<()> { | ||
| let path = PathBuf::from(UPLOAD_DIR).join(self.problem_id.to_string()); | ||
| Repository::init(&path) | ||
| .map(|_| ()) | ||
| .with_context(|| format!("Failed to init git repo at {:?}", path)) | ||
| } | ||
|
|
||
| async fn create_problem(&self) -> Result<()> { | ||
| self.git_init()?; | ||
| self.create_default_directories().await?; | ||
| self.git_add_all()?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| fn git_add_all(&self) -> Result<()> { | ||
| let repo = self.get_repository()?; | ||
| let mut idx = repo.index()?; | ||
| idx.add_all(["."].iter(), IndexAddOption::DEFAULT, None)?; | ||
| idx.write()?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| fn git_commit(&self, message: String) -> Result<String> { | ||
| self.git_add_all()?; | ||
| let repo = self.get_repository()?; | ||
| let mut idx = repo.index()?; | ||
| let tree_id = idx.write_tree()?; | ||
| let tree = repo.find_tree(tree_id)?; | ||
| let sig = repo.signature()?; | ||
utilForever marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| let parent_commits = match repo.head() { | ||
| Ok(head_ref) => { | ||
| let head = head_ref | ||
| .target() | ||
| .ok_or_else(|| anyhow::anyhow!("HEAD does not point to a valid commit"))?; | ||
| vec![repo.find_commit(head)?] | ||
| } | ||
| Err(_) => Vec::new(), | ||
| }; | ||
|
|
||
| let parents: Vec<&git2::Commit> = parent_commits.iter().collect(); | ||
| let commit_oid = repo.commit(Some("HEAD"), &sig, &sig, &message, &tree, &parents)?; | ||
| Ok(commit_oid.to_string()) | ||
| } | ||
|
|
||
| fn git_status(&self) -> Result<Vec<FileInfo>> { | ||
| let repo = self.get_repository()?; | ||
| let mut status_opts = StatusOptions::new(); | ||
| status_opts | ||
| .include_untracked(true) | ||
| .recurse_untracked_dirs(true); | ||
| let statuses = repo.statuses(Some(&mut status_opts))?; | ||
| let mut file_infos = Vec::new(); | ||
| for entry in statuses.iter() { | ||
| let status = Self::status_to_string(entry.status()); | ||
| let path = entry.path().unwrap_or("unknown").to_string(); | ||
| file_infos.push(FileInfo { status, path }); | ||
| } | ||
| Ok(file_infos) | ||
| } | ||
|
|
||
| fn git_log(&self) -> Result<Vec<ChangedLog>> { | ||
| let repo = self.get_repository()?; | ||
| let mut revwalk = repo.revwalk()?; | ||
| revwalk.push_head()?; | ||
| let mut changed_logs = Vec::new(); | ||
| for commit_id in revwalk { | ||
| let commit = &repo.find_commit(commit_id?)?; | ||
| let user = commit.author().name().unwrap_or("unknown").to_string(); | ||
| let time = commit.time(); | ||
| let message = commit.message().unwrap_or("").to_string(); | ||
| let tree = commit.tree()?; | ||
| let parent = if commit.parent_count() > 0 { | ||
| Some(commit.parent(0)?.tree()?) | ||
| } else { | ||
| None | ||
| }; | ||
| let mut diff_opts = DiffOptions::new(); | ||
| diff_opts.include_untracked(false); | ||
| diff_opts.include_ignored(false); | ||
| let diff = | ||
| repo.diff_tree_to_tree(parent.as_ref(), Some(&tree), Some(&mut diff_opts))?; | ||
| let mut paths = Vec::new(); | ||
| for delta in diff.deltas() { | ||
| let status = Self::delta_status_to_string(delta.status()); | ||
| let path = delta | ||
| .new_file() | ||
| .path() | ||
| .and_then(|p| p.to_str()) | ||
| .unwrap_or("unknown") | ||
| .to_string(); | ||
| paths.push(FileInfo { status, path }); | ||
| } | ||
| changed_logs.push(ChangedLog { | ||
| user, | ||
| time, | ||
| message, | ||
| paths, | ||
| }); | ||
| } | ||
| Ok(changed_logs) | ||
| } | ||
|
|
||
| async fn create_default_directories(&self) -> Result<()> { | ||
| let base_path = PathBuf::from(UPLOAD_DIR).join(self.problem_id.to_string()); | ||
| let directories = ["solutions", "tests", "statements"]; | ||
| for dir in directories { | ||
reddevilmidzy marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| let path = base_path.join(dir); | ||
| fs::create_dir_all(path) | ||
| .await | ||
| .with_context(|| format!("Failed to create directory: {}", dir))?; | ||
| } | ||
| Ok(()) | ||
| } | ||
|
|
||
| fn delta_status_to_string(status: git2::Delta) -> String { | ||
| match status { | ||
| git2::Delta::Added => "ADDED".to_string(), | ||
| git2::Delta::Modified => "MODIFIED".to_string(), | ||
| git2::Delta::Deleted => "DELETED".to_string(), | ||
| git2::Delta::Renamed => "RENAMED".to_string(), | ||
| git2::Delta::Typechange => "TYPECHANGE".to_string(), | ||
| _ => "OTHER".to_string(), | ||
| } | ||
| } | ||
|
|
||
| fn status_to_string(status: git2::Status) -> String { | ||
| match status { | ||
| // Not yet added to index | ||
| git2::Status::WT_NEW => "ADDED".to_string(), | ||
| git2::Status::WT_MODIFIED => "MODIFIED".to_string(), | ||
| git2::Status::WT_DELETED => "DELETED".to_string(), | ||
| git2::Status::WT_RENAMED => "RENAMED".to_string(), | ||
| git2::Status::WT_TYPECHANGE => "TYPECHANGE".to_string(), | ||
|
|
||
| // Staged state | ||
| git2::Status::INDEX_NEW => "ADDED".to_string(), | ||
| git2::Status::INDEX_MODIFIED => "MODIFIED".to_string(), | ||
| git2::Status::INDEX_DELETED => "DELETED".to_string(), | ||
| git2::Status::INDEX_RENAMED => "RENAMED".to_string(), | ||
| git2::Status::INDEX_TYPECHANGE => "TYPECHANGE".to_string(), | ||
|
|
||
| _ => "OTHER".to_string(), | ||
| } | ||
| } | ||
|
|
||
| fn get_repository(&self) -> Result<Repository> { | ||
| let path = PathBuf::from(UPLOAD_DIR).join(self.problem_id.to_string()); | ||
| if let Ok(repo) = Repository::open(&path) { | ||
| let mut config = repo.config()?; | ||
| config.set_str("user.name", "admin")?; | ||
| config.set_str("user.email", "[email protected]")?; | ||
reddevilmidzy marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
reddevilmidzy marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Ok(repo) | ||
| } else { | ||
utilForever marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Err(anyhow::anyhow!( | ||
| "Failed to open git repository at {:?}", | ||
| path | ||
| )) | ||
| } | ||
reddevilmidzy marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
| #[derive(Debug, PartialEq, Eq)] | ||
| struct ChangedLog { | ||
| user: String, | ||
| time: Time, | ||
| message: String, | ||
| paths: Vec<FileInfo>, | ||
| } | ||
|
|
||
| #[derive(Debug, PartialEq, Eq)] | ||
| struct FileInfo { | ||
| status: String, | ||
| path: String, | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use rstest::rstest; | ||
| use std::path::Path; | ||
| use tokio::fs; | ||
|
|
||
| #[rstest] | ||
| #[case(git2::Delta::Added, "ADDED")] | ||
| #[case(git2::Delta::Modified, "MODIFIED")] | ||
| #[case(git2::Delta::Deleted, "DELETED")] | ||
| #[case(git2::Delta::Renamed, "RENAMED")] | ||
| #[case(git2::Delta::Typechange, "TYPECHANGE")] | ||
| fn can_parse_delta_status_to_string(#[case] status: git2::Delta, #[case] expected: String) { | ||
| assert_eq!(GitManager::delta_status_to_string(status), expected); | ||
| } | ||
|
|
||
| #[rstest] | ||
| #[case(git2::Status::WT_NEW, "ADDED")] | ||
| #[case(git2::Status::WT_MODIFIED, "MODIFIED")] | ||
| #[case(git2::Status::WT_DELETED, "DELETED")] | ||
| #[case(git2::Status::WT_RENAMED, "RENAMED")] | ||
| #[case(git2::Status::WT_TYPECHANGE, "TYPECHANGE")] | ||
| #[case(git2::Status::INDEX_NEW, "ADDED")] | ||
| #[case(git2::Status::INDEX_MODIFIED, "MODIFIED")] | ||
| #[case(git2::Status::INDEX_DELETED, "DELETED")] | ||
| #[case(git2::Status::INDEX_RENAMED, "RENAMED")] | ||
| #[case(git2::Status::INDEX_TYPECHANGE, "TYPECHANGE")] | ||
| fn can_parse_status_to_string(#[case] status: git2::Status, #[case] expected: String) { | ||
| assert_eq!(GitManager::status_to_string(status), expected); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn can_init_git_repository() -> Result<(), std::io::Error> { | ||
| let problem_id = 10; | ||
| let git_manager = GitManager::new(problem_id); | ||
| assert!(git_manager.git_init().is_ok()); | ||
| assert!(Path::new(format!("{UPLOAD_DIR}/{problem_id}").as_str()).exists()); | ||
| assert!(Path::new(format!("{UPLOAD_DIR}/{problem_id}/.git").as_str()).exists()); | ||
|
|
||
| fs::remove_dir_all(format!("{UPLOAD_DIR}/{problem_id}")).await?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn can_create_default_file() -> Result<(), std::io::Error> { | ||
| let problem_id = 12; | ||
| let git_manager = GitManager::new(problem_id); | ||
|
Comment on lines
+254
to
+256
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 현재 테스트 방식에는 이런 문제점이 있습니다.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 문제점은 다음 PR에서 개선하나요?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵! 환경 분리까지 진행하면 변경사항이 커질 것 같아 다음 PR에서 작업하려 합니다! |
||
| assert!(git_manager.create_default_directories().await.is_ok()); | ||
| assert!(Path::new(format!("{UPLOAD_DIR}/{problem_id}/solutions").as_str()).exists()); | ||
| assert!(Path::new(format!("{UPLOAD_DIR}/{problem_id}/tests").as_str()).exists()); | ||
| assert!(Path::new(format!("{UPLOAD_DIR}/{problem_id}/statements").as_str()).exists()); | ||
|
|
||
| fs::remove_dir_all(format!("{UPLOAD_DIR}/{problem_id}")).await?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn can_create_problem() -> Result<(), std::io::Error> { | ||
| let problem_id = 13; | ||
| let git_manager = GitManager::new(problem_id); | ||
| assert!(git_manager.create_problem().await.is_ok()); | ||
| assert!(Path::new(format!("{UPLOAD_DIR}/{problem_id}").as_str()).exists()); | ||
| assert!(Path::new(format!("{UPLOAD_DIR}/{problem_id}/.git").as_str()).exists()); | ||
| assert!(Path::new(format!("{UPLOAD_DIR}/{problem_id}/solutions").as_str()).exists()); | ||
| assert!(Path::new(format!("{UPLOAD_DIR}/{problem_id}/tests").as_str()).exists()); | ||
| assert!(Path::new(format!("{UPLOAD_DIR}/{problem_id}/statements").as_str()).exists()); | ||
|
|
||
| fs::remove_dir_all(format!("{UPLOAD_DIR}/{problem_id}")).await?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn can_get_git_status() -> Result<(), tokio::io::Error> { | ||
| let problem_id = 14; | ||
| let git_manager = GitManager::new(problem_id); | ||
|
|
||
| git_manager.git_init().unwrap(); | ||
|
|
||
| fs::create_dir_all(format!("{UPLOAD_DIR}/{problem_id}/tests")).await?; | ||
| fs::write(format!("{UPLOAD_DIR}/{problem_id}/tests/1.in"), "1 2").await?; | ||
| fs::write(format!("{UPLOAD_DIR}/{problem_id}/tests/1.out"), "3").await?; | ||
|
|
||
| let file_infos = git_manager.git_status().unwrap(); | ||
| let expected = vec![ | ||
| FileInfo { | ||
| status: "ADDED".to_string(), | ||
| path: "tests/1.in".to_string(), | ||
| }, | ||
| FileInfo { | ||
| status: "ADDED".to_string(), | ||
| path: "tests/1.out".to_string(), | ||
| }, | ||
| ]; | ||
| assert_eq!(file_infos, expected); | ||
|
|
||
| fs::remove_dir_all(format!("{UPLOAD_DIR}/{problem_id}")).await?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn can_git_add() -> Result<(), tokio::io::Error> { | ||
| let problem_id = 15; | ||
| let git_manager = GitManager::new(problem_id); | ||
|
|
||
| git_manager.git_init().unwrap(); | ||
|
|
||
| let repo = git_manager.get_repository().unwrap(); | ||
| fs::create_dir_all(format!("{UPLOAD_DIR}/{problem_id}/tests")).await?; | ||
| fs::write(format!("{UPLOAD_DIR}/{problem_id}/tests/1.in"), "1 2").await?; | ||
| fs::write(format!("{UPLOAD_DIR}/{problem_id}/tests/1.out"), "3").await?; | ||
|
|
||
| assert!(git_manager.git_add_all().is_ok()); | ||
|
|
||
| let statuses = repo.statuses(None).unwrap(); | ||
|
|
||
| // 워킹 디렉토리에 존재하지 않아야 한다. | ||
| assert!(!statuses.iter().any(|e| e.status().is_wt_new())); | ||
| assert!(!statuses.iter().any(|e| e.status().is_wt_modified())); | ||
| assert!(!statuses.iter().any(|e| e.status().is_wt_deleted())); | ||
|
|
||
| // 스테이징 영역에 올라와야 한다. | ||
| assert!(statuses.iter().all(|e| e.status().is_index_new())); | ||
|
|
||
| fs::remove_dir_all(format!("{UPLOAD_DIR}/{problem_id}")).await?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn can_commit() -> Result<(), tokio::io::Error> { | ||
| let problem_id = 16; | ||
| let git_manager = GitManager::new(problem_id); | ||
| git_manager.git_init().unwrap(); | ||
|
|
||
| fs::create_dir_all(format!("{UPLOAD_DIR}/{problem_id}/tests")).await?; | ||
| fs::write(format!("{UPLOAD_DIR}/{problem_id}/tests/1.in"), "1 2").await?; | ||
| fs::write(format!("{UPLOAD_DIR}/{problem_id}/tests/1.out"), "3").await?; | ||
|
|
||
| let commit_message = "add test 1"; | ||
|
|
||
| assert!(git_manager.git_commit(commit_message.to_string()).is_ok()); | ||
|
|
||
| let repo = git_manager.get_repository().unwrap(); | ||
| let head = repo.head().unwrap(); | ||
| let commit = head.peel_to_commit().unwrap(); | ||
|
|
||
| assert_eq!(commit.message(), Some(commit_message)); | ||
|
|
||
| fs::remove_dir_all(format!("{UPLOAD_DIR}/{problem_id}")).await?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn can_get_log() -> Result<(), tokio::io::Error> { | ||
| let problem_id = 17; | ||
| let git_manager = GitManager::new(problem_id); | ||
| git_manager.git_init().unwrap(); | ||
| git_manager.create_default_directories().await.unwrap(); | ||
|
|
||
| fs::write(format!("{UPLOAD_DIR}/{problem_id}/tests/1.in"), "1 2").await?; | ||
| fs::write(format!("{UPLOAD_DIR}/{problem_id}/tests/1.out"), "3").await?; | ||
|
|
||
| git_manager | ||
| .git_commit("create default file".to_string()) | ||
reddevilmidzy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| .unwrap(); | ||
|
|
||
| let log = git_manager.git_log().unwrap(); | ||
|
|
||
| let expected_path = vec![ | ||
| FileInfo { | ||
| status: "ADDED".to_string(), | ||
| path: "tests/1.in".to_string(), | ||
| }, | ||
| FileInfo { | ||
| status: "ADDED".to_string(), | ||
| path: "tests/1.out".to_string(), | ||
| }, | ||
| ]; | ||
| assert_eq!(log[0].paths, expected_path); | ||
|
|
||
| fs::remove_dir_all(format!("{UPLOAD_DIR}/{problem_id}")).await?; | ||
| Ok(()) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| mod git; | ||
| mod handlers; | ||
| mod models; | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.