##// END OF EJS Templates
removed trailing whitespace
Thomas Arendsen Hein -
r4957:cdd33a04 default
parent child Browse files
Show More
@@ -1,353 +1,353 b''
1 1 # convert.py Foreign SCM converter
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from common import NoRepo, converter_source, converter_sink
9 9 from cvs import convert_cvs
10 10 from git import convert_git
11 11 from hg import convert_mercurial
12 12 from subversion import convert_svn
13 13
14 14 import os, shutil
15 15 from mercurial import hg, ui, util, commands
16 16
17 17 commands.norepo += " convert"
18 18
19 19 converters = [convert_cvs, convert_git, convert_svn, convert_mercurial]
20 20
21 21 def convertsource(ui, path, **opts):
22 22 for c in converters:
23 23 if not hasattr(c, 'getcommit'):
24 24 continue
25 25 try:
26 26 return c(ui, path, **opts)
27 27 except NoRepo:
28 28 pass
29 29 raise util.Abort('%s: unknown repository type' % path)
30 30
31 31 def convertsink(ui, path):
32 32 if not os.path.isdir(path):
33 33 raise util.Abort("%s: not a directory" % path)
34 34 for c in converters:
35 35 if not hasattr(c, 'putcommit'):
36 36 continue
37 37 try:
38 38 return c(ui, path)
39 39 except NoRepo:
40 40 pass
41 41 raise util.Abort('%s: unknown repository type' % path)
42 42
43 43 class convert(object):
44 44 def __init__(self, ui, source, dest, mapfile, opts):
45 45
46 46 self.source = source
47 47 self.dest = dest
48 48 self.ui = ui
49 49 self.opts = opts
50 50 self.commitcache = {}
51 51 self.mapfile = mapfile
52 52 self.mapfilefd = None
53 53 self.authors = {}
54 54 self.authorfile = None
55 55
56 56 self.map = {}
57 57 try:
58 58 origmapfile = open(self.mapfile, 'r')
59 59 for l in origmapfile:
60 60 sv, dv = l[:-1].split()
61 61 self.map[sv] = dv
62 62 origmapfile.close()
63 63 except IOError:
64 64 pass
65 65
66 66 # Read first the dst author map if any
67 67 authorfile = self.dest.authorfile()
68 68 if authorfile and os.path.exists(authorfile):
69 69 self.readauthormap(authorfile)
70 70 # Extend/Override with new author map if necessary
71 71 if opts.get('authors'):
72 72 self.readauthormap(opts.get('authors'))
73 73 self.authorfile = self.dest.authorfile()
74 74
75 75 def walktree(self, heads):
76 76 '''Return a mapping that identifies the uncommitted parents of every
77 77 uncommitted changeset.'''
78 78 visit = heads
79 79 known = {}
80 80 parents = {}
81 81 while visit:
82 82 n = visit.pop(0)
83 83 if n in known or n in self.map: continue
84 84 known[n] = 1
85 85 self.commitcache[n] = self.source.getcommit(n)
86 86 cp = self.commitcache[n].parents
87 87 parents[n] = []
88 88 for p in cp:
89 89 parents[n].append(p)
90 90 visit.append(p)
91 91
92 92 return parents
93 93
94 94 def toposort(self, parents):
95 95 '''Return an ordering such that every uncommitted changeset is
96 96 preceeded by all its uncommitted ancestors.'''
97 97 visit = parents.keys()
98 98 seen = {}
99 99 children = {}
100 100
101 101 while visit:
102 102 n = visit.pop(0)
103 103 if n in seen: continue
104 104 seen[n] = 1
105 105 # Ensure that nodes without parents are present in the 'children'
106 106 # mapping.
107 107 children.setdefault(n, [])
108 108 for p in parents[n]:
109 109 if not p in self.map:
110 110 visit.append(p)
111 111 children.setdefault(p, []).append(n)
112 112
113 113 s = []
114 114 removed = {}
115 115 visit = children.keys()
116 116 while visit:
117 117 n = visit.pop(0)
118 118 if n in removed: continue
119 119 dep = 0
120 120 if n in parents:
121 121 for p in parents[n]:
122 122 if p in self.map: continue
123 123 if p not in removed:
124 124 # we're still dependent
125 125 visit.append(n)
126 126 dep = 1
127 127 break
128 128
129 129 if not dep:
130 130 # all n's parents are in the list
131 131 removed[n] = 1
132 132 if n not in self.map:
133 133 s.append(n)
134 134 if n in children:
135 135 for c in children[n]:
136 136 visit.insert(0, c)
137 137
138 138 if self.opts.get('datesort'):
139 139 depth = {}
140 140 for n in s:
141 141 depth[n] = 0
142 142 pl = [p for p in self.commitcache[n].parents
143 143 if p not in self.map]
144 144 if pl:
145 145 depth[n] = max([depth[p] for p in pl]) + 1
146 146
147 147 s = [(depth[n], self.commitcache[n].date, n) for n in s]
148 148 s.sort()
149 149 s = [e[2] for e in s]
150 150
151 151 return s
152 152
153 153 def mapentry(self, src, dst):
154 154 if self.mapfilefd is None:
155 155 try:
156 156 self.mapfilefd = open(self.mapfile, "a")
157 157 except IOError, (errno, strerror):
158 158 raise util.Abort("Could not open map file %s: %s, %s\n" % (self.mapfile, errno, strerror))
159 159 self.map[src] = dst
160 160 self.mapfilefd.write("%s %s\n" % (src, dst))
161 161 self.mapfilefd.flush()
162 162
163 163 def writeauthormap(self):
164 164 authorfile = self.authorfile
165 165 if authorfile:
166 166 self.ui.status('Writing author map file %s\n' % authorfile)
167 167 ofile = open(authorfile, 'w+')
168 168 for author in self.authors:
169 169 ofile.write("%s=%s\n" % (author, self.authors[author]))
170 170 ofile.close()
171 171
172 172 def readauthormap(self, authorfile):
173 173 afile = open(authorfile, 'r')
174 174 for line in afile:
175 175 try:
176 176 srcauthor = line.split('=')[0].strip()
177 177 dstauthor = line.split('=')[1].strip()
178 178 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
179 179 self.ui.status(
180 180 'Overriding mapping for author %s, was %s, will be %s\n'
181 181 % (srcauthor, self.authors[srcauthor], dstauthor))
182 182 else:
183 183 self.ui.debug('Mapping author %s to %s\n'
184 184 % (srcauthor, dstauthor))
185 185 self.authors[srcauthor] = dstauthor
186 186 except IndexError:
187 187 self.ui.warn(
188 188 'Ignoring bad line in author file map %s: %s\n'
189 189 % (authorfile, line))
190 190 afile.close()
191 191
192 192 def copy(self, rev):
193 193 c = self.commitcache[rev]
194 194 files = self.source.getchanges(rev)
195
195
196 196 do_copies = (hasattr(c, 'copies') and hasattr(self.dest, 'copyfile'))
197 197
198 198 for f, v in files:
199 199 try:
200 200 data = self.source.getfile(f, v)
201 201 except IOError, inst:
202 202 self.dest.delfile(f)
203 203 else:
204 204 e = self.source.getmode(f, v)
205 205 self.dest.putfile(f, e, data)
206 206 if do_copies:
207 207 if f in c.copies:
208 208 # Merely marks that a copy happened.
209 209 self.dest.copyfile(c.copies[f], f)
210 210
211 211
212 212 r = [self.map[v] for v in c.parents]
213 213 f = [f for f, v in files]
214 214 newnode = self.dest.putcommit(f, r, c)
215 215 self.mapentry(rev, newnode)
216 216
217 217 def convert(self):
218 218 try:
219 219 self.source.setrevmap(self.map)
220 220 self.ui.status("scanning source...\n")
221 221 heads = self.source.getheads()
222 222 parents = self.walktree(heads)
223 223 self.ui.status("sorting...\n")
224 224 t = self.toposort(parents)
225 225 num = len(t)
226 226 c = None
227 227
228 228 self.ui.status("converting...\n")
229 229 for c in t:
230 230 num -= 1
231 231 desc = self.commitcache[c].desc
232 232 if "\n" in desc:
233 233 desc = desc.splitlines()[0]
234 234 author = self.commitcache[c].author
235 235 author = self.authors.get(author, author)
236 236 self.commitcache[c].author = author
237 237 self.ui.status("%d %s\n" % (num, desc))
238 238 self.copy(c)
239 239
240 240 tags = self.source.gettags()
241 241 ctags = {}
242 242 for k in tags:
243 243 v = tags[k]
244 244 if v in self.map:
245 245 ctags[k] = self.map[v]
246 246
247 247 if c and ctags:
248 248 nrev = self.dest.puttags(ctags)
249 249 # write another hash correspondence to override the previous
250 250 # one so we don't end up with extra tag heads
251 251 if nrev:
252 252 self.mapentry(c, nrev)
253 253
254 254 self.writeauthormap()
255 255 finally:
256 256 self.cleanup()
257 257
258 258 def cleanup(self):
259 259 if self.mapfilefd:
260 260 self.mapfilefd.close()
261 261
262 262 def _convert(ui, src, dest=None, mapfile=None, **opts):
263 263 '''Convert a foreign SCM repository to a Mercurial one.
264 264
265 265 Accepted source formats:
266 266 - GIT
267 267 - CVS
268 268 - SVN
269 269
270 270 Accepted destination formats:
271 271 - Mercurial
272 272
273 273 If no revision is given, all revisions will be converted. Otherwise,
274 274 convert will only import up to the named revision (given in a format
275 275 understood by the source).
276 276
277 277 If no destination directory name is specified, it defaults to the
278 278 basename of the source with \'-hg\' appended. If the destination
279 279 repository doesn\'t exist, it will be created.
280 280
281 281 If <mapfile> isn\'t given, it will be put in a default location
282 282 (<dest>/.hg/shamap by default). The <mapfile> is a simple text
283 283 file that maps each source commit ID to the destination ID for
284 284 that revision, like so:
285 285 <source ID> <destination ID>
286 286
287 287 If the file doesn\'t exist, it\'s automatically created. It\'s updated
288 288 on each commit copied, so convert-repo can be interrupted and can
289 289 be run repeatedly to copy new commits.
290 290
291 291 The [username mapping] file is a simple text file that maps each source
292 292 commit author to a destination commit author. It is handy for source SCMs
293 293 that use unix logins to identify authors (eg: CVS). One line per author
294 294 mapping and the line format is:
295 295 srcauthor=whatever string you want
296 296 '''
297 297
298 298 util._encoding = 'UTF-8'
299 299
300 300 if not dest:
301 301 dest = hg.defaultdest(src) + "-hg"
302 302 ui.status("assuming destination %s\n" % dest)
303 303
304 304 # Try to be smart and initalize things when required
305 305 created = False
306 306 if os.path.isdir(dest):
307 307 if len(os.listdir(dest)) > 0:
308 308 try:
309 309 hg.repository(ui, dest)
310 310 ui.status("destination %s is a Mercurial repository\n" % dest)
311 311 except hg.RepoError:
312 312 raise util.Abort(
313 313 "destination directory %s is not empty.\n"
314 314 "Please specify an empty directory to be initialized\n"
315 315 "or an already initialized mercurial repository"
316 316 % dest)
317 317 else:
318 318 ui.status("initializing destination %s repository\n" % dest)
319 319 hg.repository(ui, dest, create=True)
320 320 created = True
321 321 elif os.path.exists(dest):
322 322 raise util.Abort("destination %s exists and is not a directory" % dest)
323 323 else:
324 324 ui.status("initializing destination %s repository\n" % dest)
325 325 hg.repository(ui, dest, create=True)
326 326 created = True
327 327
328 328 destc = convertsink(ui, dest)
329 329
330 330 try:
331 331 srcc = convertsource(ui, src, rev=opts.get('rev'))
332 332 except Exception:
333 333 if created:
334 334 shutil.rmtree(dest, True)
335 335 raise
336 336
337 337 if not mapfile:
338 338 try:
339 339 mapfile = destc.mapfile()
340 340 except:
341 341 mapfile = os.path.join(destc, "map")
342 342
343 343 c = convert(ui, srcc, destc, mapfile, opts)
344 344 c.convert()
345 345
346 346 cmdtable = {
347 347 "convert":
348 348 (_convert,
349 349 [('A', 'authors', '', 'username mapping filename'),
350 350 ('r', 'rev', '', 'import up to target revision REV'),
351 351 ('', 'datesort', None, 'try to sort changesets by date')],
352 352 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
353 353 }
@@ -1,121 +1,121 b''
1 1 # common code for the convert extension
2 2
3 3 class NoRepo(Exception): pass
4 4
5 5 class commit(object):
6 6 def __init__(self, **parts):
7 7 self.rev = None
8 8 self.branch = None
9 9
10 10 for x in "author date desc parents".split():
11 11 if not x in parts:
12 12 raise util.Abort("commit missing field %s" % x)
13 13 self.__dict__.update(parts)
14 14 if not self.desc or self.desc.isspace():
15 15 self.desc = '*** empty log message ***'
16 16
17 17 class converter_source(object):
18 18 """Conversion source interface"""
19 19
20 20 def __init__(self, ui, path, rev=None):
21 21 """Initialize conversion source (or raise NoRepo("message")
22 22 exception if path is not a valid repository)"""
23 23 self.ui = ui
24 24 self.path = path
25 25 self.rev = rev
26 26
27 27 self.encoding = 'utf-8'
28 28
29 29 def setrevmap(self, revmap):
30 30 """set the map of already-converted revisions"""
31 31 pass
32 32
33 33 def getheads(self):
34 34 """Return a list of this repository's heads"""
35 35 raise NotImplementedError()
36 36
37 37 def getfile(self, name, rev):
38 38 """Return file contents as a string"""
39 39 raise NotImplementedError()
40 40
41 41 def getmode(self, name, rev):
42 42 """Return file mode, eg. '', 'x', or 'l'"""
43 43 raise NotImplementedError()
44 44
45 45 def getchanges(self, version):
46 46 """Return sorted list of (filename, id) tuples for all files changed in rev.
47 47
48 48 id just tells us which revision to return in getfile(), e.g. in
49 49 git it's an object hash."""
50 50 raise NotImplementedError()
51 51
52 52 def getcommit(self, version):
53 53 """Return the commit object for version"""
54 54 raise NotImplementedError()
55 55
56 56 def gettags(self):
57 57 """Return the tags as a dictionary of name: revision"""
58 58 raise NotImplementedError()
59 59
60 60 def recode(self, s, encoding=None):
61 61 if not encoding:
62 62 encoding = self.encoding or 'utf-8'
63
63
64 64 try:
65 65 return s.decode(encoding).encode("utf-8")
66 66 except:
67 67 try:
68 68 return s.decode("latin-1").encode("utf-8")
69 69 except:
70 70 return s.decode(encoding, "replace").encode("utf-8")
71 71
72 72 class converter_sink(object):
73 73 """Conversion sink (target) interface"""
74 74
75 75 def __init__(self, ui, path):
76 76 """Initialize conversion sink (or raise NoRepo("message")
77 77 exception if path is not a valid repository)"""
78 78 raise NotImplementedError()
79 79
80 80 def getheads(self):
81 81 """Return a list of this repository's heads"""
82 82 raise NotImplementedError()
83 83
84 84 def mapfile(self):
85 85 """Path to a file that will contain lines
86 86 source_rev_id sink_rev_id
87 87 mapping equivalent revision identifiers for each system."""
88 88 raise NotImplementedError()
89 89
90 90 def authorfile(self):
91 91 """Path to a file that will contain lines
92 92 srcauthor=dstauthor
93 93 mapping equivalent authors identifiers for each system."""
94 94 return None
95 95
96 96 def putfile(self, f, e, data):
97 97 """Put file for next putcommit().
98 98 f: path to file
99 99 e: '', 'x', or 'l' (regular file, executable, or symlink)
100 100 data: file contents"""
101 101 raise NotImplementedError()
102 102
103 103 def delfile(self, f):
104 104 """Delete file for next putcommit().
105 105 f: path to file"""
106 106 raise NotImplementedError()
107 107
108 108 def putcommit(self, files, parents, commit):
109 109 """Create a revision with all changed files listed in 'files'
110 110 and having listed parents. 'commit' is a commit object containing
111 111 at a minimum the author, date, and message for this changeset.
112 112 Called after putfile() and delfile() calls. Note that the sink
113 113 repository is not told to update itself to a particular revision
114 114 (or even what that revision would be) before it receives the
115 115 file data."""
116 116 raise NotImplementedError()
117 117
118 118 def puttags(self, tags):
119 119 """Put tags into sink.
120 120 tags: {tagname: sink_rev_id, ...}"""
121 121 raise NotImplementedError()
@@ -1,97 +1,97 b''
1 1 # hg backend for convert extension
2 2
3 3 import os, time
4 4 from mercurial import hg
5 5
6 6 from common import NoRepo, converter_sink
7 7
8 8 class convert_mercurial(converter_sink):
9 9 def __init__(self, ui, path):
10 10 self.path = path
11 11 self.ui = ui
12 12 try:
13 13 self.repo = hg.repository(self.ui, path)
14 14 except:
15 15 raise NoRepo("could open hg repo %s" % path)
16 16
17 17 def mapfile(self):
18 18 return os.path.join(self.path, ".hg", "shamap")
19 19
20 20 def authorfile(self):
21 21 return os.path.join(self.path, ".hg", "authormap")
22 22
23 23 def getheads(self):
24 24 h = self.repo.changelog.heads()
25 25 return [ hg.hex(x) for x in h ]
26 26
27 27 def putfile(self, f, e, data):
28 28 self.repo.wwrite(f, data, e)
29 29 if self.repo.dirstate.state(f) == '?':
30 30 self.repo.dirstate.update([f], "a")
31 31
32 32 def copyfile(self, source, dest):
33 33 self.repo.copy(source, dest)
34 34
35 35 def delfile(self, f):
36 36 try:
37 37 os.unlink(self.repo.wjoin(f))
38 38 #self.repo.remove([f])
39 39 except:
40 40 pass
41 41
42 42 def putcommit(self, files, parents, commit):
43 43 seen = {}
44 44 pl = []
45 45 for p in parents:
46 46 if p not in seen:
47 47 pl.append(p)
48 48 seen[p] = 1
49 49 parents = pl
50 50
51 51 if len(parents) < 2: parents.append("0" * 40)
52 52 if len(parents) < 2: parents.append("0" * 40)
53 53 p2 = parents.pop(0)
54 54
55 55 text = commit.desc
56 56 extra = {}
57 57 if commit.branch:
58 58 extra['branch'] = commit.branch
59 59 if commit.rev:
60 60 extra['convert_revision'] = commit.rev
61
61
62 62 while parents:
63 63 p1 = p2
64 64 p2 = parents.pop(0)
65 65 a = self.repo.rawcommit(files, text, commit.author, commit.date,
66 66 hg.bin(p1), hg.bin(p2), extra=extra)
67 67 text = "(octopus merge fixup)\n"
68 68 p2 = hg.hex(self.repo.changelog.tip())
69 69
70 70 return p2
71 71
72 72 def puttags(self, tags):
73 73 try:
74 74 old = self.repo.wfile(".hgtags").read()
75 75 oldlines = old.splitlines(1)
76 76 oldlines.sort()
77 77 except:
78 78 oldlines = []
79 79
80 80 k = tags.keys()
81 81 k.sort()
82 82 newlines = []
83 83 for tag in k:
84 84 newlines.append("%s %s\n" % (tags[tag], tag))
85 85
86 86 newlines.sort()
87 87
88 88 if newlines != oldlines:
89 89 self.ui.status("updating tags\n")
90 90 f = self.repo.wfile(".hgtags", "w")
91 91 f.write("".join(newlines))
92 92 f.close()
93 93 if not oldlines: self.repo.add([".hgtags"])
94 94 date = "%s 0" % int(time.mktime(time.gmtime()))
95 95 self.repo.rawcommit([".hgtags"], "update tags", "convert-repo",
96 96 date, self.repo.changelog.tip(), hg.nullid)
97 97 return hg.hex(self.repo.changelog.tip())
@@ -1,669 +1,669 b''
1 1 # Subversion 1.4/1.5 Python API backend
2 2 #
3 3 # Copyright(C) 2007 Daniel Holth et al
4 4 #
5 5 # Configuration options:
6 6 #
7 7 # convert.svn.trunk
8 8 # Relative path to the trunk (default: "trunk")
9 9 # convert.svn.branches
10 10 # Relative path to tree of branches (default: "branches")
11 11 #
12 12 # Set these in a hgrc, or on the command line as follows:
13 13 #
14 14 # hg convert --config convert.svn.trunk=wackoname [...]
15 15
16 16 import pprint
17 17 import locale
18 18 import os
19 19 import cPickle as pickle
20 20 from mercurial import util
21 21
22 22 # Subversion stuff. Works best with very recent Python SVN bindings
23 23 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
24 24 # these bindings.
25 25
26 26 from cStringIO import StringIO
27 27
28 28 from common import NoRepo, commit, converter_source
29 29
30 30 try:
31 31 from svn.core import SubversionException, Pool
32 32 import svn.core
33 33 import svn.ra
34 34 import svn.delta
35 35 import svn
36 36 import transport
37 37 except ImportError:
38 38 pass
39 39
40 40 class CompatibilityException(Exception): pass
41 41
42 42 class changedpath(object):
43 43 def __init__(self, p):
44 44 self.copyfrom_path = p.copyfrom_path
45 45 self.copyfrom_rev = p.copyfrom_rev
46 46 self.action = p.action
47 47
48 48 # SVN conversion code stolen from bzr-svn and tailor
49 49 class convert_svn(converter_source):
50 50 def __init__(self, ui, url, rev=None):
51 51 super(convert_svn, self).__init__(ui, url, rev=rev)
52 52
53 53 try:
54 54 SubversionException
55 55 except NameError:
56 56 msg = 'subversion python bindings could not be loaded\n'
57 57 ui.warn(msg)
58 58 raise NoRepo(msg)
59 59
60 60 self.encoding = locale.getpreferredencoding()
61 61 self.lastrevs = {}
62 62
63 63 latest = None
64 64 if rev:
65 65 try:
66 66 latest = int(rev)
67 67 except ValueError:
68 68 raise util.Abort('svn: revision %s is not an integer' % rev)
69 69 try:
70 70 # Support file://path@rev syntax. Useful e.g. to convert
71 71 # deleted branches.
72 72 at = url.rfind('@')
73 73 if at >= 0:
74 74 latest = int(url[at+1:])
75 75 url = url[:at]
76 76 except ValueError, e:
77 77 pass
78 78 self.url = url
79 79 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
80 80 try:
81 81 self.transport = transport.SvnRaTransport(url=url)
82 82 self.ra = self.transport.ra
83 83 self.ctx = self.transport.client
84 84 self.base = svn.ra.get_repos_root(self.ra)
85 85 self.module = self.url[len(self.base):]
86 86 self.modulemap = {} # revision, module
87 87 self.commits = {}
88 88 self.files = {}
89 89 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
90 90 except SubversionException, e:
91 91 raise NoRepo("couldn't open SVN repo %s" % url)
92 92
93 93 try:
94 94 self.get_blacklist()
95 95 except IOError, e:
96 96 pass
97 97
98 98 self.last_changed = self.latest(self.module, latest)
99 99
100 100 self.head = self.revid(self.last_changed)
101 101
102 102 def setrevmap(self, revmap):
103 103 lastrevs = {}
104 104 for revid in revmap.keys():
105 105 uuid, module, revnum = self.revsplit(revid)
106 106 lastrevnum = lastrevs.setdefault(module, revnum)
107 107 if revnum > lastrevnum:
108 108 lastrevs[module] = revnum
109 109 self.lastrevs = lastrevs
110 110
111 111 def exists(self, path, optrev):
112 112 try:
113 113 return svn.client.ls(self.url.rstrip('/') + '/' + path,
114 114 optrev, False, self.ctx)
115 115 except SubversionException, err:
116 116 return []
117 117
118 118 def getheads(self):
119 119 # detect standard /branches, /tags, /trunk layout
120 120 optrev = svn.core.svn_opt_revision_t()
121 121 optrev.kind = svn.core.svn_opt_revision_number
122 122 optrev.value.number = self.last_changed
123 123 rpath = self.url.strip('/')
124 124 cfgtrunk = self.ui.config('convert', 'svn.trunk')
125 125 cfgbranches = self.ui.config('convert', 'svn.branches')
126 126 trunk = (cfgtrunk or 'trunk').strip('/')
127 127 branches = (cfgbranches or 'branches').strip('/')
128 128 if self.exists(trunk, optrev) and self.exists(branches, optrev):
129 129 self.ui.note('found trunk at %r and branches at %r\n' %
130 130 (trunk, branches))
131 131 oldmodule = self.module
132 132 self.module += '/' + trunk
133 133 lt = self.latest(self.module, self.last_changed)
134 134 self.head = self.revid(lt)
135 135 self.heads = [self.head]
136 136 branchnames = svn.client.ls(rpath + '/' + branches, optrev, False,
137 137 self.ctx)
138 138 for branch in branchnames.keys():
139 139 if oldmodule:
140 140 module = '/' + oldmodule + '/' + branches + '/' + branch
141 141 else:
142 142 module = '/' + branches + '/' + branch
143 143 brevnum = self.latest(module, self.last_changed)
144 144 brev = self.revid(brevnum, module)
145 145 self.ui.note('found branch %s at %d\n' % (branch, brevnum))
146 146 self.heads.append(brev)
147 147 elif cfgtrunk or cfgbranches:
148 148 raise util.Abort(_('trunk/branch layout expected, '
149 149 'but not found'))
150 150 else:
151 151 self.ui.note('working with one branch\n')
152 152 self.heads = [self.head]
153 153 return self.heads
154 154
155 155 def getfile(self, file, rev):
156 156 data, mode = self._getfile(file, rev)
157 157 self.modecache[(file, rev)] = mode
158 158 return data
159 159
160 def getmode(self, file, rev):
160 def getmode(self, file, rev):
161 161 return self.modecache[(file, rev)]
162 162
163 163 def getchanges(self, rev):
164 164 self.modecache = {}
165 165 files = self.files[rev]
166 166 cl = files
167 167 cl.sort()
168 168 # caller caches the result, so free it here to release memory
169 169 del self.files[rev]
170 170 return cl
171 171
172 172 def getcommit(self, rev):
173 173 if rev not in self.commits:
174 174 uuid, module, revnum = self.revsplit(rev)
175 175 self.module = module
176 176 self.reparent(module)
177 177 stop = self.lastrevs.get(module, 0)
178 178 self._fetch_revisions(from_revnum=revnum, to_revnum=stop)
179 179 commit = self.commits[rev]
180 180 # caller caches the result, so free it here to release memory
181 181 del self.commits[rev]
182 182 return commit
183 183
184 184 def get_log(self, paths, start, end, limit=0, discover_changed_paths=True,
185 185 strict_node_history=False):
186 186 '''wrapper for svn.ra.get_log.
187 187 on a large repository, svn.ra.get_log pins huge amounts of
188 188 memory that cannot be recovered. work around it by forking
189 189 and writing results over a pipe.'''
190 190
191 191 def child(fp):
192 192 protocol = -1
193 193 def receiver(orig_paths, revnum, author, date, message, pool):
194 194 if orig_paths is not None:
195 195 for k, v in orig_paths.iteritems():
196 196 orig_paths[k] = changedpath(v)
197 197 pickle.dump((orig_paths, revnum, author, date, message),
198 198 fp, protocol)
199 199
200 200 try:
201 201 # Use an ra of our own so that our parent can consume
202 202 # our results without confusing the server.
203 203 t = transport.SvnRaTransport(url=self.url)
204 204 svn.ra.get_log(t.ra, paths, start, end, limit,
205 205 discover_changed_paths,
206 206 strict_node_history,
207 207 receiver)
208 208 except SubversionException, (_, num):
209 209 self.ui.print_exc()
210 210 pickle.dump(num, fp, protocol)
211 211 else:
212 212 pickle.dump(None, fp, protocol)
213 213 fp.close()
214 214
215 215 def parent(fp):
216 216 while True:
217 217 entry = pickle.load(fp)
218 218 try:
219 219 orig_paths, revnum, author, date, message = entry
220 220 except:
221 221 if entry is None:
222 222 break
223 223 raise SubversionException("child raised exception", entry)
224 224 yield entry
225 225
226 226 rfd, wfd = os.pipe()
227 227 pid = os.fork()
228 228 if pid:
229 229 os.close(wfd)
230 230 for p in parent(os.fdopen(rfd, 'rb')):
231 231 yield p
232 232 ret = os.waitpid(pid, 0)[1]
233 233 if ret:
234 234 raise util.Abort(_('get_log %s') % util.explain_exit(ret))
235 235 else:
236 236 os.close(rfd)
237 237 child(os.fdopen(wfd, 'wb'))
238 238 os._exit(0)
239 239
240 240 def gettags(self):
241 241 tags = {}
242 242 start = self.revnum(self.head)
243 243 try:
244 244 for entry in self.get_log(['/tags'], 0, start):
245 245 orig_paths, revnum, author, date, message = entry
246 246 for path in orig_paths:
247 247 if not path.startswith('/tags/'):
248 248 continue
249 249 ent = orig_paths[path]
250 250 source = ent.copyfrom_path
251 251 rev = ent.copyfrom_rev
252 252 tag = path.split('/', 2)[2]
253 253 tags[tag] = self.revid(rev, module=source)
254 254 except SubversionException, (_, num):
255 255 self.ui.note('no tags found at revision %d\n' % start)
256 256 return tags
257 257
258 258 # -- helper functions --
259 259
260 260 def revid(self, revnum, module=None):
261 261 if not module:
262 262 module = self.module
263 263 return (u"svn:%s%s@%s" % (self.uuid, module, revnum)).decode(self.encoding)
264 264
265 265 def revnum(self, rev):
266 266 return int(rev.split('@')[-1])
267 267
268 268 def revsplit(self, rev):
269 269 url, revnum = rev.encode(self.encoding).split('@', 1)
270 270 revnum = int(revnum)
271 271 parts = url.split('/', 1)
272 272 uuid = parts.pop(0)[4:]
273 273 mod = ''
274 274 if parts:
275 275 mod = '/' + parts[0]
276 276 return uuid, mod, revnum
277 277
278 278 def latest(self, path, stop=0):
279 279 'find the latest revision affecting path, up to stop'
280 280 if not stop:
281 281 stop = svn.ra.get_latest_revnum(self.ra)
282 282 try:
283 283 self.reparent('')
284 284 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
285 285 self.reparent(self.module)
286 286 except SubversionException:
287 287 dirent = None
288 288 if not dirent:
289 289 print self.base, path
290 290 raise util.Abort('%s not found up to revision %d' % (path, stop))
291 291
292 292 return dirent.created_rev
293 293
294 294 def get_blacklist(self):
295 295 """Avoid certain revision numbers.
296 296 It is not uncommon for two nearby revisions to cancel each other
297 297 out, e.g. 'I copied trunk into a subdirectory of itself instead
298 298 of making a branch'. The converted repository is significantly
299 299 smaller if we ignore such revisions."""
300 300 self.blacklist = set()
301 301 blacklist = self.blacklist
302 302 for line in file("blacklist.txt", "r"):
303 303 if not line.startswith("#"):
304 304 try:
305 305 svn_rev = int(line.strip())
306 306 blacklist.add(svn_rev)
307 307 except ValueError, e:
308 308 pass # not an integer or a comment
309 309
310 310 def is_blacklisted(self, svn_rev):
311 311 return svn_rev in self.blacklist
312 312
313 313 def reparent(self, module):
314 314 svn_url = self.base + module
315 315 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
316 316 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
317 317
318 318 def _fetch_revisions(self, from_revnum = 0, to_revnum = 347):
319 319 def get_entry_from_path(path, module=self.module):
320 320 # Given the repository url of this wc, say
321 321 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
322 322 # extract the "entry" portion (a relative path) from what
323 323 # svn log --xml says, ie
324 324 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
325 325 # that is to say "tests/PloneTestCase.py"
326 326
327 327 if path.startswith(module):
328 328 relative = path[len(module):]
329 329 if relative.startswith('/'):
330 330 return relative[1:]
331 331 else:
332 332 return relative
333 333
334 334 # The path is outside our tracked tree...
335 335 self.ui.debug('Ignoring %r since it is not under %r\n' % (path, module))
336 336 return None
337 337
338 338 self.child_cset = None
339 339 def parselogentry(orig_paths, revnum, author, date, message):
340 340 self.ui.debug("parsing revision %d (%d changes)\n" %
341 341 (revnum, len(orig_paths)))
342 342
343 343 if revnum in self.modulemap:
344 344 new_module = self.modulemap[revnum]
345 345 if new_module != self.module:
346 346 self.module = new_module
347 347 self.reparent(self.module)
348 348
349 349 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
350 350 copies = {}
351 351 entries = []
352 352 rev = self.revid(revnum)
353 353 parents = []
354 354
355 355 # branch log might return entries for a parent we already have
356 356 if (rev in self.commits or
357 357 (revnum < self.lastrevs.get(self.module, 0))):
358 358 return
359 359
360 360 try:
361 361 branch = self.module.split("/")[-1]
362 362 if branch == 'trunk':
363 363 branch = ''
364 364 except IndexError:
365 365 branch = None
366 366
367 367 orig_paths = orig_paths.items()
368 368 orig_paths.sort()
369 369 for path, ent in orig_paths:
370 370 # self.ui.write("path %s\n" % path)
371 371 if path == self.module: # Follow branching back in history
372 372 if ent:
373 373 if ent.copyfrom_path:
374 374 # ent.copyfrom_rev may not be the actual last revision
375 375 prev = self.latest(ent.copyfrom_path, ent.copyfrom_rev)
376 376 self.modulemap[prev] = ent.copyfrom_path
377 377 parents = [self.revid(prev, ent.copyfrom_path)]
378 378 self.ui.note('found parent of branch %s at %d: %s\n' % \
379 379 (self.module, prev, ent.copyfrom_path))
380 380 else:
381 381 self.ui.debug("No copyfrom path, don't know what to do.\n")
382 382 # Maybe it was added and there is no more history.
383 383 entrypath = get_entry_from_path(path, module=self.module)
384 384 # self.ui.write("entrypath %s\n" % entrypath)
385 385 if entrypath is None:
386 386 # Outside our area of interest
387 387 self.ui.debug("boring@%s: %s\n" % (revnum, path))
388 388 continue
389 389 entry = entrypath.decode(self.encoding)
390 390
391 391 kind = svn.ra.check_path(self.ra, entrypath, revnum)
392 392 if kind == svn.core.svn_node_file:
393 393 if ent.copyfrom_path:
394 394 copyfrom_path = get_entry_from_path(ent.copyfrom_path)
395 395 if copyfrom_path:
396 396 self.ui.debug("Copied to %s from %s@%s\n" % (entry, copyfrom_path, ent.copyfrom_rev))
397 397 # It's probably important for hg that the source
398 398 # exists in the revision's parent, not just the
399 399 # ent.copyfrom_rev
400 400 fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev)
401 401 if fromkind != 0:
402 402 copies[self.recode(entry)] = self.recode(copyfrom_path)
403 403 entries.append(self.recode(entry))
404 404 elif kind == 0: # gone, but had better be a deleted *file*
405 405 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
406 406
407 407 # if a branch is created but entries are removed in the same
408 408 # changeset, get the right fromrev
409 409 if parents:
410 410 uuid, old_module, fromrev = self.revsplit(parents[0])
411 411 else:
412 412 fromrev = revnum - 1
413 413 # might always need to be revnum - 1 in these 3 lines?
414 414 old_module = self.modulemap.get(fromrev, self.module)
415 415
416 416 basepath = old_module + "/" + get_entry_from_path(path, module=self.module)
417 417 entrypath = old_module + "/" + get_entry_from_path(path, module=self.module)
418 418
419 419 def lookup_parts(p):
420 420 rc = None
421 421 parts = p.split("/")
422 422 for i in range(len(parts)):
423 423 part = "/".join(parts[:i])
424 424 info = part, copyfrom.get(part, None)
425 425 if info[1] is not None:
426 426 self.ui.debug("Found parent directory %s\n" % info[1])
427 427 rc = info
428 428 return rc
429 429
430 430 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
431 431
432 432 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
433 433
434 434 # need to remove fragment from lookup_parts and replace with copyfrom_path
435 435 if frompath is not None:
436 436 self.ui.debug("munge-o-matic\n")
437 437 self.ui.debug(entrypath + '\n')
438 438 self.ui.debug(entrypath[len(frompath):] + '\n')
439 439 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
440 440 fromrev = froment.copyfrom_rev
441 441 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
442 442
443 443 fromkind = svn.ra.check_path(self.ra, entrypath, fromrev)
444 444 if fromkind == svn.core.svn_node_file: # a deleted file
445 445 entries.append(self.recode(entry))
446 446 elif fromkind == svn.core.svn_node_dir:
447 447 # print "Deleted/moved non-file:", revnum, path, ent
448 448 # children = self._find_children(path, revnum - 1)
449 449 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
450 450 # Sometimes this is tricky. For example: in
451 451 # The Subversion Repository revision 6940 a dir
452 # was copied and one of its files was deleted
452 # was copied and one of its files was deleted
453 453 # from the new location in the same commit. This
454 454 # code can't deal with that yet.
455 455 if ent.action == 'C':
456 456 children = self._find_children(path, fromrev)
457 457 else:
458 458 oroot = entrypath.strip('/')
459 459 nroot = path.strip('/')
460 460 children = self._find_children(oroot, fromrev)
461 461 children = [s.replace(oroot,nroot) for s in children]
462 462 # Mark all [files, not directories] as deleted.
463 463 for child in children:
464 464 # Can we move a child directory and its
465 465 # parent in the same commit? (probably can). Could
466 # cause problems if instead of revnum -1,
466 # cause problems if instead of revnum -1,
467 467 # we have to look in (copyfrom_path, revnum - 1)
468 468 entrypath = get_entry_from_path("/" + child, module=old_module)
469 469 if entrypath:
470 470 entry = self.recode(entrypath.decode(self.encoding))
471 471 if entry in copies:
472 472 # deleted file within a copy
473 473 del copies[entry]
474 474 else:
475 475 entries.append(entry)
476 476 else:
477 477 self.ui.debug('unknown path in revision %d: %s\n' % \
478 478 (revnum, path))
479 479 elif kind == svn.core.svn_node_dir:
480 480 # Should probably synthesize normal file entries
481 481 # and handle as above to clean up copy/rename handling.
482 482
483 483 # If the directory just had a prop change,
484 484 # then we shouldn't need to look for its children.
485 485 # Also this could create duplicate entries. Not sure
486 486 # whether this will matter. Maybe should make entries a set.
487 487 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
488 488 # This will fail if a directory was copied
489 489 # from another branch and then some of its files
490 490 # were deleted in the same transaction.
491 491 children = self._find_children(path, revnum)
492 492 children.sort()
493 493 for child in children:
494 494 # Can we move a child directory and its
495 495 # parent in the same commit? (probably can). Could
496 # cause problems if instead of revnum -1,
496 # cause problems if instead of revnum -1,
497 497 # we have to look in (copyfrom_path, revnum - 1)
498 498 entrypath = get_entry_from_path("/" + child, module=self.module)
499 499 # print child, self.module, entrypath
500 500 if entrypath:
501 501 # Need to filter out directories here...
502 502 kind = svn.ra.check_path(self.ra, entrypath, revnum)
503 503 if kind != svn.core.svn_node_dir:
504 504 entries.append(self.recode(entrypath))
505 505
506 506 # Copies here (must copy all from source)
507 507 # Probably not a real problem for us if
508 508 # source does not exist
509 509
510 510 # Can do this with the copy command "hg copy"
511 511 # if ent.copyfrom_path:
512 512 # copyfrom_entry = get_entry_from_path(ent.copyfrom_path.decode(self.encoding),
513 513 # module=self.module)
514 514 # copyto_entry = entrypath
515 515 #
516 516 # print "copy directory", copyfrom_entry, 'to', copyto_entry
517 517 #
518 518 # copies.append((copyfrom_entry, copyto_entry))
519 519
520 520 if ent.copyfrom_path:
521 521 copyfrom_path = ent.copyfrom_path.decode(self.encoding)
522 522 copyfrom_entry = get_entry_from_path(copyfrom_path, module=self.module)
523 523 if copyfrom_entry:
524 524 copyfrom[path] = ent
525 525 self.ui.debug("mark %s came from %s\n" % (path, copyfrom[path]))
526 526
527 527 # Good, /probably/ a regular copy. Really should check
528 528 # to see whether the parent revision actually contains
529 529 # the directory in question.
530 530 children = self._find_children(self.recode(copyfrom_path), ent.copyfrom_rev)
531 531 children.sort()
532 532 for child in children:
533 533 entrypath = get_entry_from_path("/" + child, module=self.module)
534 534 if entrypath:
535 535 entry = entrypath.decode(self.encoding)
536 536 # print "COPY COPY From", copyfrom_entry, entry
537 537 copyto_path = path + entry[len(copyfrom_entry):]
538 538 copyto_entry = get_entry_from_path(copyto_path, module=self.module)
539 539 # print "COPY", entry, "COPY To", copyto_entry
540 540 copies[self.recode(copyto_entry)] = self.recode(entry)
541 541 # copy from quux splort/quuxfile
542 542
543 543 self.modulemap[revnum] = self.module # track backwards in time
544 544 # a list of (filename, id) where id lets us retrieve the file.
545 # eg in git, id is the object hash. for svn it'll be the
545 # eg in git, id is the object hash. for svn it'll be the
546 546 self.files[rev] = zip(entries, [rev] * len(entries))
547 547 if not entries:
548 548 return
549 549
550 550 # Example SVN datetime. Includes microseconds.
551 551 # ISO-8601 conformant
552 552 # '2007-01-04T17:35:00.902377Z'
553 553 date = util.parsedate(date[:18] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
554 554
555 555 log = message and self.recode(message)
556 556 author = author and self.recode(author) or ''
557 557
558 558 cset = commit(author=author,
559 date=util.datestr(date),
560 desc=log,
559 date=util.datestr(date),
560 desc=log,
561 561 parents=parents,
562 562 copies=copies,
563 563 branch=branch,
564 564 rev=rev.encode('utf-8'))
565 565
566 566 self.commits[rev] = cset
567 567 if self.child_cset and not self.child_cset.parents:
568 568 self.child_cset.parents = [rev]
569 569 self.child_cset = cset
570 570
571 571 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
572 572 (self.module, from_revnum, to_revnum))
573 573
574 574 try:
575 575 discover_changed_paths = True
576 576 strict_node_history = False
577 577 for entry in self.get_log([self.module], from_revnum, to_revnum):
578 578 orig_paths, revnum, author, date, message = entry
579 579 if self.is_blacklisted(revnum):
580 580 self.ui.note('skipping blacklisted revision %d\n' % revnum)
581 581 continue
582 582 if orig_paths is None:
583 583 self.ui.debug('revision %d has no entries\n' % revnum)
584 584 continue
585 585 parselogentry(orig_paths, revnum, author, date, message)
586 586 except SubversionException, (_, num):
587 587 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
588 raise NoSuchRevision(branch=self,
588 raise NoSuchRevision(branch=self,
589 589 revision="Revision number %d" % to_revnum)
590 590 raise
591 591
592 592 def _getfile(self, file, rev):
593 593 io = StringIO()
594 594 # TODO: ra.get_file transmits the whole file instead of diffs.
595 595 mode = ''
596 596 try:
597 597 revnum = self.revnum(rev)
598 598 if self.module != self.modulemap[revnum]:
599 599 self.module = self.modulemap[revnum]
600 600 self.reparent(self.module)
601 601 info = svn.ra.get_file(self.ra, file, revnum, io)
602 602 if isinstance(info, list):
603 603 info = info[-1]
604 604 mode = ("svn:executable" in info) and 'x' or ''
605 605 mode = ("svn:special" in info) and 'l' or mode
606 606 except SubversionException, e:
607 607 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
608 608 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
609 609 if e.apr_err in notfound: # File not found
610 610 raise IOError()
611 611 raise
612 612 data = io.getvalue()
613 613 if mode == 'l':
614 614 link_prefix = "link "
615 615 if data.startswith(link_prefix):
616 616 data = data[len(link_prefix):]
617 617 return data, mode
618 618
619 619 def _find_children(self, path, revnum):
620 620 path = path.strip("/")
621 621
622 622 def _find_children_fallback(path, revnum):
623 623 # SWIG python bindings for getdir are broken up to at least 1.4.3
624 624 pool = Pool()
625 625 optrev = svn.core.svn_opt_revision_t()
626 626 optrev.kind = svn.core.svn_opt_revision_number
627 627 optrev.value.number = revnum
628 628 rpath = '/'.join([self.base, path]).strip('/')
629 629 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev, True, self.ctx, pool).keys()]
630 630
631 631 if hasattr(self, '_find_children_fallback'):
632 632 return _find_children_fallback(path, revnum)
633 633
634 634 self.reparent("/" + path)
635 635 pool = Pool()
636 636
637 637 children = []
638 638 def find_children_inner(children, path, revnum = revnum):
639 639 if hasattr(svn.ra, 'get_dir2'): # Since SVN 1.4
640 640 fields = 0xffffffff # Binding does not provide SVN_DIRENT_ALL
641 641 getdir = svn.ra.get_dir2(self.ra, path, revnum, fields, pool)
642 642 else:
643 643 getdir = svn.ra.get_dir(self.ra, path, revnum, pool)
644 644 if type(getdir) == dict:
645 645 # python binding for getdir is broken up to at least 1.4.3
646 646 raise CompatibilityException()
647 647 dirents = getdir[0]
648 648 if type(dirents) == int:
649 649 # got here once due to infinite recursion bug
650 650 # pprint.pprint(getdir)
651 651 return
652 652 c = dirents.keys()
653 653 c.sort()
654 654 for child in c:
655 655 dirent = dirents[child]
656 656 if dirent.kind == svn.core.svn_node_dir:
657 657 find_children_inner(children, (path + "/" + child).strip("/"))
658 658 else:
659 659 children.append((path + "/" + child).strip("/"))
660 660
661 661 try:
662 662 find_children_inner(children, "")
663 663 except CompatibilityException:
664 664 self._find_children_fallback = True
665 665 self.reparent(self.module)
666 666 return _find_children_fallback(path, revnum)
667 667
668 668 self.reparent(self.module)
669 669 return [path + "/" + c for c in children]
@@ -1,125 +1,125 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2007 Daniel Holth <dholth@fastmail.fm>
4 4 # This is a stripped-down version of the original bzr-svn transport.py,
5 5 # Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
6 6
7 7 # This program is free software; you can redistribute it and/or modify
8 8 # it under the terms of the GNU General Public License as published by
9 9 # the Free Software Foundation; either version 2 of the License, or
10 10 # (at your option) any later version.
11 11
12 12 # This program is distributed in the hope that it will be useful,
13 13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 15 # GNU General Public License for more details.
16 16
17 17 # You should have received a copy of the GNU General Public License
18 18 # along with this program; if not, write to the Free Software
19 19 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 20
21 21 from cStringIO import StringIO
22 22 import os
23 23 from tempfile import mktemp
24 24
25 25 from svn.core import SubversionException, Pool
26 26 import svn.ra
27 27 import svn.client
28 28 import svn.core
29 29
30 # Some older versions of the Python bindings need to be
30 # Some older versions of the Python bindings need to be
31 31 # explicitly initialized. But what we want to do probably
32 32 # won't work worth a darn against those libraries anyway!
33 33 svn.ra.initialize()
34 34
35 35 svn_config = svn.core.svn_config_get_config(None)
36 36
37 37
38 38 def _create_auth_baton(pool):
39 39 """Create a Subversion authentication baton. """
40 40 import svn.client
41 41 # Give the client context baton a suite of authentication
42 42 # providers.h
43 43 providers = [
44 44 svn.client.get_simple_provider(pool),
45 45 svn.client.get_username_provider(pool),
46 46 svn.client.get_ssl_client_cert_file_provider(pool),
47 47 svn.client.get_ssl_client_cert_pw_file_provider(pool),
48 48 svn.client.get_ssl_server_trust_file_provider(pool),
49 49 ]
50 50 return svn.core.svn_auth_open(providers, pool)
51 51
52 52 class NotBranchError(SubversionException):
53 53 pass
54 54
55 55 class SvnRaTransport(object):
56 56 """
57 57 Open an ra connection to a Subversion repository.
58 58 """
59 59 def __init__(self, url="", ra=None):
60 60 self.pool = Pool()
61 61 self.svn_url = url
62 62 self.username = ''
63 63 self.password = ''
64 64
65 65 # Only Subversion 1.4 has reparent()
66 66 if ra is None or not hasattr(svn.ra, 'reparent'):
67 67 self.client = svn.client.create_context(self.pool)
68 68 ab = _create_auth_baton(self.pool)
69 69 if False:
70 70 svn.core.svn_auth_set_parameter(
71 71 ab, svn.core.SVN_AUTH_PARAM_DEFAULT_USERNAME, self.username)
72 72 svn.core.svn_auth_set_parameter(
73 73 ab, svn.core.SVN_AUTH_PARAM_DEFAULT_PASSWORD, self.password)
74 74 self.client.auth_baton = ab
75 75 self.client.config = svn_config
76 76 try:
77 77 self.ra = svn.client.open_ra_session(
78 self.svn_url.encode('utf8'),
78 self.svn_url.encode('utf8'),
79 79 self.client, self.pool)
80 80 except SubversionException, (_, num):
81 81 if num in (svn.core.SVN_ERR_RA_ILLEGAL_URL,
82 82 svn.core.SVN_ERR_RA_LOCAL_REPOS_OPEN_FAILED,
83 83 svn.core.SVN_ERR_BAD_URL):
84 84 raise NotBranchError(url)
85 85 raise
86 86 else:
87 87 self.ra = ra
88 88 svn.ra.reparent(self.ra, self.svn_url.encode('utf8'))
89 89
90 90 class Reporter:
91 91 def __init__(self, (reporter, report_baton)):
92 92 self._reporter = reporter
93 93 self._baton = report_baton
94 94
95 95 def set_path(self, path, revnum, start_empty, lock_token, pool=None):
96 96 svn.ra.reporter2_invoke_set_path(self._reporter, self._baton,
97 97 path, revnum, start_empty, lock_token, pool)
98 98
99 99 def delete_path(self, path, pool=None):
100 100 svn.ra.reporter2_invoke_delete_path(self._reporter, self._baton,
101 101 path, pool)
102 102
103 103 def link_path(self, path, url, revision, start_empty, lock_token,
104 104 pool=None):
105 105 svn.ra.reporter2_invoke_link_path(self._reporter, self._baton,
106 106 path, url, revision, start_empty, lock_token,
107 107 pool)
108 108
109 109 def finish_report(self, pool=None):
110 110 svn.ra.reporter2_invoke_finish_report(self._reporter,
111 111 self._baton, pool)
112 112
113 113 def abort_report(self, pool=None):
114 114 svn.ra.reporter2_invoke_abort_report(self._reporter,
115 115 self._baton, pool)
116 116
117 117 def do_update(self, revnum, path, *args, **kwargs):
118 118 return self.Reporter(svn.ra.do_update(self.ra, revnum, path, *args, **kwargs))
119 119
120 120 def clone(self, offset=None):
121 121 """See Transport.clone()."""
122 122 if offset is None:
123 123 return self.__class__(self.base)
124 124
125 125 return SvnRaTransport(urlutils.join(self.base, offset), ra=self.ra)
@@ -1,168 +1,168 b''
1 1 # Copyright (C) 2006 - Marco Barisione <marco@barisione.org>
2 2 #
3 3 # This is a small extension for Mercurial (http://www.selenic.com/mercurial)
4 4 # that removes files not known to mercurial
5 5 #
6 6 # This program was inspired by the "cvspurge" script contained in CVS utilities
7 7 # (http://www.red-bean.com/cvsutils/).
8 8 #
9 9 # To enable the "purge" extension put these lines in your ~/.hgrc:
10 10 # [extensions]
11 11 # hgext.purge =
12 12 #
13 13 # For help on the usage of "hg purge" use:
14 14 # hg help purge
15 15 #
16 16 # This program is free software; you can redistribute it and/or modify
17 17 # it under the terms of the GNU General Public License as published by
18 18 # the Free Software Foundation; either version 2 of the License, or
19 19 # (at your option) any later version.
20 20 #
21 21 # This program is distributed in the hope that it will be useful,
22 22 # but WITHOUT ANY WARRANTY; without even the implied warranty of
23 23 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 24 # GNU General Public License for more details.
25 25 #
26 26 # You should have received a copy of the GNU General Public License
27 27 # along with this program; if not, write to the Free Software
28 28 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
29 29
30 30 from mercurial import hg, util
31 31 from mercurial.i18n import _
32 32 import os
33 33
34 def dopurge(ui, repo, dirs=None, act=True, ignored=False,
34 def dopurge(ui, repo, dirs=None, act=True, ignored=False,
35 35 abort_on_err=False, eol='\n',
36 36 force=False, include=None, exclude=None):
37 37 def error(msg):
38 38 if abort_on_err:
39 39 raise util.Abort(msg)
40 40 else:
41 41 ui.warn(_('warning: %s\n') % msg)
42 42
43 43 def remove(remove_func, name):
44 44 if act:
45 45 try:
46 46 remove_func(os.path.join(repo.root, name))
47 47 except OSError, e:
48 48 error(_('%s cannot be removed') % name)
49 49 else:
50 50 ui.write('%s%s' % (name, eol))
51 51
52 52 directories = []
53 53 files = []
54 54 missing = []
55 55 roots, match, anypats = util.cmdmatcher(repo.root, repo.getcwd(), dirs,
56 56 include, exclude)
57 57 for src, f, st in repo.dirstate.statwalk(files=roots, match=match,
58 58 ignored=ignored, directories=True):
59 59 if src == 'd':
60 60 directories.append(f)
61 61 elif src == 'm':
62 62 missing.append(f)
63 63 elif src == 'f' and f not in repo.dirstate:
64 64 files.append(f)
65 65
66 66 _check_missing(ui, repo, missing, force)
67 67
68 68 directories.sort()
69 69
70 70 for f in files:
71 71 if f not in repo.dirstate:
72 72 ui.note(_('Removing file %s\n') % f)
73 73 remove(os.remove, f)
74 74
75 75 for f in directories[::-1]:
76 76 if match(f) and not os.listdir(repo.wjoin(f)):
77 77 ui.note(_('Removing directory %s\n') % f)
78 78 remove(os.rmdir, f)
79 79
80 80 def _check_missing(ui, repo, missing, force=False):
81 81 """Abort if there is the chance of having problems with name-mangling fs
82 82
83 83 In a name mangling filesystem (e.g. a case insensitive one)
84 84 dirstate.walk() can yield filenames different from the ones
85 85 stored in the dirstate. This already confuses the status and
86 86 add commands, but with purge this may cause data loss.
87 87
88 88 To prevent this, _check_missing will abort if there are missing
89 89 files. The force option will let the user skip the check if he
90 90 knows it is safe.
91 91
92 92 Even with the force option this function will check if any of the
93 93 missing files is still available in the working dir: if so there
94 94 may be some problem with the underlying filesystem, so it
95 95 aborts unconditionally."""
96 96
97 97 found = [f for f in missing if util.lexists(repo.wjoin(f))]
98 98
99 99 if found:
100 100 if not ui.quiet:
101 101 ui.warn(_("The following tracked files weren't listed by the "
102 102 "filesystem, but could still be found:\n"))
103 103 for f in found:
104 104 ui.warn("%s\n" % f)
105 105 if util.checkfolding(repo.path):
106 106 ui.warn(_("This is probably due to a case-insensitive "
107 107 "filesystem\n"))
108 108 raise util.Abort(_("purging on name mangling filesystems is not "
109 109 "yet fully supported"))
110 110
111 111 if missing and not force:
112 112 raise util.Abort(_("there are missing files in the working dir and "
113 113 "purge still has problems with them due to name "
114 114 "mangling filesystems. "
115 115 "Use --force if you know what you are doing"))
116 116
117 117
118 118 def purge(ui, repo, *dirs, **opts):
119 119 '''removes files not tracked by mercurial
120 120
121 121 Delete files not known to mercurial, this is useful to test local and
122 122 uncommitted changes in the otherwise clean source tree.
123 123
124 124 This means that purge will delete:
125 125 - Unknown files: files marked with "?" by "hg status"
126 126 - Ignored files: files usually ignored by Mercurial because they match
127 127 a pattern in a ".hgignore" file
128 128 - Empty directories: in fact Mercurial ignores directories unless they
129 129 contain files under source control managment
130 130 But it will leave untouched:
131 131 - Unmodified tracked files
132 132 - Modified tracked files
133 133 - New files added to the repository (with "hg add")
134 134
135 135 If directories are given on the command line, only files in these
136 136 directories are considered.
137 137
138 138 Be careful with purge, you could irreversibly delete some files you
139 139 forgot to add to the repository. If you only want to print the list of
140 140 files that this program would delete use the --print option.
141 141 '''
142 142 act = not opts['print']
143 143 ignored = bool(opts['all'])
144 144 abort_on_err = bool(opts['abort_on_err'])
145 145 eol = opts['print0'] and '\0' or '\n'
146 146 if eol == '\0':
147 147 # --print0 implies --print
148 148 act = False
149 149 force = bool(opts['force'])
150 150 include = opts['include']
151 151 exclude = opts['exclude']
152 152 dopurge(ui, repo, dirs, act, ignored, abort_on_err,
153 153 eol, force, include, exclude)
154 154
155 155
156 156 cmdtable = {
157 157 'purge|clean':
158 158 (purge,
159 159 [('a', 'abort-on-err', None, _('abort if an error occurs')),
160 160 ('', 'all', None, _('purge ignored files too')),
161 161 ('f', 'force', None, _('purge even when missing files are detected')),
162 162 ('p', 'print', None, _('print the file names instead of deleting them')),
163 163 ('0', 'print0', None, _('end filenames with NUL, for use with xargs'
164 164 ' (implies -p)')),
165 165 ('I', 'include', [], _('include names matching the given patterns')),
166 166 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
167 167 _('hg purge [OPTION]... [DIR]...'))
168 168 }
@@ -1,258 +1,258 b''
1 1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 9 from mercurial import demandimport; demandimport.enable()
10 10 import os, mimetools, cStringIO
11 11 from mercurial.i18n import gettext as _
12 12 from mercurial import ui, hg, util, templater
13 13 from common import get_mtime, staticfile, style_map, paritygen
14 14 from hgweb_mod import hgweb
15 15
16 16 # This is a stopgap
17 17 class hgwebdir(object):
18 18 def __init__(self, config, parentui=None):
19 19 def cleannames(items):
20 20 return [(name.strip(os.sep), path) for name, path in items]
21 21
22 22 self.parentui = parentui
23 23 self.motd = None
24 24 self.style = None
25 25 self.stripecount = None
26 26 self.repos_sorted = ('name', False)
27 27 if isinstance(config, (list, tuple)):
28 28 self.repos = cleannames(config)
29 29 self.repos_sorted = ('', False)
30 30 elif isinstance(config, dict):
31 31 self.repos = cleannames(config.items())
32 32 self.repos.sort()
33 33 else:
34 34 if isinstance(config, util.configparser):
35 35 cp = config
36 36 else:
37 37 cp = util.configparser()
38 38 cp.read(config)
39 39 self.repos = []
40 40 if cp.has_section('web'):
41 41 if cp.has_option('web', 'motd'):
42 42 self.motd = cp.get('web', 'motd')
43 43 if cp.has_option('web', 'style'):
44 44 self.style = cp.get('web', 'style')
45 45 if cp.has_option('web', 'stripes'):
46 46 self.stripecount = int(cp.get('web', 'stripes'))
47 47 if cp.has_section('paths'):
48 48 self.repos.extend(cleannames(cp.items('paths')))
49 49 if cp.has_section('collections'):
50 50 for prefix, root in cp.items('collections'):
51 51 for path in util.walkrepos(root):
52 52 repo = os.path.normpath(path)
53 53 name = repo
54 54 if name.startswith(prefix):
55 55 name = name[len(prefix):]
56 56 self.repos.append((name.lstrip(os.sep), repo))
57 57 self.repos.sort()
58 58
59 59 def run(self):
60 60 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
61 61 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
62 62 import mercurial.hgweb.wsgicgi as wsgicgi
63 63 from request import wsgiapplication
64 64 def make_web_app():
65 65 return self
66 66 wsgicgi.launch(wsgiapplication(make_web_app))
67 67
68 68 def run_wsgi(self, req):
69 69 def header(**map):
70 70 header_file = cStringIO.StringIO(
71 71 ''.join(tmpl("header", encoding=util._encoding, **map)))
72 72 msg = mimetools.Message(header_file, 0)
73 73 req.header(msg.items())
74 74 yield header_file.read()
75 75
76 76 def footer(**map):
77 77 yield tmpl("footer", **map)
78 78
79 79 def motd(**map):
80 80 if self.motd is not None:
81 81 yield self.motd
82 82 else:
83 83 yield config('web', 'motd', '')
84 84
85 85 parentui = self.parentui or ui.ui(report_untrusted=False)
86 86
87 87 def config(section, name, default=None, untrusted=True):
88 88 return parentui.config(section, name, default, untrusted)
89 89
90 90 url = req.env['REQUEST_URI'].split('?')[0]
91 91 if not url.endswith('/'):
92 92 url += '/'
93 93 pathinfo = req.env.get('PATH_INFO', '').strip('/') + '/'
94 94 base = url[:len(url) - len(pathinfo)]
95 95 if not base.endswith('/'):
96 96 base += '/'
97 97
98 98 staticurl = config('web', 'staticurl') or base + 'static/'
99 99 if not staticurl.endswith('/'):
100 100 staticurl += '/'
101 101
102 102 style = self.style
103 103 if style is None:
104 104 style = config('web', 'style', '')
105 105 if req.form.has_key('style'):
106 106 style = req.form['style'][0]
107 107 if self.stripecount is None:
108 108 self.stripecount = int(config('web', 'stripes', 1))
109 109 mapfile = style_map(templater.templatepath(), style)
110 110 tmpl = templater.templater(mapfile, templater.common_filters,
111 111 defaults={"header": header,
112 112 "footer": footer,
113 113 "motd": motd,
114 114 "url": url,
115 115 "staticurl": staticurl})
116 116
117 117 def archivelist(ui, nodeid, url):
118 118 allowed = ui.configlist("web", "allow_archive", untrusted=True)
119 119 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
120 120 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
121 121 untrusted=True):
122 122 yield {"type" : i[0], "extension": i[1],
123 123 "node": nodeid, "url": url}
124 124
125 125 def entries(sortcolumn="", descending=False, subdir="", **map):
126 126 def sessionvars(**map):
127 127 fields = []
128 128 if req.form.has_key('style'):
129 129 style = req.form['style'][0]
130 130 if style != get('web', 'style', ''):
131 131 fields.append(('style', style))
132 132
133 133 separator = url[-1] == '?' and ';' or '?'
134 134 for name, value in fields:
135 135 yield dict(name=name, value=value, separator=separator)
136 136 separator = ';'
137 137
138 138 rows = []
139 139 parity = paritygen(self.stripecount)
140 140 for name, path in self.repos:
141 141 if not name.startswith(subdir):
142 142 continue
143 143 name = name[len(subdir):]
144 144
145 145 u = ui.ui(parentui=parentui)
146 146 try:
147 147 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
148 148 except IOError:
149 149 pass
150 150 def get(section, name, default=None):
151 151 return u.config(section, name, default, untrusted=True)
152 152
153 153 if u.configbool("web", "hidden", untrusted=True):
154 154 continue
155 155
156 156 url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name])
157 157 .replace("//", "/")) + '/'
158 158
159 159 # update time with local timezone
160 160 try:
161 161 d = (get_mtime(path), util.makedate()[1])
162 162 except OSError:
163 163 continue
164 164
165 165 contact = (get("ui", "username") or # preferred
166 166 get("web", "contact") or # deprecated
167 167 get("web", "author", "")) # also
168 168 description = get("web", "description", "")
169 169 name = get("web", "name", name)
170 170 row = dict(contact=contact or "unknown",
171 171 contact_sort=contact.upper() or "unknown",
172 172 name=name,
173 173 name_sort=name,
174 174 url=url,
175 175 description=description or "unknown",
176 176 description_sort=description.upper() or "unknown",
177 177 lastchange=d,
178 178 lastchange_sort=d[1]-d[0],
179 179 sessionvars=sessionvars,
180 180 archives=archivelist(u, "tip", url))
181 181 if (not sortcolumn
182 182 or (sortcolumn, descending) == self.repos_sorted):
183 183 # fast path for unsorted output
184 184 row['parity'] = parity.next()
185 185 yield row
186 186 else:
187 187 rows.append((row["%s_sort" % sortcolumn], row))
188 188 if rows:
189 189 rows.sort()
190 190 if descending:
191 191 rows.reverse()
192 192 for key, row in rows:
193 193 row['parity'] = parity.next()
194 194 yield row
195 195
196 196 def makeindex(req, subdir=""):
197 197 sortable = ["name", "description", "contact", "lastchange"]
198 198 sortcolumn, descending = self.repos_sorted
199 199 if req.form.has_key('sort'):
200 200 sortcolumn = req.form['sort'][0]
201 201 descending = sortcolumn.startswith('-')
202 202 if descending:
203 203 sortcolumn = sortcolumn[1:]
204 204 if sortcolumn not in sortable:
205 205 sortcolumn = ""
206 206
207 207 sort = [("sort_%s" % column,
208 208 "%s%s" % ((not descending and column == sortcolumn)
209 209 and "-" or "", column))
210 210 for column in sortable]
211 211 req.write(tmpl("index", entries=entries, subdir=subdir,
212 212 sortcolumn=sortcolumn, descending=descending,
213 213 **dict(sort)))
214 214
215 215 try:
216 216 virtual = req.env.get("PATH_INFO", "").strip('/')
217 217 if virtual.startswith('static/'):
218 218 static = os.path.join(templater.templatepath(), 'static')
219 219 fname = virtual[7:]
220 220 req.write(staticfile(static, fname, req) or
221 221 tmpl('error', error='%r not found' % fname))
222 222 elif virtual:
223 223 repos = dict(self.repos)
224 224 while virtual:
225 225 real = repos.get(virtual)
226 226 if real:
227 227 req.env['REPO_NAME'] = virtual
228 228 try:
229 229 repo = hg.repository(parentui, real)
230 230 hgweb(repo).run_wsgi(req)
231 231 except IOError, inst:
232 232 req.write(tmpl("error", error=inst.strerror))
233 233 except hg.RepoError, inst:
234 234 req.write(tmpl("error", error=str(inst)))
235 235 return
236 236
237 237 # browse subdirectories
238 238 subdir = virtual + '/'
239 239 if [r for r in repos if r.startswith(subdir)]:
240 240 makeindex(req, subdir)
241 241 return
242 242
243 243 up = virtual.rfind('/')
244 244 if up < 0:
245 245 break
246 246 virtual = virtual[:up]
247
247
248 248 req.write(tmpl("notfound", repo=virtual))
249 249 else:
250 250 if req.form.has_key('static'):
251 251 static = os.path.join(templater.templatepath(), "static")
252 252 fname = req.form['static'][0]
253 253 req.write(staticfile(static, fname, req)
254 254 or tmpl("error", error="%r not found" % fname))
255 255 else:
256 256 makeindex(req)
257 257 finally:
258 258 tmpl = None
@@ -1,289 +1,289 b''
1 1 # hgweb/server.py - The standalone hg web server.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 9 import os, sys, errno, urllib, BaseHTTPServer, socket, SocketServer, traceback
10 10 from mercurial import ui, hg, util, templater
11 11 from hgweb_mod import hgweb
12 12 from hgwebdir_mod import hgwebdir
13 13 from request import wsgiapplication
14 14 from mercurial.i18n import gettext as _
15 15
16 16 def _splitURI(uri):
17 17 """ Return path and query splited from uri
18 18
19 19 Just like CGI environment, the path is unquoted, the query is
20 20 not.
21 21 """
22 22 if '?' in uri:
23 23 path, query = uri.split('?', 1)
24 24 else:
25 25 path, query = uri, ''
26 26 return urllib.unquote(path), query
27 27
28 28 class _error_logger(object):
29 29 def __init__(self, handler):
30 30 self.handler = handler
31 31 def flush(self):
32 32 pass
33 33 def write(self, str):
34 34 self.writelines(str.split('\n'))
35 35 def writelines(self, seq):
36 36 for msg in seq:
37 37 self.handler.log_error("HG error: %s", msg)
38 38
39 39 class _hgwebhandler(object, BaseHTTPServer.BaseHTTPRequestHandler):
40 40
41 41 url_scheme = 'http'
42
42
43 43 def __init__(self, *args, **kargs):
44 44 self.protocol_version = 'HTTP/1.1'
45 45 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kargs)
46 46
47 47 def log_error(self, format, *args):
48 48 errorlog = self.server.errorlog
49 49 errorlog.write("%s - - [%s] %s\n" % (self.client_address[0],
50 50 self.log_date_time_string(),
51 51 format % args))
52 52
53 53 def log_message(self, format, *args):
54 54 accesslog = self.server.accesslog
55 55 accesslog.write("%s - - [%s] %s\n" % (self.client_address[0],
56 56 self.log_date_time_string(),
57 57 format % args))
58 58
59 59 def do_write(self):
60 60 try:
61 61 self.do_hgweb()
62 62 except socket.error, inst:
63 63 if inst[0] != errno.EPIPE:
64 64 raise
65 65
66 66 def do_POST(self):
67 67 try:
68 68 self.do_write()
69 69 except StandardError, inst:
70 70 self._start_response("500 Internal Server Error", [])
71 71 self._write("Internal Server Error")
72 72 tb = "".join(traceback.format_exception(*sys.exc_info()))
73 73 self.log_error("Exception happened during processing request '%s':\n%s",
74 74 self.path, tb)
75 75
76 76 def do_GET(self):
77 77 self.do_POST()
78 78
79 79 def do_hgweb(self):
80 80 path_info, query = _splitURI(self.path)
81 81
82 82 env = {}
83 83 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
84 84 env['REQUEST_METHOD'] = self.command
85 85 env['SERVER_NAME'] = self.server.server_name
86 86 env['SERVER_PORT'] = str(self.server.server_port)
87 87 env['REQUEST_URI'] = self.path
88 88 env['PATH_INFO'] = path_info
89 89 env['REMOTE_HOST'] = self.client_address[0]
90 90 env['REMOTE_ADDR'] = self.client_address[0]
91 91 if query:
92 92 env['QUERY_STRING'] = query
93 93
94 94 if self.headers.typeheader is None:
95 95 env['CONTENT_TYPE'] = self.headers.type
96 96 else:
97 97 env['CONTENT_TYPE'] = self.headers.typeheader
98 98 length = self.headers.getheader('content-length')
99 99 if length:
100 100 env['CONTENT_LENGTH'] = length
101 101 for header in [h for h in self.headers.keys()
102 102 if h not in ('content-type', 'content-length')]:
103 103 hkey = 'HTTP_' + header.replace('-', '_').upper()
104 104 hval = self.headers.getheader(header)
105 105 hval = hval.replace('\n', '').strip()
106 106 if hval:
107 107 env[hkey] = hval
108 108 env['SERVER_PROTOCOL'] = self.request_version
109 109 env['wsgi.version'] = (1, 0)
110 110 env['wsgi.url_scheme'] = self.url_scheme
111 111 env['wsgi.input'] = self.rfile
112 112 env['wsgi.errors'] = _error_logger(self)
113 113 env['wsgi.multithread'] = isinstance(self.server,
114 114 SocketServer.ThreadingMixIn)
115 115 env['wsgi.multiprocess'] = isinstance(self.server,
116 116 SocketServer.ForkingMixIn)
117 117 env['wsgi.run_once'] = 0
118 118
119 119 self.close_connection = True
120 120 self.saved_status = None
121 121 self.saved_headers = []
122 122 self.sent_headers = False
123 123 self.length = None
124 124 req = self.server.reqmaker(env, self._start_response)
125 125 for data in req:
126 126 if data:
127 127 self._write(data)
128 128
129 129 def send_headers(self):
130 130 if not self.saved_status:
131 131 raise AssertionError("Sending headers before start_response() called")
132 132 saved_status = self.saved_status.split(None, 1)
133 133 saved_status[0] = int(saved_status[0])
134 134 self.send_response(*saved_status)
135 135 should_close = True
136 136 for h in self.saved_headers:
137 137 self.send_header(*h)
138 138 if h[0].lower() == 'content-length':
139 139 should_close = False
140 140 self.length = int(h[1])
141 141 # The value of the Connection header is a list of case-insensitive
142 142 # tokens separated by commas and optional whitespace.
143 143 if 'close' in [token.strip().lower() for token in
144 144 self.headers.get('connection', '').split(',')]:
145 145 should_close = True
146 146 if should_close:
147 147 self.send_header('Connection', 'close')
148 148 self.close_connection = should_close
149 149 self.end_headers()
150 150 self.sent_headers = True
151 151
152 152 def _start_response(self, http_status, headers, exc_info=None):
153 153 code, msg = http_status.split(None, 1)
154 154 code = int(code)
155 155 self.saved_status = http_status
156 156 bad_headers = ('connection', 'transfer-encoding')
157 157 self.saved_headers = [h for h in headers
158 158 if h[0].lower() not in bad_headers]
159 159 return self._write
160 160
161 161 def _write(self, data):
162 162 if not self.saved_status:
163 163 raise AssertionError("data written before start_response() called")
164 164 elif not self.sent_headers:
165 165 self.send_headers()
166 166 if self.length is not None:
167 167 if len(data) > self.length:
168 168 raise AssertionError("Content-length header sent, but more bytes than specified are being written.")
169 169 self.length = self.length - len(data)
170 170 self.wfile.write(data)
171 171 self.wfile.flush()
172 172
173 173 class _shgwebhandler(_hgwebhandler):
174 174
175 175 url_scheme = 'https'
176
176
177 177 def setup(self):
178 178 self.connection = self.request
179 179 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
180 180 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
181 181
182 182 def do_write(self):
183 183 from OpenSSL.SSL import SysCallError
184 184 try:
185 185 super(_shgwebhandler, self).do_write()
186 186 except SysCallError, inst:
187 187 if inst.args[0] != errno.EPIPE:
188 188 raise
189 189
190 190 def handle_one_request(self):
191 191 from OpenSSL.SSL import SysCallError, ZeroReturnError
192 192 try:
193 193 super(_shgwebhandler, self).handle_one_request()
194 194 except (SysCallError, ZeroReturnError):
195 195 self.close_connection = True
196 196 pass
197 197
198 198 def create_server(ui, repo):
199 199 use_threads = True
200 200
201 201 def openlog(opt, default):
202 202 if opt and opt != '-':
203 203 return open(opt, 'w')
204 204 return default
205 205
206 206 address = repo.ui.config("web", "address", "")
207 207 port = int(repo.ui.config("web", "port", 8000))
208 208 use_ipv6 = repo.ui.configbool("web", "ipv6")
209 209 webdir_conf = repo.ui.config("web", "webdir_conf")
210 210 ssl_cert = repo.ui.config("web", "certificate")
211 211 accesslog = openlog(repo.ui.config("web", "accesslog", "-"), sys.stdout)
212 212 errorlog = openlog(repo.ui.config("web", "errorlog", "-"), sys.stderr)
213 213
214 214 if use_threads:
215 215 try:
216 216 from threading import activeCount
217 217 except ImportError:
218 218 use_threads = False
219 219
220 220 if use_threads:
221 221 _mixin = SocketServer.ThreadingMixIn
222 222 else:
223 223 if hasattr(os, "fork"):
224 224 _mixin = SocketServer.ForkingMixIn
225 225 else:
226 226 class _mixin:
227 227 pass
228 228
229 229 class MercurialHTTPServer(object, _mixin, BaseHTTPServer.HTTPServer):
230 230
231 231 # SO_REUSEADDR has broken semantics on windows
232 232 if os.name == 'nt':
233 233 allow_reuse_address = 0
234 234
235 235 def __init__(self, *args, **kargs):
236 236 BaseHTTPServer.HTTPServer.__init__(self, *args, **kargs)
237 237 self.accesslog = accesslog
238 238 self.errorlog = errorlog
239 239 self.daemon_threads = True
240 240 def make_handler():
241 241 if webdir_conf:
242 242 hgwebobj = hgwebdir(webdir_conf, ui)
243 243 elif repo is not None:
244 244 hgwebobj = hgweb(hg.repository(repo.ui, repo.root))
245 245 else:
246 246 raise hg.RepoError(_("There is no Mercurial repository here"
247 247 " (.hg not found)"))
248 248 return hgwebobj
249 249 self.reqmaker = wsgiapplication(make_handler)
250 250
251 251 addr = address
252 252 if addr in ('', '::'):
253 253 addr = socket.gethostname()
254 254
255 255 self.addr, self.port = addr, port
256 256
257 257 if ssl_cert:
258 258 try:
259 259 from OpenSSL import SSL
260 260 ctx = SSL.Context(SSL.SSLv23_METHOD)
261 261 except ImportError:
262 262 raise util.Abort("SSL support is unavailable")
263 263 ctx.use_privatekey_file(ssl_cert)
264 264 ctx.use_certificate_file(ssl_cert)
265 265 sock = socket.socket(self.address_family, self.socket_type)
266 266 self.socket = SSL.Connection(ctx, sock)
267 267 self.server_bind()
268 268 self.server_activate()
269 269
270 270 class IPv6HTTPServer(MercurialHTTPServer):
271 271 address_family = getattr(socket, 'AF_INET6', None)
272 272
273 273 def __init__(self, *args, **kwargs):
274 274 if self.address_family is None:
275 275 raise hg.RepoError(_('IPv6 not available on this system'))
276 276 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
277 277
278 278 if ssl_cert:
279 279 handler = _shgwebhandler
280 280 else:
281 281 handler = _hgwebhandler
282 282
283 283 try:
284 284 if use_ipv6:
285 285 return IPv6HTTPServer((address, port), handler)
286 286 else:
287 287 return MercurialHTTPServer((address, port), handler)
288 288 except socket.error, inst:
289 289 raise util.Abort(_('cannot start server: %s') % inst.args[1])
General Comments 0
You need to be logged in to leave comments. Login now