use crate::model::{ConflictingIssue, FileConflict, Status};
use super::SqliteRepository;
impl SqliteRepository {
pub(crate) fn list_file_conflicts_impl(
&self,
issue_id: i64,
) -> anyhow::Result<Vec<FileConflict>> {
let sql = "
SELECT
f1.path AS file,
i.id AS conflict_id,
i.title AS conflict_title
FROM issue_files f1
JOIN issue_files f2
ON f2.path = f1.path
AND f2.issue_id != f1.issue_id
JOIN issues i
ON i.id = f2.issue_id
AND i.status = ?
WHERE f1.issue_id = ?
ORDER BY f1.path, i.id
";
let mut stmt = self.conn.prepare(sql)?;
let rows = stmt.query_map(
rusqlite::params![Status::InProgress.label(), issue_id],
|r| {
Ok((
r.get::<_, String>(0)?, r.get::<_, i64>(1)?, r.get::<_, String>(2)?, ))
},
)?;
let mut result: Vec<FileConflict> = Vec::new();
for row in rows {
let (file, id, title) = row?;
let issue = ConflictingIssue {
id,
title,
status: Status::InProgress,
};
if let Some(last) = result.last_mut()
&& last.file == file
{
last.conflicts_with.push(issue);
continue;
}
result.push(FileConflict {
file,
conflicts_with: vec![issue],
});
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use crate::db::{CreateIssueInput, Repository, SqliteRepository};
use crate::model::{Kind, Priority, Status};
fn make_issue(repo: &SqliteRepository, title: &str, status: Status) -> i64 {
repo.create_issue(&CreateIssueInput {
parent_id: None,
title: title.to_string(),
description: String::new(),
status,
priority: Priority::Medium,
kind: Kind::Task,
assignee: None,
labels: vec![],
files: vec![],
actor: None,
})
.unwrap()
.id
}
#[test]
fn no_file_attachments_returns_empty() {
let repo = SqliteRepository::open_in_memory().unwrap();
let id = make_issue(&repo, "A", Status::InProgress);
let conflicts = repo.list_file_conflicts(id).unwrap();
assert!(conflicts.is_empty());
}
#[test]
fn attachments_but_no_other_in_progress_returns_empty() {
let repo = SqliteRepository::open_in_memory().unwrap();
let id = make_issue(&repo, "A", Status::InProgress);
repo.add_file(id, "src/lib.rs").unwrap();
let other = make_issue(&repo, "B", Status::Todo);
repo.add_file(other, "src/lib.rs").unwrap();
let conflicts = repo.list_file_conflicts(id).unwrap();
assert!(conflicts.is_empty());
}
#[test]
fn one_file_shared_with_one_in_progress_issue() {
let repo = SqliteRepository::open_in_memory().unwrap();
let id = make_issue(&repo, "A", Status::InProgress);
repo.add_file(id, "src/lib.rs").unwrap();
let other = make_issue(&repo, "B", Status::InProgress);
repo.add_file(other, "src/lib.rs").unwrap();
let conflicts = repo.list_file_conflicts(id).unwrap();
assert_eq!(conflicts.len(), 1);
assert_eq!(conflicts[0].file, "src/lib.rs");
assert_eq!(conflicts[0].conflicts_with.len(), 1);
assert_eq!(conflicts[0].conflicts_with[0].id, other);
}
#[test]
fn two_files_shared_with_two_different_in_progress_issues() {
let repo = SqliteRepository::open_in_memory().unwrap();
let id = make_issue(&repo, "A", Status::InProgress);
repo.add_file(id, "src/foo.rs").unwrap();
repo.add_file(id, "src/bar.rs").unwrap();
let other1 = make_issue(&repo, "B", Status::InProgress);
repo.add_file(other1, "src/foo.rs").unwrap();
let other2 = make_issue(&repo, "C", Status::InProgress);
repo.add_file(other2, "src/bar.rs").unwrap();
let conflicts = repo.list_file_conflicts(id).unwrap();
assert_eq!(conflicts.len(), 2);
assert_eq!(conflicts[0].file, "src/bar.rs");
assert_eq!(conflicts[0].conflicts_with[0].id, other2);
assert_eq!(conflicts[1].file, "src/foo.rs");
assert_eq!(conflicts[1].conflicts_with[0].id, other1);
}
#[test]
fn conflicts_with_done_or_todo_not_returned() {
let repo = SqliteRepository::open_in_memory().unwrap();
let id = make_issue(&repo, "A", Status::InProgress);
repo.add_file(id, "src/lib.rs").unwrap();
for status in [Status::Done, Status::Todo, Status::Backlog, Status::Review] {
let other = make_issue(&repo, &format!("Other {:?}", status), status);
repo.add_file(other, "src/lib.rs").unwrap();
}
let conflicts = repo.list_file_conflicts(id).unwrap();
assert!(
conflicts.is_empty(),
"expected no conflicts, got: {:?}",
conflicts
);
}
}