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(¤t_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)
}
}