1use crate::core::GitSheetsError;
5use serde::{Deserialize, Serialize};
6use std::fs;
7use std::path::Path;
8
9fn row_exists(row: &[String], target: &[Vec<String>]) -> bool {
11 target.iter().any(|target_row| target_row == row)
12}
13
14pub use crate::core::{Snapshot, TableHashes};
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct DiffSummary {
20 pub rows_added: usize,
22 pub rows_removed: usize,
24 pub rows_modified: usize,
26 pub columns_added: usize,
28 pub columns_removed: usize,
30}
31
32#[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#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct SnapshotDiff {
67 pub from_id: String,
69 pub to_id: String,
71 pub summary: DiffSummary,
73 pub changes: Vec<Change>,
75}
76
77impl SnapshotDiff {
78 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 let from_headers = &from.table.headers;
91 let to_headers = &to.table.headers;
92
93 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 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 let from_rows = &from.table.rows;
117 let to_rows = &to.table.rows;
118
119 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 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 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 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 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}