17
|
1
|
use super::SubRepo; |
|
2
|
use crate::Result; |
|
3
|
use hg_parser::{file_content, FileType, ManifestEntryDetails, MercurialRepository, Revision}; |
|
4
|
|
|
5
|
use logger::log::{error, info, trace}; |
|
6
|
use serde::{Deserialize, Serialize}; |
|
7
|
use std::{ |
|
8
|
collections::HashMap, |
|
9
|
fs::File, |
|
10
|
io::{LineWriter, Read, Write}, |
|
11
|
net::SocketAddr, |
|
12
|
path::{Path, PathBuf}, |
|
13
|
time::Instant, |
|
14
|
}; |
|
15
|
|
|
16
|
/// Mercurial configuration type -- corresponds to .hgrc file |
|
17
|
/// |
|
18
|
/// TODO set 'default' and 'default_push' paths |
|
19
|
#[derive(Serialize, Deserialize, Debug, PartialEq)] |
|
20
|
pub struct MercurialConfig { |
|
21
|
pub ui: HashMap<String, String>, |
|
22
|
pub extensions: Option<HashMap<String, Option<String>>>, |
|
23
|
pub paths: Option<HashMap<String, String>>, |
|
24
|
pub web: HgwebConfig, |
|
25
|
} |
|
26
|
|
|
27
|
impl MercurialConfig { |
|
28
|
pub fn handle<P: AsRef<Path>>(path: P) -> MercurialRepository { |
|
29
|
MercurialRepository::open(path).unwrap() |
|
30
|
} |
|
31
|
} |
|
32
|
|
|
33
|
/// Mercurial '.hgsub' file handle, which is just a list of PATH=SOURCE pairs. |
|
34
|
pub struct HgSubFile { |
|
35
|
pub path: PathBuf, // path to the .hgsub file |
|
36
|
pub subrepos: Vec<SubRepo>, // Vec containing `SubRepo` handles |
|
37
|
} |
|
38
|
|
|
39
|
impl HgSubFile { |
|
40
|
/// Create a new '.hgsub' file handle |
|
41
|
pub fn new() -> Self { |
|
42
|
HgSubFile { |
|
43
|
path: PathBuf::from(".hgsub"), |
|
44
|
subrepos: vec![], |
|
45
|
} |
|
46
|
} |
|
47
|
/// ensure that the path is in a hg repo. |
|
48
|
fn parent_is_hg(&self) -> bool { |
|
49
|
if self.path.parent().unwrap().join(".hg").exists() { |
|
50
|
true |
|
51
|
} else { |
|
52
|
false |
|
53
|
} |
|
54
|
} |
|
55
|
/// insert a subrepo into this HgSubFile. does not clone the source |
|
56
|
/// or ensure that path exists. Takes an optional argument of 'hg' |
|
57
|
/// or 'git' to indicate the subrepo-type. Value can be ommitted to |
|
58
|
pub fn insert(&mut self, path: &str, source: &str, vcs: Option<&str>) -> Result<()> { |
|
59
|
let mut prefix = ""; |
|
60
|
// set prefix based on vcs (repo type) |
|
61
|
if let Some(i) = vcs { |
|
62
|
match i { |
|
63
|
"hg" => prefix = "hg", |
|
64
|
"git" => prefix = "git", |
|
65
|
_ => { |
|
66
|
error!("failed to recognize repo type") |
|
67
|
} |
|
68
|
} |
|
69
|
} |
|
70
|
|
|
71
|
let source = format!("{}:{}", prefix, source); |
|
72
|
|
|
73
|
let subrepo = SubRepo { |
|
74
|
vcs: vcs.unwrap().to_string(), |
|
75
|
origin: source.to_string(), |
|
76
|
path: path.to_string(), |
|
77
|
}; |
|
78
|
|
|
79
|
self.subrepos.push(subrepo); |
|
80
|
Ok(()) |
|
81
|
} |
|
82
|
|
|
83
|
/// Save subs to `.hgsub` file specified in the `path` field of |
|
84
|
/// this struct. This will overwrite any existing file at that path. |
|
85
|
pub fn save(self) -> Result<()> { |
|
86
|
match self.parent_is_hg() { |
|
87
|
true => { |
|
88
|
let mut file = File::open(self.path).unwrap(); |
|
89
|
for i in self.subrepos.iter() { |
|
90
|
write!(file, "{} = {}", i.path, i.origin)?; |
|
91
|
} |
|
92
|
} |
|
93
|
false => { |
|
94
|
error!("Parent is not a Mercurial repo!") |
|
95
|
} |
|
96
|
} |
|
97
|
Ok(()) |
|
98
|
} |
|
99
|
|
|
100
|
/// Sort the full contents of a .hgsub file alphabetically. |
|
101
|
pub fn sort(self) -> Result<Self> { |
|
102
|
let mut fd = File::open(&self.path)?; |
|
103
|
let len = fd.metadata().unwrap().len() as usize; |
|
104
|
let mut bufr = String::with_capacity(len); |
|
105
|
fd.read_to_string(&mut bufr).unwrap(); // This reads the entire file into memory. |
|
106
|
drop(fd); // drop the old readonly file descriptor |
|
107
|
|
|
108
|
let mut subs = Vec::new(); |
|
109
|
|
|
110
|
trace!("starting sort of {:?} lines", &self.path.canonicalize()?); |
|
111
|
for line in bufr.lines() { |
|
112
|
subs.push(line.as_bytes()); |
|
113
|
} |
|
114
|
subs.sort_unstable(); |
|
115
|
trace!("lines have been sorted"); |
|
116
|
|
|
117
|
let file = File::create(&self.path)?; |
|
118
|
let mut file = LineWriter::new(file); |
|
119
|
for sub in subs.iter() { |
|
120
|
file.write_all(sub)?; |
|
121
|
file.write_all(b"\n")?; |
|
122
|
} |
|
123
|
|
|
124
|
file.flush()?; |
|
125
|
info!("sorted {:?}", &self.path.canonicalize()?); |
|
126
|
|
|
127
|
Ok(self) |
|
128
|
} |
|
129
|
} |
|
130
|
|
|
131
|
impl Default for HgSubFile { |
|
132
|
fn default() -> Self { |
|
133
|
HgSubFile { |
|
134
|
path: PathBuf::from(".hgsub"), |
|
135
|
subrepos: vec![], |
|
136
|
} |
|
137
|
} |
|
138
|
} |
|
139
|
|
|
140
|
/// Hgweb configuration type |
|
141
|
/// |
|
142
|
/// Based on the configuration file for 'hgweb' scripts. |
|
143
|
/// |
|
144
|
/// We don't store the file path in a field because all HgwebConfig |
|
145
|
/// values are relative to env::current_dir() |
|
146
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] |
|
147
|
pub struct HgwebConfig { |
|
148
|
pub name: String, |
|
149
|
pub contact: String, |
|
150
|
pub description: String, |
|
151
|
pub extensions: Vec<String>, |
|
152
|
pub socket: SocketAddr, |
|
153
|
pub paths: HashMap<PathBuf, PathBuf>, |
|
154
|
} |
|
155
|
|
|
156
|
impl Default for HgwebConfig { |
|
157
|
fn default() -> Self { |
|
158
|
HgwebConfig { |
|
159
|
name: "".to_string(), |
|
160
|
contact: "".to_string(), |
|
161
|
description: "".to_string(), |
|
162
|
extensions: vec![], |
|
163
|
socket: "0.0.0.0:0" |
|
164
|
.parse() |
|
165
|
.expect("could not parse hgweb socketaddr"), |
|
166
|
paths: HashMap::new(), |
|
167
|
} |
|
168
|
} |
|
169
|
} |
|
170
|
|
|
171
|
/// from hg_parser crate docs - given a mercurial repo as a path, |
|
172
|
/// exports to git fast-import format (to stdout) |
|
173
|
pub fn export_hg_git<P: AsRef<Path>>(path: P) -> Result<()> { |
|
174
|
let start = Instant::now(); |
|
175
|
let repo = MercurialRepository::open(path).expect("could not open repo path"); |
|
176
|
|
|
177
|
let stdout = std::io::stdout(); |
|
178
|
let mut writer = stdout.lock(); |
|
179
|
|
|
180
|
for changeset in &repo { |
|
181
|
let revision = changeset.revision; |
|
182
|
eprintln!("rev: {:?}", revision); |
|
183
|
|
|
184
|
let header = &changeset.header; |
|
185
|
let mut branch = None; |
|
186
|
let mut closed = false; |
|
187
|
for (key, value) in &header.extra { |
|
188
|
if key == b"branch" { |
|
189
|
branch = Some(value.as_slice()); |
|
190
|
} |
|
191
|
|
|
192
|
if key == b"close" && value == b"1" { |
|
193
|
closed = true; |
|
194
|
} |
|
195
|
} |
|
196
|
|
|
197
|
let mut branch: Vec<_> = branch.unwrap_or_else(|| b"master").into(); |
|
198
|
for b in branch.iter_mut() { |
|
199
|
if *b == b' ' { |
|
200
|
*b = b'-'; |
|
201
|
} |
|
202
|
} |
|
203
|
|
|
204
|
let user = String::from_utf8_lossy(&header.user); |
|
205
|
let desc = String::from_utf8_lossy(&header.comment); |
|
206
|
|
|
207
|
let time = header.time.timestamp_secs(); |
|
208
|
let timezone = header.time.tz_offset_secs(); |
|
209
|
let tz = format!("{:+03}{:02}", -timezone / 3600, ((-timezone % 3600) / 60)); |
|
210
|
|
|
211
|
write!(writer, "reset refs/heads/")?; |
|
212
|
writer.write_all(&mut branch)?; |
|
213
|
write!(writer, "\ncommit refs/heads/")?; |
|
214
|
writer.write_all(&mut branch)?; |
|
215
|
writeln!(writer, "\nmark :{}", mark(revision))?; |
|
216
|
|
|
217
|
writeln!(writer, "author {} {} {}", user, time, tz)?; |
|
218
|
writeln!(writer, "committer {} {} {}", user, time, tz)?; |
|
219
|
writeln!(writer, "data {}", desc.len() + 1)?; |
|
220
|
writeln!(writer, "{}\n", desc)?; |
|
221
|
|
|
222
|
match (header.p1, header.p2) { |
|
223
|
(Some(p1), Some(p2)) => { |
|
224
|
writeln!(writer, "from :{}", mark(p1))?; |
|
225
|
writeln!(writer, "merge :{}", mark(p2))?; |
|
226
|
} |
|
227
|
(Some(p), None) | (None, Some(p)) => { |
|
228
|
writeln!(writer, "from :{}", mark(p))?; |
|
229
|
} |
|
230
|
_ => (), |
|
231
|
} |
|
232
|
|
|
233
|
for mut file in changeset.files { |
|
234
|
match (file.data, file.manifest_entry) { |
|
235
|
(None, None) => { |
|
236
|
write!(writer, "D ")?; |
|
237
|
writer.write_all(&mut file.path)?; |
|
238
|
writeln!(writer)?; |
|
239
|
} |
|
240
|
(Some(data), Some(manifest_entry)) => { |
|
241
|
write!( |
|
242
|
writer, |
|
243
|
"M {} inline ", |
|
244
|
match manifest_entry.details { |
|
245
|
ManifestEntryDetails::File(FileType::Symlink) => "120000", |
|
246
|
ManifestEntryDetails::File(FileType::Executable) => "100755", |
|
247
|
ManifestEntryDetails::Tree | ManifestEntryDetails::File(FileType::Regular) => |
|
248
|
"100644", |
|
249
|
} |
|
250
|
)?; |
|
251
|
writer.write_all(&mut file.path)?; |
|
252
|
let data = file_content(&data); |
|
253
|
writeln!(writer, "\ndata {}", data.len())?; |
|
254
|
writer.write_all(&data[..])?; |
|
255
|
} |
|
256
|
_ => panic!("Wrong file data!"), |
|
257
|
} |
|
258
|
} |
|
259
|
|
|
260
|
if closed { |
|
261
|
write!(writer, "reset refs/tags/archive/")?; |
|
262
|
writer.write_all(&mut branch)?; |
|
263
|
writeln!(writer, "\nfrom :{}\n", mark(revision))?; |
|
264
|
|
|
265
|
write!(writer, "reset refs/heads/")?; |
|
266
|
writer.write_all(&mut branch)?; |
|
267
|
writeln!(writer, "\nfrom 0000000000000000000000000000000000000000\n")?; |
|
268
|
} |
|
269
|
} |
|
270
|
|
|
271
|
for (rev, tag) in repo.tags().unwrap() { |
|
272
|
eprintln!("export tag {}", tag.name); |
|
273
|
writeln!(writer, "reset refs/tags/{}", tag.name).unwrap(); |
|
274
|
writeln!(writer, "from :{}", mark(rev)).unwrap(); |
|
275
|
writeln!(writer).unwrap(); |
|
276
|
} |
|
277
|
|
|
278
|
eprintln!("Done. Elapsed: {:?}", start.elapsed()); |
|
279
|
Ok(()) |
|
280
|
} |
|
281
|
|
|
282
|
fn mark<R: Into<Revision>>(rev: R) -> usize { |
|
283
|
(rev.into() + 1).0 as usize |
|
284
|
} |