hg_git_fast_import/
multi.rs

1use std::{
2    collections::{HashMap, HashSet},
3    path::{Path, PathBuf},
4    time::Instant,
5};
6
7use indicatif::{HumanDuration, ProgressBar, ProgressStyle};
8use tracing::{debug, info};
9
10use super::{config, env, MercurialRepo, RepositorySavedState, TargetRepository};
11use crate::error::ErrorKind;
12use crate::git::GitTargetRepository;
13
14fn construct_path<P: AsRef<Path>>(config_path: &Option<P>, target: P) -> PathBuf {
15    let target = target.as_ref();
16    if target.is_absolute() {
17        target.into()
18    } else {
19        config_path
20            .as_ref()
21            .map(|c| c.as_ref().join(target))
22            .unwrap_or_else(|| target.into())
23    }
24}
25
26pub fn multi2git<P: AsRef<Path>>(
27    verify: bool,
28    git_active_branches: Option<usize>,
29    ignore_unknown_requirements: bool,
30    env: &env::Environment,
31    config_filename: P,
32    multi_config: &config::MultiConfig,
33) -> Result<(), ErrorKind> {
34    debug!("Config: {:?}", multi_config);
35    debug!("Environment: {:?}", env);
36
37    let config_path = config_filename.as_ref().parent();
38
39    for repo in &multi_config.repositories {
40        export_repository(
41            &config_path,
42            repo,
43            env,
44            verify,
45            git_active_branches,
46            ignore_unknown_requirements,
47        )?;
48    }
49
50    let path_git = construct_path(&config_path, &multi_config.path_git);
51
52    let git_repo = GitTargetRepository::open(&path_git);
53
54    let new_repository = !path_git.exists();
55
56    let default_branch = git_repo.git_config_default_branch()?;
57    let remotes = if new_repository {
58        git_repo.create_repo(&default_branch)?;
59        HashSet::new()
60    } else {
61        git_repo.remote_list()?
62    };
63
64    let mut merge = HashMap::new();
65    for repo in &multi_config.repositories {
66        let alias = repo
67            .alias
68            .as_ref()
69            .unwrap_or_else(|| repo.config.path_prefix.as_ref().unwrap());
70        if !remotes.contains(alias) {
71            git_repo.remote_add(
72                alias,
73                construct_path(&config_path, &repo.path_git)
74                    .canonicalize()?
75                    .to_str()
76                    .unwrap(),
77            )?;
78        }
79        if let Some(merged_branches) = &repo.merged_branches {
80            for (branch_to, branch_from) in merged_branches {
81                merge
82                    .entry(branch_to)
83                    .or_insert_with(Vec::new)
84                    .push(format!("{alias}/{branch_from}"));
85            }
86        }
87    }
88
89    git_repo.fetch_all()?;
90
91    for (branch_to, branches_from) in merge {
92        git_repo.checkout(branch_to)?;
93
94        if new_repository {
95            for branch_from in branches_from {
96                git_repo.merge_unrelated(&[branch_from.as_ref()])?;
97            }
98        } else {
99            let branches_from_str: Vec<_> = branches_from.iter().map(AsRef::as_ref).collect();
100            git_repo.merge_unrelated(&branches_from_str)?;
101        }
102    }
103
104    Ok(())
105}
106
107fn export_repository(
108    config_path: &Option<&Path>,
109    repo: &config::PathRepositoryConfig,
110    env: &env::Environment,
111    verify: bool,
112    git_active_branches: Option<usize>,
113    ignore_unknown_requirements: bool,
114) -> Result<(), ErrorKind> {
115    let path_hg = construct_path(config_path, &repo.path_hg);
116
117    info!("Reading repo: {:?}", repo.path_hg);
118    let mercurial_repo = match MercurialRepo::open_with_pull(
119        &path_hg,
120        &repo.config,
121        ignore_unknown_requirements,
122        env,
123    ) {
124        Ok(repo) => repo,
125        Err(ErrorKind::HgParserFailure(fail)) => panic!("Cannot open {:?}: {:?}", path_hg, fail),
126        Err(other) => panic!("Cannot open {:?}: {:?}", path_hg, other),
127    };
128
129    info!("Verifying heads in repository {:?}", repo.path_hg);
130    if !mercurial_repo.verify_heads(repo.config.allow_unnamed_heads)? {
131        return Err(ErrorKind::VerifyFailure("Verify heads failed".into()));
132    }
133
134    let tip = mercurial_repo.changelog_len()?;
135
136    let to = if let Some(limit_high) = repo.config.limit_high {
137        tip.min(limit_high)
138    } else {
139        tip
140    };
141
142    let offset = repo.config.offset.unwrap_or(0);
143
144    let path_git = construct_path(config_path, &repo.path_git);
145
146    let mut git_repo = GitTargetRepository::open(path_git);
147
148    git_repo.set_env(env);
149
150    let mut errors = None;
151    let mut counter: usize = 0;
152    let from_tag = {
153        let (output, saved_state, default_branch) =
154            git_repo.start_import(git_active_branches, repo.config.default_branch())?;
155
156        let (from, from_tag) = if let Some(saved_state) = saved_state.as_ref() {
157            match saved_state {
158                RepositorySavedState::OffsetedRevision(rev, from_tag) => {
159                    (rev - offset, from_tag - offset)
160                }
161            }
162        } else {
163            (0, 0)
164        };
165
166        let mut brmap = repo.config.branches.clone().unwrap_or_default();
167
168        info!(
169            "Exporting commits from repo: {:?} from {} to {} offset {:?}",
170            repo.path_hg, from, to, repo.config.offset
171        );
172
173        let show_progress_bar = !env.cron;
174
175        let start = Instant::now();
176        let progress_bar = ProgressBar::new((to - from) as u64);
177        if show_progress_bar {
178            progress_bar.set_style(ProgressStyle::default_bar().template(
179                "{spinner:.green}[{elapsed_precise}] [{wide_bar:.cyan/blue}] {msg} ({eta})",
180            )?);
181        }
182        for mut changeset in mercurial_repo.range(from..to) {
183            if show_progress_bar {
184                progress_bar.inc(1);
185                progress_bar.set_message(format!("{:6}/{}", changeset.revision.0, to));
186            }
187
188            match mercurial_repo.export_commit(
189                &mut changeset,
190                counter,
191                &mut brmap,
192                output,
193                &default_branch,
194            ) {
195                Ok(progress) => counter = progress,
196                x => {
197                    errors = Some((x, changeset.revision.0));
198                    break;
199                }
200            }
201        }
202
203        if errors.is_none() {
204            if show_progress_bar {
205                progress_bar.finish_with_message(format!(
206                    "Repository {} [{};{}). Elapsed: {}",
207                    repo.path_git.to_str().unwrap(),
208                    from,
209                    to,
210                    HumanDuration(start.elapsed())
211                ));
212            }
213
214            counter = mercurial_repo.export_tags(from_tag..to, counter, output)?;
215        }
216        from_tag
217    };
218
219    if let Some((error, at)) = errors {
220        if at > 0 {
221            let at = at as usize;
222            eprintln!("Import failed at {}", at);
223            info!("Saving last success state at {}...", at);
224            git_repo.save_state(RepositorySavedState::OffsetedRevision(
225                at + offset,
226                from_tag + offset,
227            ))?;
228        }
229        error?;
230    }
231
232    info!("Issued {} commands", counter);
233
234    info!("Saving state...");
235    git_repo.save_state(RepositorySavedState::OffsetedRevision(
236        to + offset,
237        to + offset,
238    ))?;
239
240    git_repo.finish()?;
241
242    if verify {
243        git_repo.verify(
244            mercurial_repo.path().to_str().unwrap(),
245            repo.config.path_prefix.as_ref().map(|x| &x[..]),
246        )?;
247    }
248
249    Ok(())
250}