Skip to main content

gitsheets/diff/
mod.rs

1// git-sheets: Diff module - computing differences between snapshots
2// A tool for Excel sufferers who deserve better
3
4use crate::core::GitSheetsError;
5use serde::{Deserialize, Serialize};
6use std::fs;
7use std::path::Path;
8
9/// Helper function to check if a row exists in another vector
10fn row_exists(row: &[String], target: &[Vec<String>]) -> bool {
11    target.iter().any(|target_row| target_row == row)
12}
13
14// Re-export from core module
15pub use crate::core::{Snapshot, TableHashes};
16
17/// Summary of changes between snapshots
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct DiffSummary {
20    /// Number of rows added
21    pub rows_added: usize,
22    /// Number of rows removed
23    pub rows_removed: usize,
24    /// Number of rows modified
25    pub rows_modified: usize,
26    /// Number of columns added
27    pub columns_added: usize,
28    /// Number of columns removed
29    pub columns_removed: usize,
30}
31
32/// Individual change types
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub enum Change {
35    RowAdded {
36        index: usize,
37        data: Vec<String>,
38    },
39    RowRemoved {
40        index: usize,
41        data: Vec<String>,
42    },
43    RowModified {
44        index: usize,
45        old_data: Vec<String>,
46        new_data: Vec<String>,
47    },
48    CellChanged {
49        row: usize,
50        col: usize,
51        old: String,
52        new: String,
53    },
54    ColumnAdded {
55        name: String,
56        index: usize,
57    },
58    ColumnRemoved {
59        name: String,
60        index: usize,
61    },
62}
63
64/// A diff between two snapshots
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct SnapshotDiff {
67    /// ID of the from snapshot
68    pub from_id: String,
69    /// ID of the to snapshot
70    pub to_id: String,
71    /// Summary of changes
72    pub summary: DiffSummary,
73    /// Detailed changes
74    pub changes: Vec<Change>,
75}
76
77impl SnapshotDiff {
78    /// Create a diff between two snapshots
79    pub fn compute(from: &Snapshot, to: &Snapshot) -> Result<Self, GitSheetsError> {
80        let mut changes = Vec::new();
81        let mut summary = DiffSummary {
82            rows_added: 0,
83            rows_removed: 0,
84            rows_modified: 0,
85            columns_added: 0,
86            columns_removed: 0,
87        };
88
89        // Compare headers (columns)
90        let from_headers = &from.table.headers;
91        let to_headers = &to.table.headers;
92
93        // Check for added columns
94        for (idx, header) in to_headers.iter().enumerate() {
95            if !from_headers.contains(header) {
96                changes.push(Change::ColumnAdded {
97                    name: header.clone(),
98                    index: idx,
99                });
100                summary.columns_added += 1;
101            }
102        }
103
104        // Check for removed columns
105        for (idx, header) in from_headers.iter().enumerate() {
106            if !to_headers.contains(header) {
107                changes.push(Change::ColumnRemoved {
108                    name: header.clone(),
109                    index: idx,
110                });
111                summary.columns_removed += 1;
112            }
113        }
114
115        // Compare rows
116        let from_rows = &from.table.rows;
117        let to_rows = &to.table.rows;
118
119        // Check for added rows
120        for (idx, row) in to_rows.iter().enumerate() {
121            if !row_exists(&row, from_rows) {
122                changes.push(Change::RowAdded {
123                    index: idx,
124                    data: row.clone(),
125                });
126                summary.rows_added += 1;
127            }
128        }
129
130        // Check for removed rows
131        for (idx, row) in from_rows.iter().enumerate() {
132            if !row_exists(&row, to_rows) {
133                changes.push(Change::RowRemoved {
134                    index: idx,
135                    data: row.clone(),
136                });
137                summary.rows_removed += 1;
138            }
139        }
140
141        // Check for modified rows
142        let mut row_idx = 0;
143        while row_idx < from_rows.len() && row_idx < to_rows.len() {
144            let from_row = &from_rows[row_idx];
145            let to_row = &to_rows[row_idx];
146
147            if from_row != to_row {
148                changes.push(Change::RowModified {
149                    index: row_idx,
150                    old_data: from_row.clone(),
151                    new_data: to_row.clone(),
152                });
153                summary.rows_modified += 1;
154            }
155
156            row_idx += 1;
157        }
158
159        // Check for cell changes
160        let mut row_idx = 0;
161        while row_idx < from_rows.len() && row_idx < to_rows.len() {
162            let from_row = &from_rows[row_idx];
163            let to_row = &to_rows[row_idx];
164
165            if from_row != to_row {
166                for (col_idx, (from_cell, to_cell)) in
167                    from_row.iter().zip(to_row.iter()).enumerate()
168                {
169                    if from_cell != to_cell {
170                        changes.push(Change::CellChanged {
171                            row: row_idx,
172                            col: col_idx,
173                            old: from_cell.clone(),
174                            new: to_cell.clone(),
175                        });
176                    }
177                }
178            }
179
180            row_idx += 1;
181        }
182
183        Ok(Self {
184            from_id: from.id.clone(),
185            to_id: to.id.clone(),
186            summary,
187            changes,
188        })
189    }
190
191    /// Save diff to disk as TOML
192    pub fn save(&self, path: &Path) -> Result<(), GitSheetsError> {
193        let toml_string = toml::to_string_pretty(self)?;
194        fs::write(path, toml_string)?;
195        Ok(())
196    }
197}