cupido 0.2.1

Explore your codebase with graph view.
Documentation
use petgraph::dot::Config;
use petgraph::graph::{NodeIndex, UnGraph};
use serde_derive::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::{Display, Error, Formatter};
use std::fs::File;
use std::io::Write;
use std::sync::Arc;

enum NodeType {
    File(Option<FileData>),
    Commit(Option<CommitData>),
    Issue(Option<IssueData>),
}

struct FileData {}

struct CommitData {}

struct IssueData {}

#[derive(Debug)]
enum EdgeType {
    File2Commit,
    File2Issue,
    Commit2Issue,
}

impl Display for EdgeType {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:?}", self)
    }
}

pub struct NodeData {
    _name: Arc<String>,
    _node_type: NodeType,
    node_index: NodeIndex,
}

type NodeMapping = HashMap<Arc<String>, NodeData>;

pub struct RelationGraph {
    file_mapping: NodeMapping,
    commit_mapping: NodeMapping,
    issue_mapping: NodeMapping,
    g: UnGraph<Arc<String>, EdgeType>,
}

impl RelationGraph {
    pub fn new() -> RelationGraph {
        return RelationGraph {
            file_mapping: HashMap::<Arc<String>, NodeData>::new(),
            commit_mapping: HashMap::<Arc<String>, NodeData>::new(),
            issue_mapping: HashMap::<Arc<String>, NodeData>::new(),
            g: UnGraph::<Arc<String>, EdgeType>::new_undirected(),
        };
    }

    pub fn add_commit_node(&mut self, name: &String) {
        if !self.commit_mapping.contains_key(name) {
            let name_rc: Arc<String> = Arc::from(name.to_string());
            let node_index = self.g.add_node(name_rc.clone());
            let node_data = NodeData {
                _name: name_rc.clone(),
                _node_type: NodeType::Commit(None),
                node_index,
            };
            self.commit_mapping.insert(name_rc, node_data);
        }
    }

    pub fn add_file_node(&mut self, name: &String) {
        if !self.file_mapping.contains_key(name) {
            let name_rc: Arc<String> = Arc::from(name.to_string());
            let node_index = self.g.add_node(name_rc.clone());
            let node_data = NodeData {
                _name: name_rc.clone(),
                _node_type: NodeType::File(None),
                node_index,
            };
            self.file_mapping.insert(name_rc, node_data);
        }
    }

    pub fn add_issue_node(&mut self, name: &String) {
        if !self.issue_mapping.contains_key(name) {
            let name_rc: Arc<String> = Arc::from(name.to_string());
            let node_index = self.g.add_node(name_rc.clone());
            let node_data = NodeData {
                _name: name_rc.clone(),
                _node_type: NodeType::Issue(None),
                node_index,
            };
            self.issue_mapping.insert(name_rc, node_data);
        }
    }

    pub fn get_file_node(&self, name: &String) -> Option<&NodeData> {
        self.file_mapping.get(name)
    }

    pub fn get_commit_node(&self, name: &String) -> Option<&NodeData> {
        self.commit_mapping.get(name)
    }

    pub fn get_issue_node(&self, name: &String) -> Option<&NodeData> {
        self.issue_mapping.get(name)
    }

    pub fn add_edge_file2commit(&mut self, file_name: &String, commit_name: &String) {
        if let (Some(file_data), Some(commit_data)) = (
            self.file_mapping.get(file_name),
            self.commit_mapping.get(commit_name),
        ) {
            let file_index = file_data.node_index;
            let commit_index = commit_data.node_index;

            if let Some(..) = self.g.find_edge(file_index, commit_index) {
                return;
            }
            self.g
                .add_edge(file_index, commit_index, EdgeType::File2Commit);
        }
    }

    pub fn add_edge_file2issue(&mut self, file_name: &String, issue: &String) {
        if let (Some(file_data), Some(issue_data)) = (
            self.file_mapping.get(file_name),
            self.issue_mapping.get(issue),
        ) {
            let file_index = file_data.node_index;
            let commit_index = issue_data.node_index;
            if let Some(..) = self.g.find_edge(file_index, commit_index) {
                return;
            }
            self.g
                .add_edge(file_index, commit_index, EdgeType::File2Issue);
        }
    }

