1use crate::core::GitRepo;
6use crate::error::{Result, ToriiError};
7
8pub struct CommitSignature {
10 pub oid: String,
11 pub armor: String,
12 pub payload: Vec<u8>,
13}
14
15pub struct SignedRewrite {
17 pub old: String,
18 pub new: String,
19}
20
21pub struct SignOutcome {
23 pub rewritten: Vec<SignedRewrite>,
24 pub branches_moved: usize,
25}
26
27impl GitRepo {
28 pub fn extract_commit_signature(&self, target: &str) -> Result<CommitSignature> {
31 let r = &self.repo;
32 let oid = r
33 .revparse_single(target)
34 .map_err(|e| ToriiError::Usage(format!("`{}`: {}", target, e)))?
35 .id();
36
37 let (sig_buf, payload_buf) = r.extract_signature(&oid, None).map_err(|_| {
38 ToriiError::RepoState(format!(
39 "commit {} has no GPG signature attached. Use `torii sign {}` to add one.",
40 &oid.to_string()[..8],
41 target
42 ))
43 })?;
44 let armor = std::str::from_utf8(&sig_buf)
45 .map_err(|e| ToriiError::RepoState(format!("signature is not valid UTF-8: {}", e)))?
46 .to_string();
47
48 Ok(CommitSignature {
49 oid: oid.to_string(),
50 armor,
51 payload: (&*payload_buf).to_vec(),
52 })
53 }
54
55 pub fn resolve_commit_range(&self, target: &str) -> Result<Vec<String>> {
59 Ok(self
60 .resolve_range_oids(target)?
61 .iter()
62 .map(|o| o.to_string())
63 .collect())
64 }
65
66 fn resolve_range_oids(&self, target: &str) -> Result<Vec<git2::Oid>> {
67 let r = &self.repo;
68 if let Some((from, to)) = target.split_once("..") {
69 let from_oid = r.revparse_single(from)?.id();
70 let to_oid = r.revparse_single(to)?.id();
71 let mut walk = r.revwalk()?;
72 walk.push(to_oid)?;
73 walk.hide(from_oid)?;
74 Ok(walk.flatten().collect())
75 } else {
76 Ok(vec![r.revparse_single(target)?.id()])
77 }
78 }
79
80 pub fn preview_signatures(
83 &self,
84 target: &str,
85 key: &str,
86 gpg_program: Option<&str>,
87 ) -> Result<Vec<(String, String)>> {
88 let r = &self.repo;
89 let mut out = Vec::new();
90 for oid in self.resolve_range_oids(target)? {
91 let commit = r.find_commit(oid)?;
92 let buffer = r.commit_create_buffer(
93 &commit.author(),
94 &commit.committer(),
95 commit.message().unwrap_or(""),
96 &commit.tree()?,
97 &commit
98 .parents()
99 .collect::<Vec<_>>()
100 .iter()
101 .collect::<Vec<_>>(),
102 )?;
103 let armor = crate::gpg::sign_blob(&buffer, key, gpg_program)?;
104 out.push((oid.to_string(), armor));
105 }
106 Ok(out)
107 }
108
109 pub fn sign_range(
114 &self,
115 target: &str,
116 key: &str,
117 gpg_program: Option<&str>,
118 ) -> Result<SignOutcome> {
119 let r = &self.repo;
120
121 let mut opts = git2::StatusOptions::new();
122 opts.include_untracked(false);
123 if !r.statuses(Some(&mut opts))?.is_empty() {
124 return Err(ToriiError::RepoState(
125 "working tree is dirty — commit or stash first. (`torii sign` rewrites \
126 history; running with uncommitted changes makes the resulting state \
127 hard to reason about.)"
128 .to_string(),
129 ));
130 }
131
132 let oids = self.resolve_range_oids(target)?;
133
134 let mut ordered = oids.clone();
137 ordered.reverse();
138 let mut remap: std::collections::HashMap<git2::Oid, git2::Oid> =
139 std::collections::HashMap::new();
140 let mut rewritten = Vec::new();
141
142 for oid in &ordered {
143 let commit = r.find_commit(*oid)?;
144 let parents: Vec<git2::Commit> = commit
145 .parents()
146 .map(|p| {
147 let real = remap.get(&p.id()).copied().unwrap_or(p.id());
148 r.find_commit(real).map_err(ToriiError::Git)
149 })
150 .collect::<Result<Vec<_>>>()?;
151 let parent_refs: Vec<&git2::Commit> = parents.iter().collect();
152 let tree = commit.tree()?;
153 let buffer = r.commit_create_buffer(
154 &commit.author(),
155 &commit.committer(),
156 commit.message().unwrap_or(""),
157 &tree,
158 &parent_refs,
159 )?;
160 let buffer_str = std::str::from_utf8(&buffer)
161 .map_err(|e| ToriiError::RepoState(format!("commit buffer is not UTF-8: {}", e)))?;
162 let armor = crate::gpg::sign_blob(&buffer, key, gpg_program)?;
163 let new_oid = r.commit_signed(buffer_str, &armor, Some("gpgsig"))?;
164 remap.insert(*oid, new_oid);
165 rewritten.push(SignedRewrite {
166 old: oid.to_string(),
167 new: new_oid.to_string(),
168 });
169 }
170
171 let mut moved = 0usize;
174 for b in r.branches(Some(git2::BranchType::Local))?.flatten() {
175 let (br, _) = b;
176 let tip = br.get().target();
177 if let (Some(t), Some(name)) = (tip, br.name().ok().flatten()) {
178 if let Some(new_oid) = remap.get(&t) {
179 r.reference(
180 &format!("refs/heads/{}", name),
181 *new_oid,
182 true,
183 "torii sign — re-sign history",
184 )?;
185 moved += 1;
186 }
187 }
188 }
189
190 Ok(SignOutcome {
191 rewritten,
192 branches_moved: moved,
193 })
194 }
195}