git_branch_stash/
snapshot.rs

1/// State of all branches
2#[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    /// Load branch state from a file
12    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    /// Save branch state to a file
20    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    /// Extract branch state from an existing repo
27    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    /// Update repo to match the branch state
49    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    /// Add message metadata
95    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/// State of an individual branch
104#[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}