hg_git_fast_import/
tools.rs

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
22/// Build or update marks.
23///
24/// Useful if target Git repository was updated.
25pub 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}