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