1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#[allow(clippy::wildcard_imports)]
use super::*;
impl App {
/// Launch the editor on a temp copy of `filename` and settle the result
/// (tix #1510).
///
/// The issue file's raw bytes are snapshotted **before** the child runs, and
/// the edited temp copy is compared against that snapshot **after** it exits
/// by [`App::after_editor`] — the one place the conflict decision is made.
/// Splitting the round trip this way is what lets the live
/// `Action::OpenEditor` shell-out and the programmatic entry point be the same
/// code path: the only difference between them is who suspends the terminal
/// around the child.
pub fn open_editor_round_trip(&mut self, filename: &str) -> Result<()> {
let name = editor::resolve_editor(self.store.config.editor.as_deref())?;
let original = self
.store
.get(IssueSelector::Filename(filename.to_string()))?;
let disk_path = self.store.issues_dir.join(&original.filename);
let snapshot = std::fs::read(&disk_path).map_err(TixError::Io)?;
let temp = std::env::temp_dir().join(format!(
"tix-edit-{}-{}.md",
std::process::id(),
original.number
));
std::fs::write(&temp, original.to_file_contents()).map_err(TixError::Io)?;
self.pending_edit = Some(PendingEdit {
filename: original.filename.clone(),
temp: temp.clone(),
snapshot,
});
if let Err(e) = editor::run_editor(&name, &temp, &self.store.config) {
// The child failed to launch or exited non-zero, so nothing was
// written — and the copy is **kept**. Deleting it here (as the old
// flow did) is the automatic discard tix #1506 exists to remove; the
// temp copy is the only copy of anything the editor might have
// managed to save before it failed.
self.pending_edit = None;
self.status_msg = Some(format!(
"editor: {e}; your copy is kept at {}",
temp.display()
));
return Ok(());
}
self.after_editor(&original.filename);
Ok(())
}
/// Finish an editor round trip: commit the edit, or refuse to because the file
/// changed underneath it (tix #1510).
///
/// Both entry points arrive here — [`App::open_editor_round_trip`] for the live
/// shell-out, and direct callers that write the file themselves before calling
/// this. A round trip that recorded a pending edit is compared against its
/// pre-edit snapshot and may enter [`Mode::EditConflict`] **writing nothing**;
/// one with no pending edit has no pre-edit bytes to compare against, so it
/// keeps the pre-#1510 behaviour of persisting the file it is asked to read.
pub fn after_editor(&mut self, filename: &str) {
// The lint below reports the project's current rules, and the user may
// have declared a custom attribute in `tix.toml` since this session
// started (tix #1506): adopt the file before judging the edit.
self.reload_config_if_changed();
if let Some(pending) = self
.pending_edit
.take_if(|pending| pending.filename == filename)
{
self.settle_pending_edit(pending);
return;
}
match self
.store
.get(IssueSelector::Filename(filename.to_string()))
{
// No pending edit means this caller wrote the file itself, so there
// is no retained temp copy: the issue file is the working copy.
Ok(issue) => self.commit_edited_issue(filename, issue, "Edited", None),
Err(e) => {
self.status_msg = Some(format!("editor: {e}"));
}
}
}
}