##// END OF EJS Templates
convert: unescape Perforce-escaped special characters in filenames
Eugene Baranov -
r25788:a36fd099 default
parent child Browse files
Show More
@@ -1,270 +1,284 b''
1 1 # Perforce source for convert extension.
2 2 #
3 3 # Copyright 2009, Frank Kingswood <frank@kingswood-consulting.co.uk>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from mercurial import util
9 9 from mercurial.i18n import _
10 10
11 11 from common import commit, converter_source, checktool, NoRepo
12 12 import marshal
13 13 import re
14 14
15 15 def loaditer(f):
16 16 "Yield the dictionary objects generated by p4"
17 17 try:
18 18 while True:
19 19 d = marshal.load(f)
20 20 if not d:
21 21 break
22 22 yield d
23 23 except EOFError:
24 24 pass
25 25
26 def decodefilename(filename):
27 """Perforce escapes special characters @, #, *, or %
28 with %40, %23, %2A, or %25 respectively
29
30 >>> decodefilename('portable-net45%252Bnetcore45%252Bwp8%252BMonoAndroid')
31 'portable-net45%2Bnetcore45%2Bwp8%2BMonoAndroid'
32 >>> decodefilename('//Depot/Directory/%2525/%2523/%23%40.%2A')
33 '//Depot/Directory/%25/%23/#@.*'
34 """
35 replacements = [('%2A', '*'), ('%23', '#'), ('%40', '@'), ('%25', '%')]
36 for k, v in replacements:
37 filename = filename.replace(k, v)
38 return filename
39
26 40 class p4_source(converter_source):
27 41 def __init__(self, ui, path, revs=None):
28 42 super(p4_source, self).__init__(ui, path, revs=revs)
29 43
30 44 if "/" in path and not path.startswith('//'):
31 45 raise NoRepo(_('%s does not look like a P4 repository') % path)
32 46
33 47 checktool('p4', abort=False)
34 48
35 49 self.p4changes = {}
36 50 self.heads = {}
37 51 self.changeset = {}
38 52 self.files = {}
39 53 self.copies = {}
40 54 self.tags = {}
41 55 self.lastbranch = {}
42 56 self.parent = {}
43 57 self.encoding = "latin_1"
44 58 self.depotname = {} # mapping from local name to depot name
45 59 self.localname = {} # mapping from depot name to local name
46 60 self.re_type = re.compile(
47 61 "([a-z]+)?(text|binary|symlink|apple|resource|unicode|utf\d+)"
48 62 "(\+\w+)?$")
49 63 self.re_keywords = re.compile(
50 64 r"\$(Id|Header|Date|DateTime|Change|File|Revision|Author)"
51 65 r":[^$\n]*\$")
52 66 self.re_keywords_old = re.compile("\$(Id|Header):[^$\n]*\$")
53 67
54 68 if revs and len(revs) > 1:
55 69 raise util.Abort(_("p4 source does not support specifying "
56 70 "multiple revisions"))
57 71 self._parse(ui, path)
58 72
59 73 def _parse_view(self, path):
60 74 "Read changes affecting the path"
61 75 cmd = 'p4 -G changes -s submitted %s' % util.shellquote(path)
62 76 stdout = util.popen(cmd, mode='rb')
63 77 for d in loaditer(stdout):
64 78 c = d.get("change", None)
65 79 if c:
66 80 self.p4changes[c] = True
67 81
68 82 def _parse(self, ui, path):
69 83 "Prepare list of P4 filenames and revisions to import"
70 84 ui.status(_('reading p4 views\n'))
71 85
72 86 # read client spec or view
73 87 if "/" in path:
74 88 self._parse_view(path)
75 89 if path.startswith("//") and path.endswith("/..."):
76 90 views = {path[:-3]:""}
77 91 else:
78 92 views = {"//": ""}
79 93 else:
80 94 cmd = 'p4 -G client -o %s' % util.shellquote(path)
81 95 clientspec = marshal.load(util.popen(cmd, mode='rb'))
82 96
83 97 views = {}
84 98 for client in clientspec:
85 99 if client.startswith("View"):
86 100 sview, cview = clientspec[client].split()
87 101 self._parse_view(sview)
88 102 if sview.endswith("...") and cview.endswith("..."):
89 103 sview = sview[:-3]
90 104 cview = cview[:-3]
91 105 cview = cview[2:]
92 106 cview = cview[cview.find("/") + 1:]
93 107 views[sview] = cview
94 108
95 109 # list of changes that affect our source files
96 110 self.p4changes = self.p4changes.keys()
97 111 self.p4changes.sort(key=int)
98 112
99 113 # list with depot pathnames, longest first
100 114 vieworder = views.keys()
101 115 vieworder.sort(key=len, reverse=True)
102 116
103 117 # handle revision limiting
104 118 startrev = self.ui.config('convert', 'p4.startrev', default=0)
105 119 self.p4changes = [x for x in self.p4changes
106 120 if ((not startrev or int(x) >= int(startrev)) and
107 121 (not self.revs or int(x) <= int(self.revs[0])))]
108 122
109 123 # now read the full changelists to get the list of file revisions
110 124 ui.status(_('collecting p4 changelists\n'))
111 125 lastid = None
112 126 for change in self.p4changes:
113 127 cmd = "p4 -G describe -s %s" % change
114 128 stdout = util.popen(cmd, mode='rb')
115 129 d = marshal.load(stdout)
116 130 desc = self.recode(d.get("desc", ""))
117 131 shortdesc = desc.split("\n", 1)[0]
118 132 t = '%s %s' % (d["change"], repr(shortdesc)[1:-1])
119 133 ui.status(util.ellipsis(t, 80) + '\n')
120 134
121 135 if lastid:
122 136 parents = [lastid]
123 137 else:
124 138 parents = []
125 139
126 140 date = (int(d["time"]), 0) # timezone not set
127 141 c = commit(author=self.recode(d["user"]),
128 142 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
129 143 parents=parents, desc=desc, branch='',
130 144 extra={"p4": change})
131 145
132 146 files = []
133 147 copies = {}
134 148 copiedfiles = []
135 149 i = 0
136 150 while ("depotFile%d" % i) in d and ("rev%d" % i) in d:
137 151 oldname = d["depotFile%d" % i]
138 152 filename = None
139 153 for v in vieworder:
140 154 if oldname.lower().startswith(v.lower()):
141 filename = views[v] + oldname[len(v):]
155 filename = decodefilename(views[v] + oldname[len(v):])
142 156 break
143 157 if filename:
144 158 files.append((filename, d["rev%d" % i]))
145 159 self.depotname[filename] = oldname
146 160 if (d.get("action%d" % i) == "move/add"):
147 161 copiedfiles.append(filename)
148 162 self.localname[oldname] = filename
149 163 i += 1
150 164
151 165 # Collect information about copied files
152 166 for filename in copiedfiles:
153 167 oldname = self.depotname[filename]
154 168
155 169 flcmd = 'p4 -G filelog %s' \
156 170 % util.shellquote(oldname)
157 171 flstdout = util.popen(flcmd, mode='rb')
158 172
159 173 copiedfilename = None
160 174 for d in loaditer(flstdout):
161 175 copiedoldname = None
162 176
163 177 i = 0
164 178 while ("change%d" % i) in d:
165 179 if (d["change%d" % i] == change and
166 180 d["action%d" % i] == "move/add"):
167 181 j = 0
168 182 while ("file%d,%d" % (i, j)) in d:
169 183 if d["how%d,%d" % (i, j)] == "moved from":
170 184 copiedoldname = d["file%d,%d" % (i, j)]
171 185 break
172 186 j += 1
173 187 i += 1
174 188
175 189 if copiedoldname and copiedoldname in self.localname:
176 190 copiedfilename = self.localname[copiedoldname]
177 191 break
178 192
179 193 if copiedfilename:
180 194 copies[filename] = copiedfilename
181 195 else:
182 196 ui.warn(_("cannot find source for copied file: %s@%s\n")
183 197 % (filename, change))
184 198
185 199 self.changeset[change] = c
186 200 self.files[change] = files
187 201 self.copies[change] = copies
188 202 lastid = change
189 203
190 204 if lastid:
191 205 self.heads = [lastid]
192 206
193 207 def getheads(self):
194 208 return self.heads
195 209
196 210 def getfile(self, name, rev):
197 211 cmd = 'p4 -G print %s' \
198 212 % util.shellquote("%s#%s" % (self.depotname[name], rev))
199 213
200 214 lasterror = None
201 215 while True:
202 216 stdout = util.popen(cmd, mode='rb')
203 217
204 218 mode = None
205 219 contents = ""
206 220 keywords = None
207 221
208 222 for d in loaditer(stdout):
209 223 code = d["code"]
210 224 data = d.get("data")
211 225
212 226 if code == "error":
213 227 # if this is the first time error happened
214 228 # re-attempt getting the file
215 229 if not lasterror:
216 230 lasterror = IOError(d["generic"], data)
217 231 # this will exit inner-most for-loop
218 232 break
219 233 else:
220 234 raise lasterror
221 235
222 236 elif code == "stat":
223 237 action = d.get("action")
224 238 if action in ["purge", "delete", "move/delete"]:
225 239 return None, None
226 240 p4type = self.re_type.match(d["type"])
227 241 if p4type:
228 242 mode = ""
229 243 flags = ((p4type.group(1) or "")
230 244 + (p4type.group(3) or ""))
231 245 if "x" in flags:
232 246 mode = "x"
233 247 if p4type.group(2) == "symlink":
234 248 mode = "l"
235 249 if "ko" in flags:
236 250 keywords = self.re_keywords_old
237 251 elif "k" in flags:
238 252 keywords = self.re_keywords
239 253
240 254 elif code == "text" or code == "binary":
241 255 contents += data
242 256
243 257 lasterror = None
244 258
245 259 if not lasterror:
246 260 break
247 261
248 262 if mode is None:
249 263 return None, None
250 264
251 265 if keywords:
252 266 contents = keywords.sub("$\\1$", contents)
253 267 if mode == "l" and contents.endswith("\n"):
254 268 contents = contents[:-1]
255 269
256 270 return contents, mode
257 271
258 272 def getchanges(self, rev, full):
259 273 if full:
260 274 raise util.Abort(_("convert from p4 do not support --full"))
261 275 return self.files[rev], self.copies[rev], set()
262 276
263 277 def getcommit(self, rev):
264 278 return self.changeset[rev]
265 279
266 280 def gettags(self):
267 281 return self.tags
268 282
269 283 def getchangedfiles(self, rev, i):
270 284 return sorted([x[0] for x in self.files[rev]])
@@ -1,37 +1,38 b''
1 1 # this is hack to make sure no escape characters are inserted into the output
2 2 import os, sys
3 3 if 'TERM' in os.environ:
4 4 del os.environ['TERM']
5 5 import doctest
6 6
7 7 def testmod(name, optionflags=0, testtarget=None):
8 8 __import__(name)
9 9 mod = sys.modules[name]
10 10 if testtarget is not None:
11 11 mod = getattr(mod, testtarget)
12 12 doctest.testmod(mod, optionflags=optionflags)
13 13
14 14 testmod('mercurial.changelog')
15 15 testmod('mercurial.dagparser', optionflags=doctest.NORMALIZE_WHITESPACE)
16 16 testmod('mercurial.dispatch')
17 17 testmod('mercurial.encoding')
18 18 testmod('mercurial.hg')
19 19 testmod('mercurial.hgweb.hgwebdir_mod')
20 20 testmod('mercurial.match')
21 21 testmod('mercurial.minirst')
22 22 testmod('mercurial.patch')
23 23 testmod('mercurial.pathutil')
24 24 testmod('mercurial.parser')
25 25 testmod('mercurial.revset')
26 26 testmod('mercurial.store')
27 27 testmod('mercurial.subrepo')
28 28 testmod('mercurial.templatefilters')
29 29 testmod('mercurial.templater')
30 30 testmod('mercurial.ui')
31 31 testmod('mercurial.url')
32 32 testmod('mercurial.util')
33 33 testmod('mercurial.util', testtarget='platform')
34 34 testmod('hgext.convert.cvsps')
35 35 testmod('hgext.convert.filemap')
36 testmod('hgext.convert.p4')
36 37 testmod('hgext.convert.subversion')
37 38 testmod('hgext.mq')
General Comments 0
You need to be logged in to leave comments. Login now