Fork channel

Create a new channel as a copy of main.

Rename channel

Rename main to:

Delete channel

Delete main? This cannot be undone.

graph.rs
use std::collections::{HashMap, HashSet};

use std::fs::File;
use std::io::Write;
use std::path::Path;
use walkdir::WalkDir;

use libpijul::changestore::{ChangeStore, filesystem::FileSystem};
use libpijul::pristine::Hash;
use libpijul::Base32;
use petgraph::graph::{DiGraph, NodeIndex};
use petgraph::dot::{Dot, Config};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DependencyKind {
    Standard,
    Context,
}

/// パッチ単体の情報を保持する構造体
#[derive(Debug, Clone)]
pub struct PatchInfo {
    pub hash: Hash,
    pub message: String,
    pub timestamp: String,
    pub author: String,
}

pub struct PijulGraph {
    pub graph: DiGraph<PatchInfo, DependencyKind>,
    node_indices: HashMap<Hash, NodeIndex>,
}

impl PijulGraph {
    pub fn new() -> Self {
        Self {
            graph: DiGraph::new(),
            node_indices: HashMap::new(),
        }
    }

    /// .pijul/changes からリポジトリの状態をロードしてDAGを構築する
    pub fn load_from_repo<P: AsRef<Path>>(&mut self, repo_path: P) -> Result<(), Box<dyn std::error::Error>> {
        let dot_pijul = repo_path.as_ref().join(".pijul");
        if !dot_pijul.exists() {
            return Err("Not a Pijul repository (missing .pijul directory)".into());
        }

        let store = FileSystem::from_root(repo_path.as_ref(), 0);
        let changes_dir = dot_pijul.join("changes");
        let mut edges_to_add = Vec::new();

        for entry in WalkDir::new(&changes_dir).into_iter().filter_map(|e| e.ok()) {
            let path = entry.path();
            if path.is_file() && !path.file_name().unwrap().to_string_lossy().starts_with('.') {
                let relative = path.strip_prefix(&changes_dir)?;
                let hash_str = relative.to_string_lossy().replace(std::path::MAIN_SEPARATOR, "");
                let hash_pure = hash_str.split('.').next().unwrap_or(&hash_str);

                let current_hash = match Hash::from_base32(hash_pure.as_bytes()) {
                    Some(h) => h,
                    None => continue,
                };

                let change = match store.get_change(&current_hash) {
                    Ok(c) => c,
                    Err(_) => continue,
                };

                let header = change.hashed.header;
                let author = header.authors.first().map(|a| format!("{:?}", a)).unwrap_or_else(|| "Unknown".to_string());

                let info = PatchInfo {
                    hash: current_hash,
                    message: header.message,
                    timestamp: format!("{:?}", header.timestamp),
                    author,
                };

                let node_idx = self.graph.add_node(info);
                self.node_indices.insert(current_hash, node_idx);

                for dep in change.hashed.dependencies {
                    edges_to_add.push((current_hash, dep, DependencyKind::Standard));
                }
                for extra in change.hashed.extra_known {
                    edges_to_add.push((current_hash, extra, DependencyKind::Context));
                }
            }
        }

        for (child, parent, kind) in edges_to_add {
            if let (Some(&child_idx), Some(&parent_idx)) = (self.node_indices.get(&child), self.node_indices.get(&parent)) {
                self.graph.add_edge(child_idx, parent_idx, kind);
            }
        }

        Ok(())
    }

    /// Graphviz形式のファイルをエクスポートする
    pub fn write_dot_file<P: AsRef<Path>>(&self, output_path: P) -> std::io::Result<()> {
        // 表示用に文字列だけのグラフにマップ
        let string_graph = self.graph.map(
            |_, node| {
                let s_hash = node.hash.to_base32();
                format!("[{}] {}", if s_hash.len() > 8 { &s_hash[0..8] } else { &s_hash }, node.message.replace('\n', " "))
            },
            |_, &kind| kind
        );

        let dot_output = format!("{:?}", Dot::with_config(&string_graph, &[Config::EdgeNoLabel]));
        let dot_output = dot_output.replace("label = \"Context\"", "style = \"dashed\", color = \"gray\", label = \"*\"");

        let mut file = File::create(output_path)?;
        file.write_all(dot_output.as_bytes())?;
        Ok(())
    }

    /// ★ 新機能:論理的に破綻のないすべてのパッチの組み合わせ(世界線)を列挙する
    /// 修正:上限付きで世界線を列挙する安全弁を追加
    pub fn enumerate_ideals(&self, limit: usize) -> Vec<HashSet<Hash>> {
        let mut results = Vec::new();
        let mut topo_order = match petgraph::algo::toposort(&self.graph, None) {
            Ok(order) => order,
            Err(_) => return Vec::new(),
        };
        topo_order.reverse();

        let mut current_set = HashSet::new();
        self.backtrack_ideals(0, &topo_order, &mut current_set, &mut results, limit);

        results.sort_by_key(|set| set.len());
        results
    }

    fn backtrack_ideals(
        &self,
        idx: usize,
        topo_order: &[NodeIndex],
        current_set: &mut HashSet<NodeIndex>,
        results: &mut Vec<HashSet<Hash>>,
        limit: usize,
    ) {
        // 上限に達したら探索を打ち切る
        if results.len() >= limit {
            return;
        }

        if idx == topo_order.len() {
            let hash_set = current_set.iter().map(|&n| self.graph[n].hash).collect();
            results.push(hash_set);
            return;
        }

        let node = topo_order[idx];

        // 選択肢①: 含めない
        self.backtrack_ideals(idx + 1, topo_order, current_set, results, limit);

        // 選択肢②: 含める
        let mut parents_satisfied = true;
        for parent in self.graph.neighbors_directed(node, petgraph::Direction::Outgoing) {
            if !current_set.contains(&parent) {
                parents_satisfied = false;
                break;
            }
        }

        if parents_satisfied {
            current_set.insert(node);
            self.backtrack_ideals(idx + 1, topo_order, current_set, results, limit);
            current_set.remove(&node);
        }
    }

    /// ★新機能:特定のパッチが依存する「最小の閉じられた世界」を爆速で計算する (主イデアル)
    pub fn get_minimal_world_for(&self, target_hash: &Hash) -> Option<HashSet<Hash>> {
        let start_node = *self.node_indices.get(target_hash)?;
        let mut closure = HashSet::new();
        let mut dfs = petgraph::visit::Dfs::new(&self.graph, start_node);

        // ターゲットから到達可能な(=依存している)過去のノードをすべて回収
        while let Some(nx) = dfs.next(&self.graph) {
            closure.insert(self.graph[nx].hash);
        }

        Some(closure)
    }
}