git_branch_stash/
snapshot.rs1#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
3pub struct Snapshot {
4 pub branches: Vec<Branch>,
5 #[serde(default)]
6 #[serde(skip_serializing_if = "std::collections::BTreeMap::is_empty")]
7 pub metadata: std::collections::BTreeMap<String, serde_json::Value>,
8}
9
10impl Snapshot {
11 pub fn load(path: &std::path::Path) -> Result<Self, std::io::Error> {
13 let file = std::fs::File::open(path)?;
14 let reader = std::io::BufReader::new(file);
15 let b = serde_json::from_reader(reader)?;
16 Ok(b)
17 }
18
19 pub fn save(&self, path: &std::path::Path) -> Result<(), std::io::Error> {
21 let s = serde_json::to_string_pretty(self)?;
22 std::fs::write(path, s)?;
23 Ok(())
24 }
25
26 pub fn from_repo(repo: &crate::git::GitRepo) -> Result<Self, git2::Error> {
28 let mut branches: Vec<_> = repo
29 .local_branches()
30 .map(|b| {
31 let commit = repo.find_commit(b.id).unwrap();
32 Branch {
33 name: b.name,
34 id: b.id,
35 metadata: maplit::btreemap! {
36 "summary".to_owned() => serde_json::Value::String(
37 String::from_utf8_lossy(commit.summary.as_slice()).into_owned()
38 ),
39 },
40 }
41 })
42 .collect();
43 branches.sort_unstable();
44 let metadata = Default::default();
45 Ok(Self { branches, metadata })
46 }
47
48 pub fn apply(&self, repo: &mut crate::git::GitRepo) -> Result<(), git2::Error> {
50 let head_branch = repo.head_branch();
51 let head_branch_name = head_branch.as_ref().map(|b| b.name.as_str());
52
53 let mut planned_changes = Vec::new();
54 for branch in self.branches.iter() {
55 let existing = repo.find_local_branch(&branch.name);
56 if existing.as_ref().map(|b| b.id) == Some(branch.id) {
57 log::trace!("No change for {}", branch.name);
58 } else {
59 let existing_id = existing.map(|b| b.id).unwrap_or_else(git2::Oid::zero);
60 let new_id = branch.id;
61 planned_changes.push((existing_id, new_id, branch.name.as_str()));
62 }
63 }
64
65 let transaction_repo = git2::Repository::open(repo.raw().path())?;
66 let hooks = git2_ext::hooks::Hooks::with_repo(&transaction_repo)?;
67 let transaction = hooks
68 .run_reference_transaction(&transaction_repo, &planned_changes)
69 .map_err(|err| {
70 git2::Error::new(
71 git2::ErrorCode::GenericError,
72 git2::ErrorClass::Callback,
73 err.to_string(),
74 )
75 })?;
76
77 for (_old_id, new_id, name) in &planned_changes {
78 if head_branch_name == Some(name) {
79 log::debug!("Restoring {name} (HEAD)");
80 repo.detach()?;
81 repo.branch(name, *new_id)?;
82 repo.switch(name)?;
83 } else {
84 log::debug!("Restoring {name}");
85 repo.branch(name, *new_id)?;
86 }
87 }
88
89 transaction.committed();
90
91 Ok(())
92 }
93
94 pub fn insert_message(&mut self, message: &str) {
96 self.metadata.insert(
97 "message".to_owned(),
98 serde_json::Value::String(message.to_owned()),
99 );
100 }
101}
102
103#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
105pub struct Branch {
106 pub name: String,
107 #[serde(serialize_with = "serialize_oid")]
108 #[serde(deserialize_with = "deserialize_oid")]
109 pub id: git2::Oid,
110 #[serde(default)]
111 #[serde(skip_serializing_if = "std::collections::BTreeMap::is_empty")]
112 pub metadata: std::collections::BTreeMap<String, serde_json::Value>,
113}
114
115fn serialize_oid<S>(id: &git2::Oid, serializer: S) -> Result<S::Ok, S::Error>
116where
117 S: serde::Serializer,
118{
119 let id = id.to_string();
120 serializer.serialize_str(&id)
121}
122
123fn deserialize_oid<'de, D>(deserializer: D) -> Result<git2::Oid, D::Error>
124where
125 D: serde::Deserializer<'de>,
126{
127 use serde::Deserialize;
128 let s = String::deserialize(deserializer)?;
129 git2::Oid::from_str(&s).map_err(serde::de::Error::custom)
130}
131
132impl PartialOrd for Branch {
133 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
134 Some((&self.name, self.id).cmp(&(&other.name, other.id)))
135 }
136}
137
138impl Ord for Branch {
139 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
140 (&self.name, self.id).cmp(&(&other.name, other.id))
141 }
142}