use crate::github::Issue;
const DEFAULT_TEMPLATE: &str = r#"
Please work on the following GitHub issue:
## Issue #{{number}}: {{title}}
{{body}}
## Instructions
1. Read the issue carefully and understand the requirements
2. Explore the codebase to understand the context
3. Implement the required changes
4. Write tests if applicable
5. Run tests to verify your changes work correctly
6. Commit with message referencing the issue (e.g., "Fix #{{number}}: ...")
IMPORTANT: Continue working until all tasks are complete. If you encounter errors, fix them. After committing, verify everything works and report completion status.
"#;
#[derive(Debug, Clone)]
pub struct TaskTemplate {
template: String,
}
impl TaskTemplate {
pub fn new(template: &str) -> Self {
Self {
template: template.to_string(),
}
}
pub fn default_template() -> Self {
Self::new(DEFAULT_TEMPLATE)
}
pub fn render(&self, issue: &Issue) -> String {
let body = issue.body.clone().unwrap_or_else(|| "(No description provided)".to_string());
let labels = issue.label_names();
self.template
.replace("{{number}}", &issue.number.to_string())
.replace("{{title}}", &issue.title)
.replace("{{body}}", &body)
.replace("{{url}}", &issue.html_url)
.replace("{{labels}}", &labels)
.replace("{{state}}", &issue.state)
}
pub fn template_string(&self) -> &str {
&self.template
}
}
impl Default for TaskTemplate {
fn default() -> Self {
Self::default_template()
}
}
pub fn render_task(issue: &Issue) -> String {
TaskTemplate::default().render(issue)
}
pub fn suggest_commit_message(issue: &Issue) -> String {
format!("Fix #{}: {}", issue.number, issue.title)
}
pub fn suggest_branch_name(issue: &Issue, prefix: &str) -> String {
let sanitized_title = issue
.title
.to_lowercase()
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' {
c
} else if c.is_whitespace() {
'-'
} else {
'_'
}
})
.collect::<String>();
let max_title_len = 40;
let truncated_title = if sanitized_title.len() > max_title_len {
&sanitized_title[..max_title_len]
} else {
&sanitized_title
};
format!("{}/issue-{}-{}", prefix, issue.number, truncated_title)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::github::Label;
fn create_test_issue() -> Issue {
Issue {
number: 42,
title: "Add user authentication".to_string(),
body: Some("We need to add JWT-based authentication.\n\n- Login endpoint\n- Token refresh".to_string()),
labels: vec![
Label {
name: "feature".to_string(),
color: "a2eeef".to_string(),
},
Label {
name: "security".to_string(),
color: "d73a4a".to_string(),
},
],
state: "open".to_string(),
html_url: "https://github.com/test/repo/issues/42".to_string(),
}
}
#[test]
fn test_default_template() {
let template = TaskTemplate::default();
let issue = create_test_issue();
let result = template.render(&issue);
assert!(result.contains("Issue #42: Add user authentication"));
assert!(result.contains("JWT-based authentication"));
assert!(result.contains("Fix #42"));
}
#[test]
fn test_custom_template() {
let template = TaskTemplate::new("Task: {{title}} (Issue #{{number}})\nLabels: {{labels}}");
let issue = create_test_issue();
let result = template.render(&issue);
assert_eq!(
result,
"Task: Add user authentication (Issue #42)\nLabels: feature, security"
);
}
#[test]
fn test_render_with_no_body() {
let template = TaskTemplate::default();
let issue = Issue {
number: 1,
title: "Test".to_string(),
body: None,
labels: vec![],
state: "open".to_string(),
html_url: "https://github.com/test/repo/issues/1".to_string(),
};
let result = template.render(&issue);
assert!(result.contains("(No description provided)"));
}
#[test]
fn test_url_placeholder() {
let template = TaskTemplate::new("See: {{url}}");
let issue = create_test_issue();
let result = template.render(&issue);
assert_eq!(result, "See: https://github.com/test/repo/issues/42");
}
#[test]
fn test_state_placeholder() {
let template = TaskTemplate::new("State: {{state}}");
let issue = create_test_issue();
let result = template.render(&issue);
assert_eq!(result, "State: open");
}
#[test]
fn test_render_task_function() {
let issue = create_test_issue();
let result = render_task(&issue);
assert!(result.contains("#42"));
assert!(result.contains("Add user authentication"));
}
#[test]
fn test_suggest_commit_message() {
let issue = create_test_issue();
let message = suggest_commit_message(&issue);
assert_eq!(message, "Fix #42: Add user authentication");
}
#[test]
fn test_suggest_branch_name() {
let issue = create_test_issue();
let branch = suggest_branch_name(&issue, "cctakt");
assert_eq!(branch, "cctakt/issue-42-add-user-authentication");
}
#[test]
fn test_branch_name_with_special_chars() {
let issue = Issue {
number: 123,
title: "Fix: user@email.com validation (v2)".to_string(),
body: None,
labels: vec![],
state: "open".to_string(),
html_url: "https://github.com/test/repo/issues/123".to_string(),
};
let branch = suggest_branch_name(&issue, "feature");
assert!(branch.starts_with("feature/issue-123-"));
assert!(!branch.contains('@'));
assert!(!branch.contains(':'));
assert!(!branch.contains('('));
}
#[test]
fn test_branch_name_truncation() {
let issue = Issue {
number: 1,
title: "This is a very long title that should be truncated to avoid extremely long branch names".to_string(),
body: None,
labels: vec![],
state: "open".to_string(),
html_url: "https://github.com/test/repo/issues/1".to_string(),
};
let branch = suggest_branch_name(&issue, "fix");
assert!(branch.len() < 80);
}
#[test]
fn test_template_string() {
let template = TaskTemplate::new("Hello {{title}}");
assert_eq!(template.template_string(), "Hello {{title}}");
}
}