##// END OF EJS Templates
fastexport: simplify code
Joerg Sonnenberger -
r51887:a97f2b50 default
parent child Browse files
Show More
@@ -1,216 +1,216 b''
1 1 # Copyright 2020 Joerg Sonnenberger <joerg@bec.de>
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5 """export repositories as git fast-import stream"""
6 6
7 7 # The format specification for fast-import streams can be found at
8 8 # https://git-scm.com/docs/git-fast-import#_input_format
9 9
10 10 import re
11 11
12 12 from mercurial.i18n import _
13 13 from mercurial.node import hex, nullrev
14 14 from mercurial.utils import stringutil
15 15 from mercurial import (
16 16 error,
17 17 logcmdutil,
18 18 registrar,
19 19 scmutil,
20 20 )
21 21 from .convert import convcmd
22 22
23 23 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
24 24 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
25 25 # be specifying the version(s) of Mercurial they are tested with, or
26 26 # leave the attribute unspecified.
27 27 testedwith = b"ships-with-hg-core"
28 28
29 29 cmdtable = {}
30 30 command = registrar.command(cmdtable)
31 31
32 32 GIT_PERSON_PROHIBITED = re.compile(b'[<>\n"]')
33 33 GIT_EMAIL_PROHIBITED = re.compile(b"[<> \n]")
34 34
35 35
36 36 def convert_to_git_user(authormap, user, rev):
37 37 mapped_user = authormap.get(user, user)
38 38 user_person = stringutil.person(mapped_user)
39 39 user_email = stringutil.email(mapped_user)
40 40 if GIT_EMAIL_PROHIBITED.match(user_email) or GIT_PERSON_PROHIBITED.match(
41 41 user_person
42 42 ):
43 43 raise error.Abort(
44 44 _(b"Unable to parse user into person and email for revision %s")
45 45 % rev
46 46 )
47 47 if user_person:
48 return b'"' + user_person + b'" <' + user_email + b'>'
48 return b'"%s" <%s>' % (user_person, user_email)
49 49 else:
50 return b"<" + user_email + b">"
50 return b"<%s>" % user_email
51 51
52 52
53 53 def convert_to_git_date(date):
54 54 timestamp, utcoff = date
55 55 tzsign = b"+" if utcoff <= 0 else b"-"
56 56 if utcoff % 60 != 0:
57 57 raise error.Abort(
58 58 _(b"UTC offset in %b is not an integer number of seconds") % (date,)
59 59 )
60 60 utcoff = abs(utcoff) // 60
61 61 tzh = utcoff // 60
62 62 tzmin = utcoff % 60
63 63 return b"%d " % int(timestamp) + tzsign + b"%02d%02d" % (tzh, tzmin)
64 64
65 65
66 66 def convert_to_git_ref(branch):
67 67 # XXX filter/map depending on git restrictions
68 68 return b"refs/heads/" + branch
69 69
70 70
71 71 def write_data(buf, data, add_newline=False):
72 72 buf.append(b"data %d\n" % len(data))
73 73 buf.append(data)
74 74 if add_newline or data[-1:] != b"\n":
75 75 buf.append(b"\n")
76 76
77 77
78 78 def export_commit(ui, repo, rev, marks, authormap):
79 79 ctx = repo[rev]
80 80 revid = ctx.hex()
81 81 if revid in marks:
82 82 ui.debug(b"warning: revision %s already exported, skipped\n" % revid)
83 83 return
84 84 parents = [p for p in ctx.parents() if p.rev() != nullrev]
85 85 for p in parents:
86 86 if p.hex() not in marks:
87 87 ui.warn(
88 88 _(b"warning: parent %s of %s has not been exported, skipped\n")
89 89 % (p, revid)
90 90 )
91 91 return
92 92
93 93 # For all files modified by the commit, check if they have already
94 94 # been exported and otherwise dump the blob with the new mark.
95 95 for fname in ctx.files():
96 96 if fname not in ctx:
97 97 continue
98 98 filectx = ctx.filectx(fname)
99 99 filerev = hex(filectx.filenode())
100 100 if filerev not in marks:
101 101 mark = len(marks) + 1
102 102 marks[filerev] = mark
103 103 data = filectx.data()
104 104 buf = [b"blob\n", b"mark :%d\n" % mark]
105 105 write_data(buf, data, True)
106 106 ui.write(*buf, keepprogressbar=True)
107 107 del buf
108 108
109 109 # Assign a mark for the current revision for references by
110 110 # latter merge commits.
111 111 mark = len(marks) + 1
112 112 marks[revid] = mark
113 113
114 114 ref = convert_to_git_ref(ctx.branch())
115 115 buf = [
116 116 b"commit %s\n" % ref,
117 117 b"mark :%d\n" % mark,
118 118 b"committer %s %s\n"
119 119 % (
120 120 convert_to_git_user(authormap, ctx.user(), revid),
121 121 convert_to_git_date(ctx.date()),
122 122 ),
123 123 ]
124 124 write_data(buf, ctx.description())
125 125 if parents:
126 126 buf.append(b"from :%d\n" % marks[parents[0].hex()])
127 127 if len(parents) == 2:
128 128 buf.append(b"merge :%d\n" % marks[parents[1].hex()])
129 129 p0ctx = repo[parents[0]]
130 130 files = ctx.manifest().diff(p0ctx.manifest())
131 131 else:
132 132 files = ctx.files()
133 133 filebuf = []
134 134 for fname in files:
135 135 if fname not in ctx:
136 136 filebuf.append((fname, b"D %s\n" % fname))
137 137 else:
138 138 filectx = ctx.filectx(fname)
139 139 filerev = filectx.filenode()
140 140 fileperm = b"755" if filectx.isexec() else b"644"
141 141 changed = b"M %s :%d %s\n" % (fileperm, marks[hex(filerev)], fname)
142 142 filebuf.append((fname, changed))
143 143 filebuf.sort()
144 144 buf.extend(changed for (fname, changed) in filebuf)
145 145 del filebuf
146 146 buf.append(b"\n")
147 147 ui.write(*buf, keepprogressbar=True)
148 148 del buf
149 149
150 150
151 151 isrev = re.compile(b"^[0-9a-f]{40}$")
152 152
153 153
154 154 @command(
155 155 b"fastexport",
156 156 [
157 157 (b"r", b"rev", [], _(b"revisions to export"), _(b"REV")),
158 158 (b"i", b"import-marks", b"", _(b"old marks file to read"), _(b"FILE")),
159 159 (b"e", b"export-marks", b"", _(b"new marks file to write"), _(b"FILE")),
160 160 (
161 161 b"A",
162 162 b"authormap",
163 163 b"",
164 164 _(b"remap usernames using this file"),
165 165 _(b"FILE"),
166 166 ),
167 167 ],
168 168 _(b"[OPTION]... [REV]..."),
169 169 helpcategory=command.CATEGORY_IMPORT_EXPORT,
170 170 )
171 171 def fastexport(ui, repo, *revs, **opts):
172 172 """export repository as git fast-import stream
173 173
174 174 This command lets you dump a repository as a human-readable text stream.
175 175 It can be piped into corresponding import routines like "git fast-import".
176 176 Incremental dumps can be created by using marks files.
177 177 """
178 178 revs += tuple(opts.get("rev", []))
179 179 if not revs:
180 180 revs = scmutil.revrange(repo, [b":"])
181 181 else:
182 182 revs = logcmdutil.revrange(repo, revs)
183 183 if not revs:
184 184 raise error.Abort(_(b"no revisions matched"))
185 185 authorfile = opts.get("authormap")
186 186 if authorfile:
187 187 authormap = convcmd.readauthormap(ui, authorfile)
188 188 else:
189 189 authormap = {}
190 190
191 191 import_marks = opts.get("import_marks")
192 192 marks = {}
193 193 if import_marks:
194 194 with open(import_marks, "rb") as import_marks_file:
195 195 for line in import_marks_file:
196 196 line = line.strip()
197 197 if not isrev.match(line) or line in marks:
198 198 raise error.Abort(_(b"Corrupted marks file"))
199 199 marks[line] = len(marks) + 1
200 200
201 201 revs.sort()
202 202 with ui.makeprogress(
203 203 _(b"exporting"), unit=_(b"revisions"), total=len(revs)
204 204 ) as progress:
205 205 for rev in revs:
206 206 export_commit(ui, repo, rev, marks, authormap)
207 207 progress.increment()
208 208
209 209 export_marks = opts.get("export_marks")
210 210 if export_marks:
211 211 with open(export_marks, "wb") as export_marks_file:
212 212 output_marks = [None] * len(marks)
213 213 for k, v in marks.items():
214 214 output_marks[v - 1] = k
215 215 for k in output_marks:
216 216 export_marks_file.write(k + b"\n")
General Comments 0
You need to be logged in to leave comments. Login now