    pub fn add_edge_commit2issue(&mut self, commit_name: &String, issue: &String) {
        if let (Some(commit_data), Some(issue_data)) = (
            self.commit_mapping.get(commit_name),
            self.issue_mapping.get(issue),
        ) {
            let commit_index = commit_data.node_index;
            let issue_index = issue_data.node_index;
            if let Some(..) = self.g.find_edge(commit_index, issue_index) {
                return;
            }
            self.g
                .add_edge(commit_index, issue_index, EdgeType::Commit2Issue);
        }
    }

    fn find_related(
        &self,
        entry: &String,
        src: &NodeMapping,
        target: &NodeMapping,
    ) -> Result<Vec<String>, Error> {
        if !src.contains_key(entry) {
            return Err(Error::default());
        }
        let neighbors = self.g.neighbors(src[entry].node_index);
        let related: Vec<String> = neighbors
            .filter(|node_index| {
                let data = self.g[*node_index].to_string();
                if !target.contains_key(&data) {
                    return false;
                }
                return true;
            })
            .map(|node_index| self.g[node_index].to_string())
            .collect();

        Ok(related)
    }

    pub fn file_related_commits(&self, file_name: &String) -> Result<Vec<String>, Error> {
        return self.find_related(file_name, &self.file_mapping, &self.commit_mapping);
    }

    pub fn file_related_issues(&self, file_name: &String) -> Result<Vec<String>, Error> {
        return self.find_related(file_name, &self.file_mapping, &self.issue_mapping);
    }

    pub fn issue_related_files(&self, issue_name: &String) -> Result<Vec<String>, Error> {
        return self.find_related(issue_name, &self.issue_mapping, &self.file_mapping);
    }

    pub fn issue_related_commits(&self, issue_name: &String) -> Result<Vec<String>, Error> {
        return self.find_related(issue_name, &self.issue_mapping, &self.commit_mapping);
    }

    pub fn commit_related_files(&self, commit_name: &String) -> Result<Vec<String>, Error> {
        return self.find_related(commit_name, &self.commit_mapping, &self.file_mapping);
    }

    pub fn commit_related_issues(&self, commit_name: &String) -> Result<Vec<String>, Error> {
        return self.find_related(commit_name, &self.commit_mapping, &self.issue_mapping);
    }

    pub fn export_dot(&self, file_path: &str) {
        // copy a new graph for filters
        let mut graph = RelationGraph::new();
        for (each, _) in &self.file_mapping {
            graph.add_file_node(each)
        }
        for (each, _) in &self.issue_mapping {
            graph.add_issue_node(each);
            for each_file in &self.issue_related_files(each).unwrap() {
                graph.add_edge_file2issue(each_file, each)
            }
        }

        let dot = petgraph::dot::Dot::with_config(&graph.g, &[Config::EdgeNoLabel]);
        if let Ok(mut file) = File::create(file_path) {
            file.write_all(dot.to_string().as_bytes())
                .expect("Failed to write to file");
            println!("DOT representation saved to '{}'", file_path);
        } else {
            eprintln!("Failed to create or write to '{}'", file_path);
        }
    }

    pub fn export_file_issue_mapping(&self) -> HashMap<String, Vec<String>> {
        let mut ret = HashMap::new();
        for (f, _) in &self.file_mapping {
            let fs = f.to_string();
            let issues: Result<Vec<String>, Error> = self.file_related_issues(&fs);

            if let Ok(ok_issues) = issues {
                if !ok_issues.is_empty() {
                    ret.insert(fs.clone(), ok_issues);
                }
            }
        }
        return ret;
    }

    pub fn file_size(&self) -> usize {
        return self.file_mapping.len();
    }

    pub fn commit_size(&self) -> usize {
        return self.commit_mapping.len();
    }

    pub fn issue_size(&self) -> usize {
        return self.issue_mapping.len();
    }

    pub fn size(&self) -> GraphSize {
        return GraphSize {
            file_size: self.file_size(),
            commit_size: self.commit_size(),
            issue_size: self.issue_size(),
        };
    }
}

#[derive(Deserialize, Serialize, Debug)]
pub struct GraphSize {
    file_size: usize,
    commit_size: usize,
    issue_size: usize,
}