1use std::collections::{BTreeMap, HashMap};
2use std::fs::{copy, File};
3use std::io::Write;
4use std::path::Path;
5use std::path::PathBuf;
6use std::time::{SystemTime, UNIX_EPOCH};
7
8use hg_parser::{ChangesetHeader, MercurialRepository};
9
10use crate::error::ErrorKind;
11use crate::git::GitTargetRepository;
12use crate::{read_file, TargetRepositoryError};
13
14use super::{to_str, to_string};
15
16#[derive(Hash, PartialEq, Eq)]
17struct RevisionHeader {
18 user: String,
19 date: usize,
20}
21
22pub fn build_marks<P: AsRef<Path>, S: ::std::hash::BuildHasher>(
26 authors: Option<HashMap<String, String, S>>,
27 hg_repo: P,
28 git_repo: P,
29 offset: Option<usize>,
30 backup: bool,
31) -> Result<(), ErrorKind> {
32 let git_repo = GitTargetRepository::open(git_repo);
33 let mut git_cmd = git_repo.git_cmd(&[
34 "log",
35 "--reflog",
36 "--all",
37 "--reverse",
38 "--format=format:%H%n%at%n%an <%ae>",
39 ]);
40 let git_output = git_cmd.output()?;
41 if !git_output.status.success() {
42 return Err(ErrorKind::Target(TargetRepositoryError::GitFailure(
43 git_output.status,
44 "git log failed".into(),
45 )));
46 }
47
48 let (git_repo_mapping, revlog) = load_git_revlog_lines(&git_output.stdout);
49
50 let hg_repo = MercurialRepository::open(hg_repo)?;
51
52 let marks_file = git_repo.path().join(".git/hg-git-fast-import.marks");
53
54 if backup && marks_file.exists() {
55 let now = SystemTime::now()
56 .duration_since(UNIX_EPOCH)
57 .unwrap()
58 .as_secs();
59 let backup_marks_file = marks_file.with_extension(format!("marks.backup.{}", now));
60 eprintln!("Save backup to {}", backup_marks_file.to_string_lossy());
61 copy(&marks_file, backup_marks_file)?;
62 }
63
64 let mut build_marks = BuildMarks {
65 git_repo,
66 marks_file,
67 hg_repo,
68 git_repo_mapping,
69 revlog,
70 };
71
72 build_marks.process(authors, offset)
73}
74
75struct BuildMarks<'a> {
76 git_repo: GitTargetRepository<'a>,
77 git_repo_mapping: HashMap<RevisionHeader, Vec<String>>,
78 revlog: Vec<String>,
79 marks_file: PathBuf,
80 hg_repo: MercurialRepository,
81}
82
83impl<'a> BuildMarks<'a> {
84 fn process<S: ::std::hash::BuildHasher>(
85 &mut self,
86 authors: Option<HashMap<String, String, S>>,
87 offset: Option<usize>,
88 ) -> Result<(), ErrorKind> {
89 let authors = authors.as_ref();
90 let offset = offset.unwrap_or_default() + 1;
91
92 let mut marks = self.load_marks()?;
93
94 for (index, rev) in self.hg_repo.header_iter().enumerate() {
95 let user = to_string(&rev.user);
96 let user = authors
97 .and_then(|authors| authors.get(&user))
98 .cloned()
99 .unwrap_or(user);
100 let date = rev.time.timestamp_secs() as usize;
101 let revision_header = RevisionHeader { user, date };
102 let revision_mark = index + offset;
103
104 let old_sha1_pos = marks
105 .get(&revision_mark)
106 .and_then(|old_sha1| self.revlog_position(old_sha1));
107 if old_sha1_pos.is_some() {
108 continue;
109 }
110
111 let mapped_sha1_list = self.git_repo_mapping.get_mut(&revision_header);
112 if let Some(sha1s) = mapped_sha1_list {
113 let mut sha1 = None;
114 if sha1s.len() == 1 {
115 sha1 = Some(sha1s.remove(0));
116 } else if !sha1s.is_empty() {
117 eprintln!(
118 "Found multiple ({}) sha1s for mark :{}",
119 sha1s.len(),
120 revision_mark
121 );
122 sha1 = select_from_matching(
123 &self.git_repo,
124 &self.revlog,
125 &rev,
126 sha1s,
127 revision_mark,
128 index,
129 &revision_header,
130 )?;
131 }
132 if let Some(sha1) = sha1 {
133 if let Some(old_sha1) = marks.get(&revision_mark).cloned() {
134 if old_sha1 != sha1 {
135 let old_index = self.revlog_position(&old_sha1);
136 let new_index = self.revlog_position(&sha1);
137 eprintln!(
138 "{}: set {} from {}({:?}) to {}({:?})",
139 index, revision_mark, old_sha1, old_index, sha1, new_index
140 );
141 marks.insert(revision_mark, sha1);
142 }
143 } else {
144 marks.insert(revision_mark, sha1.clone());
145 }
146 }
147 } else if mapped_sha1_list.is_none() {
148 eprintln!(
149 "Cannot find Mercurial revision {} user {} timestamp {}",
150 index, revision_header.user, rev.time,
151 );
152 }
153 }
154
155 eprintln!("Writing updated marks");
156
157 self.save_marks(marks)?;
158
159 eprintln!("Done.");
160
161 Ok(())
162 }
163
164 fn revlog_position(&self, sha1: &str) -> Option<usize> {
165 self.revlog.iter().position(|x| x == sha1)
166 }
167
168 fn load_marks(&self) -> Result<BTreeMap<usize, String>, ErrorKind> {
169 if !self.marks_file.exists() {
170 return Ok(BTreeMap::new());
171 }
172 Ok(read_file(&self.marks_file)?
173 .lines()
174 .filter_map(|x| {
175 let mut tokens = x.split_whitespace();
176 if let (Some(mark), Some(sha1)) = (tokens.next(), tokens.next()) {
177 Some((mark[1..].parse().unwrap(), sha1.into()))
178 } else {
179 None
180 }
181 })
182 .collect())
183 }
184
185 fn save_marks(&self, marks: BTreeMap<usize, String>) -> Result<(), ErrorKind> {
186 let mut f = File::create(&self.marks_file)?;
187
188 for (mark, sha1) in marks {
189 writeln!(f, ":{} {}", mark, sha1)?;
190 }
191
192 Ok(())
193 }
194}
195
196fn select_from_matching(
197 git_repo: &GitTargetRepository,
198 revlog: &[String],
199 rev: &ChangesetHeader,
200 sha1s: &mut Vec<String>,
201 revision_mark: usize,
202 index: usize,
203 revision_header: &RevisionHeader,
204) -> Result<Option<String>, ErrorKind> {
205 let indexes = find_matching_sha1_index(rev, sha1s, git_repo)?;
206
207 if indexes.len() == 1 {
208 let removed = sha1s.remove(indexes[0]);
209 eprintln!("Selected {}", &removed);
210 return Ok(Some(removed));
211 }
212
213 let select = dialoguer::Select::new()
214 .with_prompt(format!(
215 "Select sha1 to set mark :{} (pos: {}) {} {}",
216 revision_mark, index, revision_header.user, revision_header.date
217 ))
218 .items(
219 &indexes
220 .iter()
221 .map(|index| {
222 let sha1 = &sha1s[*index];
223 let new_index = revlog.iter().position(|x| x == sha1);
224 format!("{} ({:?})", sha1, new_index)
225 })
226 .collect::<Vec<_>>(),
227 );
228
229 let index_selected = select.interact_opt()?.map(|index| indexes[index]);
230
231 if let Some(index) = index_selected {
232 let removed = sha1s.remove(index);
233 eprintln!("Selected {}", &removed);
234 return Ok(Some(removed));
235 }
236
237 Ok(None)
238}
239
240fn find_matching_sha1_index(
241 rev: &ChangesetHeader,
242 sha1s: &[String],
243 git_repo: &GitTargetRepository,
244) -> Result<Vec<usize>, ErrorKind> {
245 let hg_commit_message = rev.comment.as_slice();
246 let mut result = vec![];
247
248 for (index, sha1) in sha1s.iter().enumerate() {
249 let mut git_cmd = git_repo.git_cmd(&["show", "-s", "--format=.%B.", sha1]);
250 let git_output = git_cmd.output()?;
251 if !git_output.status.success() {
252 return Err(ErrorKind::Target(TargetRepositoryError::GitFailure(
253 git_output.status,
254 "git show failed".into(),
255 )));
256 }
257
258 let comment = &git_output.stdout;
259
260 let mut from = 0;
261 while comment[from] != b'.' {
262 from += 1;
263 }
264
265 let mut to = comment.len() - 1;
266 while comment[to] != b'.' {
267 to -= 1;
268 }
269
270 let comment = &comment[from + 1..to - 1];
271
272 if hg_commit_message == comment {
273 result.push(index);
274 }
275 }
276
277 Ok(result)
278}
279
280fn load_git_revlog_lines(stdout: &[u8]) -> (HashMap<RevisionHeader, Vec<String>>, Vec<String>) {
281 let mut lines = stdout.split(|&x| x == b'\n');
282 let mut result: HashMap<RevisionHeader, Vec<String>> = HashMap::new();
283 let mut revlog = vec![];
284 while let (Some(sha1), Some(date), Some(user)) = (lines.next(), lines.next(), lines.next()) {
285 let revision_header = RevisionHeader {
286 user: to_string(user),
287 date: to_str(date).parse().unwrap(),
288 };
289 let sha1s = result.entry(revision_header).or_default();
290 let sha1 = to_string(sha1);
291 sha1s.push(sha1.clone());
292 revlog.push(sha1);
293 }
294 (result, revlog)
295}