##// END OF EJS Templates
fix file handling bugs on windows....
Vadim Gelfer -
r2176:9b42304d default
parent child Browse files
Show More
@@ -1,154 +1,156 b''
1 1 # appendfile.py - special classes to make repo updates atomic
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.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 demandload import *
9 demandload(globals(), "cStringIO changelog errno manifest os tempfile")
9 demandload(globals(), "cStringIO changelog errno manifest os tempfile util")
10 10
11 11 # writes to metadata files are ordered. reads: changelog, manifest,
12 12 # normal files. writes: normal files, manifest, changelog.
13 13
14 14 # manifest contains pointers to offsets in normal files. changelog
15 15 # contains pointers to offsets in manifest. if reader reads old
16 16 # changelog while manifest or normal files are written, it has no
17 17 # pointers into new parts of those files that are maybe not consistent
18 18 # yet, so will not read them.
19 19
20 20 # localrepo.addchangegroup thinks it writes changelog first, then
21 21 # manifest, then normal files (this is order they are available, and
22 22 # needed for computing linkrev fields), but uses appendfile to hide
23 23 # updates from readers. data not written to manifest or changelog
24 24 # until all normal files updated. write manifest first, then
25 25 # changelog.
26 26
27 27 # with this write ordering, readers cannot see inconsistent view of
28 28 # repo during update.
29 29
30 30 class appendfile(object):
31 31 '''implement enough of file protocol to append to revlog file.
32 32 appended data is written to temp file. reads and seeks span real
33 33 file and temp file. readers cannot see appended data until
34 34 writedata called.'''
35 35
36 36 def __init__(self, fp, tmpname):
37 37 if tmpname:
38 38 self.tmpname = tmpname
39 self.tmpfp = open(self.tmpname, 'ab+')
39 self.tmpfp = util.posixfile(self.tmpname, 'ab+')
40 40 else:
41 41 fd, self.tmpname = tempfile.mkstemp()
42 self.tmpfp = os.fdopen(fd, 'ab+')
42 os.close(fd)
43 self.tmpfp = util.posixfile(self.tmpname, 'ab+')
43 44 self.realfp = fp
44 45 self.offset = fp.tell()
45 46 # real file is not written by anyone else. cache its size so
46 47 # seek and read can be fast.
47 self.realsize = os.fstat(fp.fileno()).st_size
48 self.realsize = util.fstat(fp).st_size
49 self.name = fp.name
48 50
49 51 def end(self):
50 52 self.tmpfp.flush() # make sure the stat is correct
51 return self.realsize + os.fstat(self.tmpfp.fileno()).st_size
53 return self.realsize + util.fstat(self.tmpfp).st_size
52 54
53 55 def tell(self):
54 56 return self.offset
55 57
56 58 def flush(self):
57 59 self.tmpfp.flush()
58 60
59 61 def close(self):
60 62 self.realfp.close()
61 63 self.tmpfp.close()
62 64
63 65 def seek(self, offset, whence=0):
64 66 '''virtual file offset spans real file and temp file.'''
65 67 if whence == 0:
66 68 self.offset = offset
67 69 elif whence == 1:
68 70 self.offset += offset
69 71 elif whence == 2:
70 72 self.offset = self.end() + offset
71 73
72 74 if self.offset < self.realsize:
73 75 self.realfp.seek(self.offset)
74 76 else:
75 77 self.tmpfp.seek(self.offset - self.realsize)
76 78
77 79 def read(self, count=-1):
78 80 '''only trick here is reads that span real file and temp file.'''
79 81 fp = cStringIO.StringIO()
80 82 old_offset = self.offset
81 83 if self.offset < self.realsize:
82 84 s = self.realfp.read(count)
83 85 fp.write(s)
84 86 self.offset += len(s)
85 87 if count > 0:
86 88 count -= len(s)
87 89 if count != 0:
88 90 if old_offset != self.offset:
89 91 self.tmpfp.seek(self.offset - self.realsize)
90 92 s = self.tmpfp.read(count)
91 93 fp.write(s)
92 94 self.offset += len(s)
93 95 return fp.getvalue()
94 96
95 97 def write(self, s):
96 98 '''append to temp file.'''
97 99 self.tmpfp.seek(0, 2)
98 100 self.tmpfp.write(s)
99 101 # all writes are appends, so offset must go to end of file.
100 102 self.offset = self.realsize + self.tmpfp.tell()
101 103
102 104 class appendopener(object):
103 105 '''special opener for files that only read or append.'''
104 106
105 107 def __init__(self, opener):
106 108 self.realopener = opener
107 109 # key: file name, value: appendfile name
108 110 self.tmpnames = {}
109 111
110 112 def __call__(self, name, mode='r'):
111 113 '''open file.'''
112 114
113 115 assert mode in 'ra+'
114 116 try:
115 117 realfp = self.realopener(name, 'r')
116 118 except IOError, err:
117 119 if err.errno != errno.ENOENT: raise
118 120 realfp = self.realopener(name, 'w+')
119 121 tmpname = self.tmpnames.get(name)
120 122 fp = appendfile(realfp, tmpname)
121 123 if tmpname is None:
122 124 self.tmpnames[name] = fp.tmpname
123 125 return fp
124 126
125 127 def writedata(self):
126 128 '''copy data from temp files to real files.'''
127 129 # write .d file before .i file.
128 130 tmpnames = self.tmpnames.items()
129 131 tmpnames.sort()
130 132 for name, tmpname in tmpnames:
131 133 fp = open(tmpname, 'rb')
132 134 s = fp.read()
133 135 fp.close()
134 136 os.unlink(tmpname)
135 137 fp = self.realopener(name, 'a')
136 138 fp.write(s)
137 139 fp.close()
138 140
139 141 # files for changelog and manifest are in different appendopeners, so
140 142 # not mixed up together.
141 143
142 144 class appendchangelog(changelog.changelog, appendopener):
143 145 def __init__(self, opener, version):
144 146 appendopener.__init__(self, opener)
145 147 changelog.changelog.__init__(self, self, version)
146 148 def checkinlinesize(self, fp, tr):
147 149 return
148 150
149 151 class appendmanifest(manifest.manifest, appendopener):
150 152 def __init__(self, opener, version):
151 153 appendopener.__init__(self, opener)
152 154 manifest.manifest.__init__(self, self, version)
153 155 def checkinlinesize(self, fp, tr):
154 156 return
@@ -1,207 +1,207 b''
1 1 """
2 2 bundlerepo.py - repository class for viewing uncompressed bundles
3 3
4 4 This provides a read-only repository interface to bundles as if
5 5 they were part of the actual repository.
6 6
7 7 Copyright 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
8 8
9 9 This software may be used and distributed according to the terms
10 10 of the GNU General Public License, incorporated herein by reference.
11 11 """
12 12
13 13 from node import *
14 14 from i18n import gettext as _
15 15 from demandload import demandload
16 16 demandload(globals(), "changegroup util os struct")
17 17
18 18 import localrepo, changelog, manifest, filelog, revlog
19 19
20 20 class bundlerevlog(revlog.revlog):
21 21 def __init__(self, opener, indexfile, datafile, bundlefile,
22 22 linkmapper=None):
23 23 # How it works:
24 24 # to retrieve a revision, we need to know the offset of
25 25 # the revision in the bundlefile (an opened file).
26 26 #
27 27 # We store this offset in the index (start), to differentiate a
28 28 # rev in the bundle and from a rev in the revlog, we check
29 29 # len(index[r]). If the tuple is bigger than 7, it is a bundle
30 30 # (it is bigger since we store the node to which the delta is)
31 31 #
32 32 revlog.revlog.__init__(self, opener, indexfile, datafile)
33 33 self.bundlefile = bundlefile
34 34 self.basemap = {}
35 35 def chunkpositer():
36 36 for chunk in changegroup.chunkiter(bundlefile):
37 37 pos = bundlefile.tell()
38 38 yield chunk, pos - len(chunk)
39 39 n = self.count()
40 40 prev = None
41 41 for chunk, start in chunkpositer():
42 42 size = len(chunk)
43 43 if size < 80:
44 44 raise util.Abort("invalid changegroup")
45 45 start += 80
46 46 size -= 80
47 47 node, p1, p2, cs = struct.unpack("20s20s20s20s", chunk[:80])
48 48 if node in self.nodemap:
49 49 prev = node
50 50 continue
51 51 for p in (p1, p2):
52 52 if not p in self.nodemap:
53 53 raise RevlogError(_("unknown parent %s") % short(p1))
54 54 if linkmapper is None:
55 55 link = n
56 56 else:
57 57 link = linkmapper(cs)
58 58
59 59 if not prev:
60 60 prev = p1
61 61 # start, size, base is not used, link, p1, p2, delta ref
62 62 if self.version == 0:
63 63 e = (start, size, None, link, p1, p2, node)
64 64 else:
65 65 e = (self.offset_type(start, 0), size, -1, None, link,
66 66 self.rev(p1), self.rev(p2), node)
67 67 self.basemap[n] = prev
68 68 self.index.append(e)
69 69 self.nodemap[node] = n
70 70 prev = node
71 71 n += 1
72 72
73 73 def bundle(self, rev):
74 74 """is rev from the bundle"""
75 75 if rev < 0:
76 76 return False
77 77 return rev in self.basemap
78 78 def bundlebase(self, rev): return self.basemap[rev]
79 79 def chunk(self, rev, df=None):
80 80 # Warning: in case of bundle, the diff is against bundlebase,
81 81 # not against rev - 1
82 82 # XXX: could use some caching
83 83 if not self.bundle(rev):
84 84 return revlog.revlog.chunk(self, rev)
85 85 self.bundlefile.seek(self.start(rev))
86 86 return self.bundlefile.read(self.length(rev))
87 87
88 88 def revdiff(self, rev1, rev2):
89 89 """return or calculate a delta between two revisions"""
90 90 if self.bundle(rev1) and self.bundle(rev2):
91 91 # hot path for bundle
92 92 revb = self.rev(self.bundlebase(rev2))
93 93 if revb == rev1:
94 94 return self.chunk(rev2)
95 95 elif not self.bundle(rev1) and not self.bundle(rev2):
96 96 return revlog.revlog.chunk(self, rev1, rev2)
97 97
98 98 return self.diff(self.revision(self.node(rev1)),
99 99 self.revision(self.node(rev2)))
100 100
101 101 def revision(self, node):
102 102 """return an uncompressed revision of a given"""
103 103 if node == nullid: return ""
104 104
105 105 text = None
106 106 chain = []
107 107 iter_node = node
108 108 rev = self.rev(iter_node)
109 109 # reconstruct the revision if it is from a changegroup
110 110 while self.bundle(rev):
111 111 if self.cache and self.cache[0] == iter_node:
112 112 text = self.cache[2]
113 113 break
114 114 chain.append(rev)
115 115 iter_node = self.bundlebase(rev)
116 116 rev = self.rev(iter_node)
117 117 if text is None:
118 118 text = revlog.revlog.revision(self, iter_node)
119 119
120 120 while chain:
121 121 delta = self.chunk(chain.pop())
122 122 text = self.patches(text, [delta])
123 123
124 124 p1, p2 = self.parents(node)
125 125 if node != revlog.hash(text, p1, p2):
126 126 raise RevlogError(_("integrity check failed on %s:%d")
127 127 % (self.datafile, self.rev(node)))
128 128
129 129 self.cache = (node, self.rev(node), text)
130 130 return text
131 131
132 132 def addrevision(self, text, transaction, link, p1=None, p2=None, d=None):
133 133 raise NotImplementedError
134 134 def addgroup(self, revs, linkmapper, transaction, unique=0):
135 135 raise NotImplementedError
136 136 def strip(self, rev, minlink):
137 137 raise NotImplementedError
138 138 def checksize(self):
139 139 raise NotImplementedError
140 140
141 141 class bundlechangelog(bundlerevlog, changelog.changelog):
142 142 def __init__(self, opener, bundlefile):
143 143 changelog.changelog.__init__(self, opener)
144 144 bundlerevlog.__init__(self, opener, "00changelog.i", "00changelog.d",
145 145 bundlefile)
146 146
147 147 class bundlemanifest(bundlerevlog, manifest.manifest):
148 148 def __init__(self, opener, bundlefile, linkmapper):
149 149 manifest.manifest.__init__(self, opener)
150 150 bundlerevlog.__init__(self, opener, self.indexfile, self.datafile,
151 151 bundlefile, linkmapper)
152 152
153 153 class bundlefilelog(bundlerevlog, filelog.filelog):
154 154 def __init__(self, opener, path, bundlefile, linkmapper):
155 155 filelog.filelog.__init__(self, opener, path)
156 156 bundlerevlog.__init__(self, opener, self.indexfile, self.datafile,
157 157 bundlefile, linkmapper)
158 158
159 159 class bundlerepository(localrepo.localrepository):
160 160 def __init__(self, ui, path, bundlename):
161 161 localrepo.localrepository.__init__(self, ui, path)
162 162 f = open(bundlename, "rb")
163 s = os.fstat(f.fileno())
163 s = util.fstat(f)
164 164 self.bundlefile = f
165 165 header = self.bundlefile.read(6)
166 166 if not header.startswith("HG"):
167 167 raise util.Abort(_("%s: not a Mercurial bundle file") % bundlename)
168 168 elif not header.startswith("HG10"):
169 169 raise util.Abort(_("%s: unknown bundle version") % bundlename)
170 170 elif header == "HG10BZ":
171 171 raise util.Abort(_("%s: compressed bundle not supported")
172 172 % bundlename)
173 173 elif header == "HG10UN":
174 174 # uncompressed bundle supported
175 175 pass
176 176 else:
177 177 raise util.Abort(_("%s: unknown bundle compression type")
178 178 % bundlename)
179 179 self.changelog = bundlechangelog(self.opener, self.bundlefile)
180 180 self.manifest = bundlemanifest(self.opener, self.bundlefile,
181 181 self.changelog.rev)
182 182 # dict with the mapping 'filename' -> position in the bundle
183 183 self.bundlefilespos = {}
184 184 while 1:
185 185 f = changegroup.getchunk(self.bundlefile)
186 186 if not f:
187 187 break
188 188 self.bundlefilespos[f] = self.bundlefile.tell()
189 189 for c in changegroup.chunkiter(self.bundlefile):
190 190 pass
191 191
192 192 def dev(self):
193 193 return -1
194 194
195 195 def file(self, f):
196 196 if f[0] == '/':
197 197 f = f[1:]
198 198 if f in self.bundlefilespos:
199 199 self.bundlefile.seek(self.bundlefilespos[f])
200 200 return bundlefilelog(self.opener, f, self.bundlefile,
201 201 self.changelog.rev)
202 202 else:
203 203 return filelog.filelog(self.opener, f)
204 204
205 205 def close(self):
206 206 """Close assigned bundle file immediately."""
207 207 self.bundlefile.close()
@@ -1,1241 +1,1241 b''
1 1 """
2 2 revlog.py - storage back-end for mercurial
3 3
4 4 This provides efficient delta storage with O(1) retrieve and append
5 5 and O(changes) merge between branches
6 6
7 7 Copyright 2005 Matt Mackall <mpm@selenic.com>
8 8
9 9 This software may be used and distributed according to the terms
10 10 of the GNU General Public License, incorporated herein by reference.
11 11 """
12 12
13 13 from node import *
14 14 from i18n import gettext as _
15 15 from demandload import demandload
16 16 demandload(globals(), "binascii changegroup errno heapq mdiff os")
17 demandload(globals(), "sha struct zlib")
17 demandload(globals(), "sha struct util zlib")
18 18
19 19 # revlog version strings
20 20 REVLOGV0 = 0
21 21 REVLOGNG = 1
22 22
23 23 # revlog flags
24 24 REVLOGNGINLINEDATA = (1 << 16)
25 25
26 26 def flagstr(flag):
27 27 if flag == "inline":
28 28 return REVLOGNGINLINEDATA
29 29 raise RevlogError(_("unknown revlog flag %s" % flag))
30 30
31 31 def hash(text, p1, p2):
32 32 """generate a hash from the given text and its parent hashes
33 33
34 34 This hash combines both the current file contents and its history
35 35 in a manner that makes it easy to distinguish nodes with the same
36 36 content in the revision graph.
37 37 """
38 38 l = [p1, p2]
39 39 l.sort()
40 40 s = sha.new(l[0])
41 41 s.update(l[1])
42 42 s.update(text)
43 43 return s.digest()
44 44
45 45 def compress(text):
46 46 """ generate a possibly-compressed representation of text """
47 47 if not text: return ("", text)
48 48 if len(text) < 44:
49 49 if text[0] == '\0': return ("", text)
50 50 return ('u', text)
51 51 bin = zlib.compress(text)
52 52 if len(bin) > len(text):
53 53 if text[0] == '\0': return ("", text)
54 54 return ('u', text)
55 55 return ("", bin)
56 56
57 57 def decompress(bin):
58 58 """ decompress the given input """
59 59 if not bin: return bin
60 60 t = bin[0]
61 61 if t == '\0': return bin
62 62 if t == 'x': return zlib.decompress(bin)
63 63 if t == 'u': return bin[1:]
64 64 raise RevlogError(_("unknown compression type %r") % t)
65 65
66 66 indexformatv0 = ">4l20s20s20s"
67 67 v0shaoffset = 56
68 68 # index ng:
69 69 # 6 bytes offset
70 70 # 2 bytes flags
71 71 # 4 bytes compressed length
72 72 # 4 bytes uncompressed length
73 73 # 4 bytes: base rev
74 74 # 4 bytes link rev
75 75 # 4 bytes parent 1 rev
76 76 # 4 bytes parent 2 rev
77 77 # 32 bytes: nodeid
78 78 indexformatng = ">Qiiiiii20s12x"
79 79 ngshaoffset = 32
80 80 versionformat = ">i"
81 81
82 82 class lazyparser(object):
83 83 """
84 84 this class avoids the need to parse the entirety of large indices
85 85 """
86 86 def __init__(self, dataf, size, indexformat, shaoffset):
87 87 self.dataf = dataf
88 88 self.format = indexformat
89 89 self.s = struct.calcsize(indexformat)
90 90 self.indexformat = indexformat
91 91 self.datasize = size
92 92 self.l = size/self.s
93 93 self.index = [None] * self.l
94 94 self.map = {nullid: -1}
95 95 self.allmap = 0
96 96 self.all = 0
97 97 self.mapfind_count = 0
98 98 self.shaoffset = shaoffset
99 99
100 100 def loadmap(self):
101 101 """
102 102 during a commit, we need to make sure the rev being added is
103 103 not a duplicate. This requires loading the entire index,
104 104 which is fairly slow. loadmap can load up just the node map,
105 105 which takes much less time.
106 106 """
107 107 if self.allmap: return
108 108 start = 0
109 109 end = self.datasize
110 110 self.allmap = 1
111 111 cur = 0
112 112 count = 0
113 113 blocksize = self.s * 256
114 114 self.dataf.seek(0)
115 115 while cur < end:
116 116 data = self.dataf.read(blocksize)
117 117 off = 0
118 118 for x in xrange(256):
119 119 n = data[off + self.shaoffset:off + self.shaoffset + 20]
120 120 self.map[n] = count
121 121 count += 1
122 122 if count >= self.l:
123 123 break
124 124 off += self.s
125 125 cur += blocksize
126 126
127 127 def loadblock(self, blockstart, blocksize, data=None):
128 128 if self.all: return
129 129 if data is None:
130 130 self.dataf.seek(blockstart)
131 131 data = self.dataf.read(blocksize)
132 132 lend = len(data) / self.s
133 133 i = blockstart / self.s
134 134 off = 0
135 135 for x in xrange(lend):
136 136 if self.index[i + x] == None:
137 137 b = data[off : off + self.s]
138 138 self.index[i + x] = b
139 139 n = b[self.shaoffset:self.shaoffset + 20]
140 140 self.map[n] = i + x
141 141 off += self.s
142 142
143 143 def findnode(self, node):
144 144 """search backwards through the index file for a specific node"""
145 145 if self.allmap: return None
146 146
147 147 # hg log will cause many many searches for the manifest
148 148 # nodes. After we get called a few times, just load the whole
149 149 # thing.
150 150 if self.mapfind_count > 8:
151 151 self.loadmap()
152 152 if node in self.map:
153 153 return node
154 154 return None
155 155 self.mapfind_count += 1
156 156 last = self.l - 1
157 157 while self.index[last] != None:
158 158 if last == 0:
159 159 self.all = 1
160 160 self.allmap = 1
161 161 return None
162 162 last -= 1
163 163 end = (last + 1) * self.s
164 164 blocksize = self.s * 256
165 165 while end >= 0:
166 166 start = max(end - blocksize, 0)
167 167 self.dataf.seek(start)
168 168 data = self.dataf.read(end - start)
169 169 findend = end - start
170 170 while True:
171 171 # we're searching backwards, so weh have to make sure
172 172 # we don't find a changeset where this node is a parent
173 173 off = data.rfind(node, 0, findend)
174 174 findend = off
175 175 if off >= 0:
176 176 i = off / self.s
177 177 off = i * self.s
178 178 n = data[off + self.shaoffset:off + self.shaoffset + 20]
179 179 if n == node:
180 180 self.map[n] = i + start / self.s
181 181 return node
182 182 else:
183 183 break
184 184 end -= blocksize
185 185 return None
186 186
187 187 def loadindex(self, i=None, end=None):
188 188 if self.all: return
189 189 all = False
190 190 if i == None:
191 191 blockstart = 0
192 192 blocksize = (512 / self.s) * self.s
193 193 end = self.datasize
194 194 all = True
195 195 else:
196 196 if end:
197 197 blockstart = i * self.s
198 198 end = end * self.s
199 199 blocksize = end - blockstart
200 200 else:
201 201 blockstart = (i & ~(32)) * self.s
202 202 blocksize = self.s * 64
203 203 end = blockstart + blocksize
204 204 while blockstart < end:
205 205 self.loadblock(blockstart, blocksize)
206 206 blockstart += blocksize
207 207 if all: self.all = True
208 208
209 209 class lazyindex(object):
210 210 """a lazy version of the index array"""
211 211 def __init__(self, parser):
212 212 self.p = parser
213 213 def __len__(self):
214 214 return len(self.p.index)
215 215 def load(self, pos):
216 216 if pos < 0:
217 217 pos += len(self.p.index)
218 218 self.p.loadindex(pos)
219 219 return self.p.index[pos]
220 220 def __getitem__(self, pos):
221 221 ret = self.p.index[pos] or self.load(pos)
222 222 if isinstance(ret, str):
223 223 ret = struct.unpack(self.p.indexformat, ret)
224 224 return ret
225 225 def __setitem__(self, pos, item):
226 226 self.p.index[pos] = item
227 227 def __delitem__(self, pos):
228 228 del self.p.index[pos]
229 229 def append(self, e):
230 230 self.p.index.append(e)
231 231
232 232 class lazymap(object):
233 233 """a lazy version of the node map"""
234 234 def __init__(self, parser):
235 235 self.p = parser
236 236 def load(self, key):
237 237 n = self.p.findnode(key)
238 238 if n == None:
239 239 raise KeyError(key)
240 240 def __contains__(self, key):
241 241 if key in self.p.map:
242 242 return True
243 243 self.p.loadmap()
244 244 return key in self.p.map
245 245 def __iter__(self):
246 246 yield nullid
247 247 for i in xrange(self.p.l):
248 248 ret = self.p.index[i]
249 249 if not ret:
250 250 self.p.loadindex(i)
251 251 ret = self.p.index[i]
252 252 if isinstance(ret, str):
253 253 ret = struct.unpack(self.p.indexformat, ret)
254 254 yield ret[-1]
255 255 def __getitem__(self, key):
256 256 try:
257 257 return self.p.map[key]
258 258 except KeyError:
259 259 try:
260 260 self.load(key)
261 261 return self.p.map[key]
262 262 except KeyError:
263 263 raise KeyError("node " + hex(key))
264 264 def __setitem__(self, key, val):
265 265 self.p.map[key] = val
266 266 def __delitem__(self, key):
267 267 del self.p.map[key]
268 268
269 269 class RevlogError(Exception): pass
270 270
271 271 class revlog(object):
272 272 """
273 273 the underlying revision storage object
274 274
275 275 A revlog consists of two parts, an index and the revision data.
276 276
277 277 The index is a file with a fixed record size containing
278 278 information on each revision, includings its nodeid (hash), the
279 279 nodeids of its parents, the position and offset of its data within
280 280 the data file, and the revision it's based on. Finally, each entry
281 281 contains a linkrev entry that can serve as a pointer to external
282 282 data.
283 283
284 284 The revision data itself is a linear collection of data chunks.
285 285 Each chunk represents a revision and is usually represented as a
286 286 delta against the previous chunk. To bound lookup time, runs of
287 287 deltas are limited to about 2 times the length of the original
288 288 version data. This makes retrieval of a version proportional to
289 289 its size, or O(1) relative to the number of revisions.
290 290
291 291 Both pieces of the revlog are written to in an append-only
292 292 fashion, which means we never need to rewrite a file to insert or
293 293 remove data, and can use some simple techniques to avoid the need
294 294 for locking while reading.
295 295 """
296 296 def __init__(self, opener, indexfile, datafile, defversion=0):
297 297 """
298 298 create a revlog object
299 299
300 300 opener is a function that abstracts the file opening operation
301 301 and can be used to implement COW semantics or the like.
302 302 """
303 303 self.indexfile = indexfile
304 304 self.datafile = datafile
305 305 self.opener = opener
306 306
307 307 self.indexstat = None
308 308 self.cache = None
309 309 self.chunkcache = None
310 310 self.defversion = defversion
311 311 self.load()
312 312
313 313 def load(self):
314 314 v = self.defversion
315 315 try:
316 316 f = self.opener(self.indexfile)
317 317 i = f.read(4)
318 318 f.seek(0)
319 319 except IOError, inst:
320 320 if inst.errno != errno.ENOENT:
321 321 raise
322 322 i = ""
323 323 else:
324 324 try:
325 st = os.fstat(f.fileno())
325 st = util.fstat(f)
326 326 except AttributeError, inst:
327 327 st = None
328 328 else:
329 329 oldst = self.indexstat
330 330 if (oldst and st.st_dev == oldst.st_dev
331 331 and st.st_ino == oldst.st_ino
332 332 and st.st_mtime == oldst.st_mtime
333 333 and st.st_ctime == oldst.st_ctime):
334 334 return
335 335 self.indexstat = st
336 336 if len(i) > 0:
337 337 v = struct.unpack(versionformat, i)[0]
338 338 flags = v & ~0xFFFF
339 339 fmt = v & 0xFFFF
340 340 if fmt == 0:
341 341 if flags:
342 342 raise RevlogError(_("index %s invalid flags %x for format v0" %
343 343 (self.indexfile, flags)))
344 344 elif fmt == REVLOGNG:
345 345 if flags & ~REVLOGNGINLINEDATA:
346 346 raise RevlogError(_("index %s invalid flags %x for revlogng" %
347 347 (self.indexfile, flags)))
348 348 else:
349 349 raise RevlogError(_("index %s invalid format %d" %
350 350 (self.indexfile, fmt)))
351 351 self.version = v
352 352 if v == 0:
353 353 self.indexformat = indexformatv0
354 354 shaoffset = v0shaoffset
355 355 else:
356 356 self.indexformat = indexformatng
357 357 shaoffset = ngshaoffset
358 358
359 359 if i:
360 360 if not self.inlinedata() and st and st.st_size > 10000:
361 361 # big index, let's parse it on demand
362 362 parser = lazyparser(f, st.st_size, self.indexformat, shaoffset)
363 363 self.index = lazyindex(parser)
364 364 self.nodemap = lazymap(parser)
365 365 else:
366 366 i = f.read()
367 367 self.parseindex(i)
368 368 if self.inlinedata():
369 369 # we've already got the entire data file read in, save it
370 370 # in the chunk data
371 371 self.chunkcache = (0, i)
372 372 if self.version != 0:
373 373 e = list(self.index[0])
374 374 type = self.ngtype(e[0])
375 375 e[0] = self.offset_type(0, type)
376 376 self.index[0] = e
377 377 else:
378 378 self.nodemap = { nullid: -1}
379 379 self.index = []
380 380
381 381
382 382 def parseindex(self, data):
383 383 s = struct.calcsize(self.indexformat)
384 384 l = len(data)
385 385 self.index = []
386 386 self.nodemap = {nullid: -1}
387 387 inline = self.inlinedata()
388 388 off = 0
389 389 n = 0
390 390 while off < l:
391 391 e = struct.unpack(self.indexformat, data[off:off + s])
392 392 self.index.append(e)
393 393 self.nodemap[e[-1]] = n
394 394 n += 1
395 395 off += s
396 396 if inline:
397 397 off += e[1]
398 398
399 399 def ngoffset(self, q):
400 400 if q & 0xFFFF:
401 401 raise RevlogError(_('%s: incompatible revision flag %x') %
402 402 (self.indexfile, type))
403 403 return long(q >> 16)
404 404
405 405 def ngtype(self, q):
406 406 return int(q & 0xFFFF)
407 407
408 408 def offset_type(self, offset, type):
409 409 return long(long(offset) << 16 | type)
410 410
411 411 def loadindex(self, start, end):
412 412 """load a block of indexes all at once from the lazy parser"""
413 413 if isinstance(self.index, lazyindex):
414 414 self.index.p.loadindex(start, end)
415 415
416 416 def loadindexmap(self):
417 417 """loads both the map and the index from the lazy parser"""
418 418 if isinstance(self.index, lazyindex):
419 419 p = self.index.p
420 420 p.loadindex()
421 421 self.nodemap = p.map
422 422
423 423 def loadmap(self):
424 424 """loads the map from the lazy parser"""
425 425 if isinstance(self.nodemap, lazymap):
426 426 self.nodemap.p.loadmap()
427 427 self.nodemap = self.nodemap.p.map
428 428
429 429 def inlinedata(self): return self.version & REVLOGNGINLINEDATA
430 430 def tip(self): return self.node(len(self.index) - 1)
431 431 def count(self): return len(self.index)
432 432 def node(self, rev):
433 433 return (rev < 0) and nullid or self.index[rev][-1]
434 434 def rev(self, node):
435 435 try:
436 436 return self.nodemap[node]
437 437 except KeyError:
438 438 raise RevlogError(_('%s: no node %s') % (self.indexfile, hex(node)))
439 439 def linkrev(self, node): return self.index[self.rev(node)][-4]
440 440 def parents(self, node):
441 441 if node == nullid: return (nullid, nullid)
442 442 r = self.rev(node)
443 443 d = self.index[r][-3:-1]
444 444 if self.version == 0:
445 445 return d
446 446 return [ self.node(x) for x in d ]
447 447 def start(self, rev):
448 448 if rev < 0:
449 449 return -1
450 450 if self.version != 0:
451 451 return self.ngoffset(self.index[rev][0])
452 452 return self.index[rev][0]
453 453
454 454 def end(self, rev): return self.start(rev) + self.length(rev)
455 455
456 456 def size(self, rev):
457 457 """return the length of the uncompressed text for a given revision"""
458 458 l = -1
459 459 if self.version != 0:
460 460 l = self.index[rev][2]
461 461 if l >= 0:
462 462 return l
463 463
464 464 t = self.revision(self.node(rev))
465 465 return len(t)
466 466
467 467 # alternate implementation, The advantage to this code is it
468 468 # will be faster for a single revision. But, the results are not
469 469 # cached, so finding the size of every revision will be slower.
470 470 """
471 471 if self.cache and self.cache[1] == rev:
472 472 return len(self.cache[2])
473 473
474 474 base = self.base(rev)
475 475 if self.cache and self.cache[1] >= base and self.cache[1] < rev:
476 476 base = self.cache[1]
477 477 text = self.cache[2]
478 478 else:
479 479 text = self.revision(self.node(base))
480 480
481 481 l = len(text)
482 482 for x in xrange(base + 1, rev + 1):
483 483 l = mdiff.patchedsize(l, self.chunk(x))
484 484 return l
485 485 """
486 486
487 487 def length(self, rev):
488 488 if rev < 0:
489 489 return 0
490 490 else:
491 491 return self.index[rev][1]
492 492 def base(self, rev): return (rev < 0) and rev or self.index[rev][-5]
493 493
494 494 def reachable(self, rev, stop=None):
495 495 reachable = {}
496 496 visit = [rev]
497 497 reachable[rev] = 1
498 498 if stop:
499 499 stopn = self.rev(stop)
500 500 else:
501 501 stopn = 0
502 502 while visit:
503 503 n = visit.pop(0)
504 504 if n == stop:
505 505 continue
506 506 if n == nullid:
507 507 continue
508 508 for p in self.parents(n):
509 509 if self.rev(p) < stopn:
510 510 continue
511 511 if p not in reachable:
512 512 reachable[p] = 1
513 513 visit.append(p)
514 514 return reachable
515 515
516 516 def nodesbetween(self, roots=None, heads=None):
517 517 """Return a tuple containing three elements. Elements 1 and 2 contain
518 518 a final list bases and heads after all the unreachable ones have been
519 519 pruned. Element 0 contains a topologically sorted list of all
520 520
521 521 nodes that satisfy these constraints:
522 522 1. All nodes must be descended from a node in roots (the nodes on
523 523 roots are considered descended from themselves).
524 524 2. All nodes must also be ancestors of a node in heads (the nodes in
525 525 heads are considered to be their own ancestors).
526 526
527 527 If roots is unspecified, nullid is assumed as the only root.
528 528 If heads is unspecified, it is taken to be the output of the
529 529 heads method (i.e. a list of all nodes in the repository that
530 530 have no children)."""
531 531 nonodes = ([], [], [])
532 532 if roots is not None:
533 533 roots = list(roots)
534 534 if not roots:
535 535 return nonodes
536 536 lowestrev = min([self.rev(n) for n in roots])
537 537 else:
538 538 roots = [nullid] # Everybody's a descendent of nullid
539 539 lowestrev = -1
540 540 if (lowestrev == -1) and (heads is None):
541 541 # We want _all_ the nodes!
542 542 return ([self.node(r) for r in xrange(0, self.count())],
543 543 [nullid], list(self.heads()))
544 544 if heads is None:
545 545 # All nodes are ancestors, so the latest ancestor is the last
546 546 # node.
547 547 highestrev = self.count() - 1
548 548 # Set ancestors to None to signal that every node is an ancestor.
549 549 ancestors = None
550 550 # Set heads to an empty dictionary for later discovery of heads
551 551 heads = {}
552 552 else:
553 553 heads = list(heads)
554 554 if not heads:
555 555 return nonodes
556 556 ancestors = {}
557 557 # Start at the top and keep marking parents until we're done.
558 558 nodestotag = heads[:]
559 559 # Turn heads into a dictionary so we can remove 'fake' heads.
560 560 # Also, later we will be using it to filter out the heads we can't
561 561 # find from roots.
562 562 heads = dict.fromkeys(heads, 0)
563 563 # Remember where the top was so we can use it as a limit later.
564 564 highestrev = max([self.rev(n) for n in nodestotag])
565 565 while nodestotag:
566 566 # grab a node to tag
567 567 n = nodestotag.pop()
568 568 # Never tag nullid
569 569 if n == nullid:
570 570 continue
571 571 # A node's revision number represents its place in a
572 572 # topologically sorted list of nodes.
573 573 r = self.rev(n)
574 574 if r >= lowestrev:
575 575 if n not in ancestors:
576 576 # If we are possibly a descendent of one of the roots
577 577 # and we haven't already been marked as an ancestor
578 578 ancestors[n] = 1 # Mark as ancestor
579 579 # Add non-nullid parents to list of nodes to tag.
580 580 nodestotag.extend([p for p in self.parents(n) if
581 581 p != nullid])
582 582 elif n in heads: # We've seen it before, is it a fake head?
583 583 # So it is, real heads should not be the ancestors of
584 584 # any other heads.
585 585 heads.pop(n)
586 586 if not ancestors:
587 587 return nonodes
588 588 # Now that we have our set of ancestors, we want to remove any
589 589 # roots that are not ancestors.
590 590
591 591 # If one of the roots was nullid, everything is included anyway.
592 592 if lowestrev > -1:
593 593 # But, since we weren't, let's recompute the lowest rev to not
594 594 # include roots that aren't ancestors.
595 595
596 596 # Filter out roots that aren't ancestors of heads
597 597 roots = [n for n in roots if n in ancestors]
598 598 # Recompute the lowest revision
599 599 if roots:
600 600 lowestrev = min([self.rev(n) for n in roots])
601 601 else:
602 602 # No more roots? Return empty list
603 603 return nonodes
604 604 else:
605 605 # We are descending from nullid, and don't need to care about
606 606 # any other roots.
607 607 lowestrev = -1
608 608 roots = [nullid]
609 609 # Transform our roots list into a 'set' (i.e. a dictionary where the
610 610 # values don't matter.
611 611 descendents = dict.fromkeys(roots, 1)
612 612 # Also, keep the original roots so we can filter out roots that aren't
613 613 # 'real' roots (i.e. are descended from other roots).
614 614 roots = descendents.copy()
615 615 # Our topologically sorted list of output nodes.
616 616 orderedout = []
617 617 # Don't start at nullid since we don't want nullid in our output list,
618 618 # and if nullid shows up in descedents, empty parents will look like
619 619 # they're descendents.
620 620 for r in xrange(max(lowestrev, 0), highestrev + 1):
621 621 n = self.node(r)
622 622 isdescendent = False
623 623 if lowestrev == -1: # Everybody is a descendent of nullid
624 624 isdescendent = True
625 625 elif n in descendents:
626 626 # n is already a descendent
627 627 isdescendent = True
628 628 # This check only needs to be done here because all the roots
629 629 # will start being marked is descendents before the loop.
630 630 if n in roots:
631 631 # If n was a root, check if it's a 'real' root.
632 632 p = tuple(self.parents(n))
633 633 # If any of its parents are descendents, it's not a root.
634 634 if (p[0] in descendents) or (p[1] in descendents):
635 635 roots.pop(n)
636 636 else:
637 637 p = tuple(self.parents(n))
638 638 # A node is a descendent if either of its parents are
639 639 # descendents. (We seeded the dependents list with the roots
640 640 # up there, remember?)
641 641 if (p[0] in descendents) or (p[1] in descendents):
642 642 descendents[n] = 1
643 643 isdescendent = True
644 644 if isdescendent and ((ancestors is None) or (n in ancestors)):
645 645 # Only include nodes that are both descendents and ancestors.
646 646 orderedout.append(n)
647 647 if (ancestors is not None) and (n in heads):
648 648 # We're trying to figure out which heads are reachable
649 649 # from roots.
650 650 # Mark this head as having been reached
651 651 heads[n] = 1
652 652 elif ancestors is None:
653 653 # Otherwise, we're trying to discover the heads.
654 654 # Assume this is a head because if it isn't, the next step
655 655 # will eventually remove it.
656 656 heads[n] = 1
657 657 # But, obviously its parents aren't.
658 658 for p in self.parents(n):
659 659 heads.pop(p, None)
660 660 heads = [n for n in heads.iterkeys() if heads[n] != 0]
661 661 roots = roots.keys()
662 662 assert orderedout
663 663 assert roots
664 664 assert heads
665 665 return (orderedout, roots, heads)
666 666
667 667 def heads(self, start=None):
668 668 """return the list of all nodes that have no children
669 669
670 670 if start is specified, only heads that are descendants of
671 671 start will be returned
672 672
673 673 """
674 674 if start is None:
675 675 start = nullid
676 676 reachable = {start: 1}
677 677 heads = {start: 1}
678 678 startrev = self.rev(start)
679 679
680 680 for r in xrange(startrev + 1, self.count()):
681 681 n = self.node(r)
682 682 for pn in self.parents(n):
683 683 if pn in reachable:
684 684 reachable[n] = 1
685 685 heads[n] = 1
686 686 if pn in heads:
687 687 del heads[pn]
688 688 return heads.keys()
689 689
690 690 def children(self, node):
691 691 """find the children of a given node"""
692 692 c = []
693 693 p = self.rev(node)
694 694 for r in range(p + 1, self.count()):
695 695 n = self.node(r)
696 696 for pn in self.parents(n):
697 697 if pn == node:
698 698 c.append(n)
699 699 continue
700 700 elif pn == nullid:
701 701 continue
702 702 return c
703 703
704 704 def lookup(self, id):
705 705 """locate a node based on revision number or subset of hex nodeid"""
706 706 try:
707 707 rev = int(id)
708 708 if str(rev) != id: raise ValueError
709 709 if rev < 0: rev = self.count() + rev
710 710 if rev < 0 or rev >= self.count(): raise ValueError
711 711 return self.node(rev)
712 712 except (ValueError, OverflowError):
713 713 c = []
714 714 for n in self.nodemap:
715 715 if hex(n).startswith(id):
716 716 c.append(n)
717 717 if len(c) > 1: raise RevlogError(_("Ambiguous identifier"))
718 718 if len(c) < 1: raise RevlogError(_("No match found"))
719 719 return c[0]
720 720
721 721 return None
722 722
723 723 def diff(self, a, b):
724 724 """return a delta between two revisions"""
725 725 return mdiff.textdiff(a, b)
726 726
727 727 def patches(self, t, pl):
728 728 """apply a list of patches to a string"""
729 729 return mdiff.patches(t, pl)
730 730
731 731 def chunk(self, rev, df=None, cachelen=4096):
732 732 start, length = self.start(rev), self.length(rev)
733 733 inline = self.inlinedata()
734 734 if inline:
735 735 start += (rev + 1) * struct.calcsize(self.indexformat)
736 736 end = start + length
737 737 def loadcache(df):
738 738 cache_length = max(cachelen, length) # 4k
739 739 if not df:
740 740 if inline:
741 741 df = self.opener(self.indexfile)
742 742 else:
743 743 df = self.opener(self.datafile)
744 744 df.seek(start)
745 745 self.chunkcache = (start, df.read(cache_length))
746 746
747 747 if not self.chunkcache:
748 748 loadcache(df)
749 749
750 750 cache_start = self.chunkcache[0]
751 751 cache_end = cache_start + len(self.chunkcache[1])
752 752 if start >= cache_start and end <= cache_end:
753 753 # it is cached
754 754 offset = start - cache_start
755 755 else:
756 756 loadcache(df)
757 757 offset = 0
758 758
759 759 #def checkchunk():
760 760 # df = self.opener(self.datafile)
761 761 # df.seek(start)
762 762 # return df.read(length)
763 763 #assert s == checkchunk()
764 764 return decompress(self.chunkcache[1][offset:offset + length])
765 765
766 766 def delta(self, node):
767 767 """return or calculate a delta between a node and its predecessor"""
768 768 r = self.rev(node)
769 769 return self.revdiff(r - 1, r)
770 770
771 771 def revdiff(self, rev1, rev2):
772 772 """return or calculate a delta between two revisions"""
773 773 b1 = self.base(rev1)
774 774 b2 = self.base(rev2)
775 775 if b1 == b2 and rev1 + 1 == rev2:
776 776 return self.chunk(rev2)
777 777 else:
778 778 return self.diff(self.revision(self.node(rev1)),
779 779 self.revision(self.node(rev2)))
780 780
781 781 def revision(self, node):
782 782 """return an uncompressed revision of a given"""
783 783 if node == nullid: return ""
784 784 if self.cache and self.cache[0] == node: return self.cache[2]
785 785
786 786 # look up what we need to read
787 787 text = None
788 788 rev = self.rev(node)
789 789 base = self.base(rev)
790 790
791 791 if self.inlinedata():
792 792 # we probably have the whole chunk cached
793 793 df = None
794 794 else:
795 795 df = self.opener(self.datafile)
796 796
797 797 # do we have useful data cached?
798 798 if self.cache and self.cache[1] >= base and self.cache[1] < rev:
799 799 base = self.cache[1]
800 800 text = self.cache[2]
801 801 self.loadindex(base, rev + 1)
802 802 else:
803 803 self.loadindex(base, rev + 1)
804 804 text = self.chunk(base, df=df)
805 805
806 806 bins = []
807 807 for r in xrange(base + 1, rev + 1):
808 808 bins.append(self.chunk(r, df=df))
809 809
810 810 text = self.patches(text, bins)
811 811
812 812 p1, p2 = self.parents(node)
813 813 if node != hash(text, p1, p2):
814 814 raise RevlogError(_("integrity check failed on %s:%d")
815 815 % (self.datafile, rev))
816 816
817 817 self.cache = (node, rev, text)
818 818 return text
819 819
820 820 def checkinlinesize(self, tr, fp=None):
821 821 if not self.inlinedata():
822 822 return
823 823 if not fp:
824 824 fp = self.opener(self.indexfile, 'r')
825 825 fp.seek(0, 2)
826 826 size = fp.tell()
827 827 if size < 131072:
828 828 return
829 829 trinfo = tr.find(self.indexfile)
830 830 if trinfo == None:
831 831 raise RevlogError(_("%s not found in the transaction" %
832 832 self.indexfile))
833 833
834 834 trindex = trinfo[2]
835 835 dataoff = self.start(trindex)
836 836
837 837 tr.add(self.datafile, dataoff)
838 838 df = self.opener(self.datafile, 'w')
839 839 calc = struct.calcsize(self.indexformat)
840 840 for r in xrange(self.count()):
841 841 start = self.start(r) + (r + 1) * calc
842 842 length = self.length(r)
843 843 fp.seek(start)
844 844 d = fp.read(length)
845 845 df.write(d)
846 846 fp.close()
847 847 df.close()
848 848 fp = self.opener(self.indexfile, 'w', atomictemp=True)
849 849 self.version &= ~(REVLOGNGINLINEDATA)
850 850 if self.count():
851 851 x = self.index[0]
852 852 e = struct.pack(self.indexformat, *x)[4:]
853 853 l = struct.pack(versionformat, self.version)
854 854 fp.write(l)
855 855 fp.write(e)
856 856
857 857 for i in xrange(1, self.count()):
858 858 x = self.index[i]
859 859 e = struct.pack(self.indexformat, *x)
860 860 fp.write(e)
861 861
862 862 # if we don't call rename, the temp file will never replace the
863 863 # real index
864 864 fp.rename()
865 865
866 866 tr.replace(self.indexfile, trindex * calc)
867 867 self.chunkcache = None
868 868
869 869 def addrevision(self, text, transaction, link, p1=None, p2=None, d=None):
870 870 """add a revision to the log
871 871
872 872 text - the revision data to add
873 873 transaction - the transaction object used for rollback
874 874 link - the linkrev data to add
875 875 p1, p2 - the parent nodeids of the revision
876 876 d - an optional precomputed delta
877 877 """
878 878 if text is None: text = ""
879 879 if p1 is None: p1 = self.tip()
880 880 if p2 is None: p2 = nullid
881 881
882 882 node = hash(text, p1, p2)
883 883
884 884 if node in self.nodemap:
885 885 return node
886 886
887 887 n = self.count()
888 888 t = n - 1
889 889
890 890 if n:
891 891 base = self.base(t)
892 892 start = self.start(base)
893 893 end = self.end(t)
894 894 if not d:
895 895 prev = self.revision(self.tip())
896 896 d = self.diff(prev, str(text))
897 897 data = compress(d)
898 898 l = len(data[1]) + len(data[0])
899 899 dist = end - start + l
900 900
901 901 # full versions are inserted when the needed deltas
902 902 # become comparable to the uncompressed text
903 903 if not n or dist > len(text) * 2:
904 904 data = compress(text)
905 905 l = len(data[1]) + len(data[0])
906 906 base = n
907 907 else:
908 908 base = self.base(t)
909 909
910 910 offset = 0
911 911 if t >= 0:
912 912 offset = self.end(t)
913 913
914 914 if self.version == 0:
915 915 e = (offset, l, base, link, p1, p2, node)
916 916 else:
917 917 e = (self.offset_type(offset, 0), l, len(text),
918 918 base, link, self.rev(p1), self.rev(p2), node)
919 919
920 920 self.index.append(e)
921 921 self.nodemap[node] = n
922 922 entry = struct.pack(self.indexformat, *e)
923 923
924 924 if not self.inlinedata():
925 925 transaction.add(self.datafile, offset)
926 926 transaction.add(self.indexfile, n * len(entry))
927 927 f = self.opener(self.datafile, "a")
928 928 if data[0]:
929 929 f.write(data[0])
930 930 f.write(data[1])
931 931 f.close()
932 932 f = self.opener(self.indexfile, "a")
933 933 else:
934 934 f = self.opener(self.indexfile, "a+")
935 935 f.seek(0, 2)
936 936 transaction.add(self.indexfile, f.tell(), self.count() - 1)
937 937
938 938 if len(self.index) == 1 and self.version != 0:
939 939 l = struct.pack(versionformat, self.version)
940 940 f.write(l)
941 941 entry = entry[4:]
942 942
943 943 f.write(entry)
944 944
945 945 if self.inlinedata():
946 946 f.write(data[0])
947 947 f.write(data[1])
948 948 self.checkinlinesize(transaction, f)
949 949
950 950 self.cache = (node, n, text)
951 951 return node
952 952
953 953 def ancestor(self, a, b):
954 954 """calculate the least common ancestor of nodes a and b"""
955 955
956 956 # start with some short cuts for the linear cases
957 957 if a == b:
958 958 return a
959 959 ra = self.rev(a)
960 960 rb = self.rev(b)
961 961 if ra < rb:
962 962 last = b
963 963 first = a
964 964 else:
965 965 last = a
966 966 first = b
967 967
968 968 # reachable won't include stop in the list, so we have to use a parent
969 969 reachable = self.reachable(last, stop=self.parents(first)[0])
970 970 if first in reachable:
971 971 return first
972 972
973 973 # calculate the distance of every node from root
974 974 dist = {nullid: 0}
975 975 for i in xrange(self.count()):
976 976 n = self.node(i)
977 977 p1, p2 = self.parents(n)
978 978 dist[n] = max(dist[p1], dist[p2]) + 1
979 979
980 980 # traverse ancestors in order of decreasing distance from root
981 981 def ancestors(node):
982 982 # we store negative distances because heap returns smallest member
983 983 h = [(-dist[node], node)]
984 984 seen = {}
985 985 while h:
986 986 d, n = heapq.heappop(h)
987 987 if n not in seen:
988 988 seen[n] = 1
989 989 yield (-d, n)
990 990 for p in self.parents(n):
991 991 heapq.heappush(h, (-dist[p], p))
992 992
993 993 def generations(node):
994 994 sg, s = None, {}
995 995 for g,n in ancestors(node):
996 996 if g != sg:
997 997 if sg:
998 998 yield sg, s
999 999 sg, s = g, {n:1}
1000 1000 else:
1001 1001 s[n] = 1
1002 1002 yield sg, s
1003 1003
1004 1004 x = generations(a)
1005 1005 y = generations(b)
1006 1006 gx = x.next()
1007 1007 gy = y.next()
1008 1008
1009 1009 # increment each ancestor list until it is closer to root than
1010 1010 # the other, or they match
1011 1011 while 1:
1012 1012 #print "ancestor gen %s %s" % (gx[0], gy[0])
1013 1013 if gx[0] == gy[0]:
1014 1014 # find the intersection
1015 1015 i = [ n for n in gx[1] if n in gy[1] ]
1016 1016 if i:
1017 1017 return i[0]
1018 1018 else:
1019 1019 #print "next"
1020 1020 gy = y.next()
1021 1021 gx = x.next()
1022 1022 elif gx[0] < gy[0]:
1023 1023 #print "next y"
1024 1024 gy = y.next()
1025 1025 else:
1026 1026 #print "next x"
1027 1027 gx = x.next()
1028 1028
1029 1029 def group(self, nodelist, lookup, infocollect=None):
1030 1030 """calculate a delta group
1031 1031
1032 1032 Given a list of changeset revs, return a set of deltas and
1033 1033 metadata corresponding to nodes. the first delta is
1034 1034 parent(nodes[0]) -> nodes[0] the receiver is guaranteed to
1035 1035 have this parent as it has all history before these
1036 1036 changesets. parent is parent[0]
1037 1037 """
1038 1038 revs = [self.rev(n) for n in nodelist]
1039 1039
1040 1040 # if we don't have any revisions touched by these changesets, bail
1041 1041 if not revs:
1042 1042 yield changegroup.closechunk()
1043 1043 return
1044 1044
1045 1045 # add the parent of the first rev
1046 1046 p = self.parents(self.node(revs[0]))[0]
1047 1047 revs.insert(0, self.rev(p))
1048 1048
1049 1049 # build deltas
1050 1050 for d in xrange(0, len(revs) - 1):
1051 1051 a, b = revs[d], revs[d + 1]
1052 1052 nb = self.node(b)
1053 1053
1054 1054 if infocollect is not None:
1055 1055 infocollect(nb)
1056 1056
1057 1057 d = self.revdiff(a, b)
1058 1058 p = self.parents(nb)
1059 1059 meta = nb + p[0] + p[1] + lookup(nb)
1060 1060 yield changegroup.genchunk("%s%s" % (meta, d))
1061 1061
1062 1062 yield changegroup.closechunk()
1063 1063
1064 1064 def addgroup(self, revs, linkmapper, transaction, unique=0):
1065 1065 """
1066 1066 add a delta group
1067 1067
1068 1068 given a set of deltas, add them to the revision log. the
1069 1069 first delta is against its parent, which should be in our
1070 1070 log, the rest are against the previous delta.
1071 1071 """
1072 1072
1073 1073 #track the base of the current delta log
1074 1074 r = self.count()
1075 1075 t = r - 1
1076 1076 node = None
1077 1077
1078 1078 base = prev = -1
1079 1079 start = end = textlen = 0
1080 1080 if r:
1081 1081 end = self.end(t)
1082 1082
1083 1083 ifh = self.opener(self.indexfile, "a+")
1084 1084 ifh.seek(0, 2)
1085 1085 transaction.add(self.indexfile, ifh.tell(), self.count())
1086 1086 if self.inlinedata():
1087 1087 dfh = None
1088 1088 else:
1089 1089 transaction.add(self.datafile, end)
1090 1090 dfh = self.opener(self.datafile, "a")
1091 1091
1092 1092 # loop through our set of deltas
1093 1093 chain = None
1094 1094 for chunk in revs:
1095 1095 node, p1, p2, cs = struct.unpack("20s20s20s20s", chunk[:80])
1096 1096 link = linkmapper(cs)
1097 1097 if node in self.nodemap:
1098 1098 # this can happen if two branches make the same change
1099 1099 # if unique:
1100 1100 # raise RevlogError(_("already have %s") % hex(node[:4]))
1101 1101 chain = node
1102 1102 continue
1103 1103 delta = chunk[80:]
1104 1104
1105 1105 for p in (p1, p2):
1106 1106 if not p in self.nodemap:
1107 1107 raise RevlogError(_("unknown parent %s") % short(p1))
1108 1108
1109 1109 if not chain:
1110 1110 # retrieve the parent revision of the delta chain
1111 1111 chain = p1
1112 1112 if not chain in self.nodemap:
1113 1113 raise RevlogError(_("unknown base %s") % short(chain[:4]))
1114 1114
1115 1115 # full versions are inserted when the needed deltas become
1116 1116 # comparable to the uncompressed text or when the previous
1117 1117 # version is not the one we have a delta against. We use
1118 1118 # the size of the previous full rev as a proxy for the
1119 1119 # current size.
1120 1120
1121 1121 if chain == prev:
1122 1122 tempd = compress(delta)
1123 1123 cdelta = tempd[0] + tempd[1]
1124 1124 textlen = mdiff.patchedsize(textlen, delta)
1125 1125
1126 1126 if chain != prev or (end - start + len(cdelta)) > textlen * 2:
1127 1127 # flush our writes here so we can read it in revision
1128 1128 if dfh:
1129 1129 dfh.flush()
1130 1130 ifh.flush()
1131 1131 text = self.revision(chain)
1132 1132 text = self.patches(text, [delta])
1133 1133 chk = self.addrevision(text, transaction, link, p1, p2)
1134 1134 if chk != node:
1135 1135 raise RevlogError(_("consistency error adding group"))
1136 1136 textlen = len(text)
1137 1137 else:
1138 1138 if self.version == 0:
1139 1139 e = (end, len(cdelta), base, link, p1, p2, node)
1140 1140 else:
1141 1141 e = (self.offset_type(end, 0), len(cdelta), textlen, base,
1142 1142 link, self.rev(p1), self.rev(p2), node)
1143 1143 self.index.append(e)
1144 1144 self.nodemap[node] = r
1145 1145 if self.inlinedata():
1146 1146 ifh.write(struct.pack(self.indexformat, *e))
1147 1147 ifh.write(cdelta)
1148 1148 self.checkinlinesize(transaction, ifh)
1149 1149 if not self.inlinedata():
1150 1150 dfh = self.opener(self.datafile, "a")
1151 1151 ifh = self.opener(self.indexfile, "a")
1152 1152 else:
1153 1153 if not dfh:
1154 1154 # addrevision switched from inline to conventional
1155 1155 # reopen the index
1156 1156 dfh = self.opener(self.datafile, "a")
1157 1157 ifh = self.opener(self.indexfile, "a")
1158 1158 dfh.write(cdelta)
1159 1159 ifh.write(struct.pack(self.indexformat, *e))
1160 1160
1161 1161 t, r, chain, prev = r, r + 1, node, node
1162 1162 base = self.base(t)
1163 1163 start = self.start(base)
1164 1164 end = self.end(t)
1165 1165
1166 1166 if node is None:
1167 1167 raise RevlogError(_("group to be added is empty"))
1168 1168 return node
1169 1169
1170 1170 def strip(self, rev, minlink):
1171 1171 if self.count() == 0 or rev >= self.count():
1172 1172 return
1173 1173
1174 1174 if isinstance(self.index, lazyindex):
1175 1175 self.loadindexmap()
1176 1176
1177 1177 # When stripping away a revision, we need to make sure it
1178 1178 # does not actually belong to an older changeset.
1179 1179 # The minlink parameter defines the oldest revision
1180 1180 # we're allowed to strip away.
1181 1181 while minlink > self.index[rev][-4]:
1182 1182 rev += 1
1183 1183 if rev >= self.count():
1184 1184 return
1185 1185
1186 1186 # first truncate the files on disk
1187 1187 end = self.start(rev)
1188 1188 if not self.inlinedata():
1189 1189 df = self.opener(self.datafile, "a")
1190 1190 df.truncate(end)
1191 1191 end = rev * struct.calcsize(self.indexformat)
1192 1192 else:
1193 1193 end += rev * struct.calcsize(self.indexformat)
1194 1194
1195 1195 indexf = self.opener(self.indexfile, "a")
1196 1196 indexf.truncate(end)
1197 1197
1198 1198 # then reset internal state in memory to forget those revisions
1199 1199 self.cache = None
1200 1200 self.chunkcache = None
1201 1201 for x in xrange(rev, self.count()):
1202 1202 del self.nodemap[self.node(x)]
1203 1203
1204 1204 del self.index[rev:]
1205 1205
1206 1206 def checksize(self):
1207 1207 expected = 0
1208 1208 if self.count():
1209 1209 expected = self.end(self.count() - 1)
1210 1210
1211 1211 try:
1212 1212 f = self.opener(self.datafile)
1213 1213 f.seek(0, 2)
1214 1214 actual = f.tell()
1215 1215 dd = actual - expected
1216 1216 except IOError, inst:
1217 1217 if inst.errno != errno.ENOENT:
1218 1218 raise
1219 1219 dd = 0
1220 1220
1221 1221 try:
1222 1222 f = self.opener(self.indexfile)
1223 1223 f.seek(0, 2)
1224 1224 actual = f.tell()
1225 1225 s = struct.calcsize(self.indexformat)
1226 1226 i = actual / s
1227 1227 di = actual - (i * s)
1228 1228 if self.inlinedata():
1229 1229 databytes = 0
1230 1230 for r in xrange(self.count()):
1231 1231 databytes += self.length(r)
1232 1232 dd = 0
1233 1233 di = actual - self.count() * s - databytes
1234 1234 except IOError, inst:
1235 1235 if inst.errno != errno.ENOENT:
1236 1236 raise
1237 1237 di = 0
1238 1238
1239 1239 return (dd, di)
1240 1240
1241 1241
@@ -1,155 +1,155 b''
1 1 # sshrepo.py - ssh repository proxy class for mercurial
2 2 #
3 3 # Copyright 2005 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 node import *
9 9 from remoterepo import *
10 10 from i18n import gettext as _
11 11 from demandload import *
12 12 demandload(globals(), "hg os re stat")
13 13
14 14 class sshrepository(remoterepository):
15 15 def __init__(self, ui, path):
16 16 self.url = path
17 17 self.ui = ui
18 18
19 19 m = re.match(r'ssh://(([^@]+)@)?([^:/]+)(:(\d+))?(/(.*))?', path)
20 20 if not m:
21 21 raise hg.RepoError(_("couldn't parse destination %s") % path)
22 22
23 23 self.user = m.group(2)
24 24 self.host = m.group(3)
25 25 self.port = m.group(5)
26 26 self.path = m.group(7) or "."
27 27
28 28 args = self.user and ("%s@%s" % (self.user, self.host)) or self.host
29 29 args = self.port and ("%s -p %s") % (args, self.port) or args
30 30
31 31 sshcmd = self.ui.config("ui", "ssh", "ssh")
32 32 remotecmd = self.ui.config("ui", "remotecmd", "hg")
33 33 cmd = '%s %s "%s -R %s serve --stdio"'
34 34 cmd = cmd % (sshcmd, args, remotecmd, self.path)
35 35
36 36 ui.note('running %s\n' % cmd)
37 37 self.pipeo, self.pipei, self.pipee = os.popen3(cmd, 'b')
38 38
39 39 # skip any noise generated by remote shell
40 40 r = self.do_cmd("between", pairs=("%s-%s" % ("0"*40, "0"*40)))
41 41 l1 = ""
42 42 l2 = "dummy"
43 43 max_noise = 500
44 44 while l2 and max_noise:
45 45 l2 = r.readline()
46 46 self.readerr()
47 47 if l1 == "1\n" and l2 == "\n":
48 48 break
49 49 if l1:
50 50 ui.debug(_("remote: "), l1)
51 51 l1 = l2
52 52 max_noise -= 1
53 53 else:
54 54 if l1:
55 55 ui.debug(_("remote: "), l1)
56 56 raise hg.RepoError(_("no response from remote hg"))
57 57
58 58 def readerr(self):
59 59 while 1:
60 size = os.fstat(self.pipee.fileno())[stat.ST_SIZE]
60 size = util.fstat(self.pipee).st_size
61 61 if size == 0: break
62 62 l = self.pipee.readline()
63 63 if not l: break
64 64 self.ui.status(_("remote: "), l)
65 65
66 66 def __del__(self):
67 67 try:
68 68 self.pipeo.close()
69 69 self.pipei.close()
70 70 # read the error descriptor until EOF
71 71 for l in self.pipee:
72 72 self.ui.status(_("remote: "), l)
73 73 self.pipee.close()
74 74 except:
75 75 pass
76 76
77 77 def dev(self):
78 78 return -1
79 79
80 80 def do_cmd(self, cmd, **args):
81 81 self.ui.debug(_("sending %s command\n") % cmd)
82 82 self.pipeo.write("%s\n" % cmd)
83 83 for k, v in args.items():
84 84 self.pipeo.write("%s %d\n" % (k, len(v)))
85 85 self.pipeo.write(v)
86 86 self.pipeo.flush()
87 87
88 88 return self.pipei
89 89
90 90 def call(self, cmd, **args):
91 91 r = self.do_cmd(cmd, **args)
92 92 l = r.readline()
93 93 self.readerr()
94 94 try:
95 95 l = int(l)
96 96 except:
97 97 raise hg.RepoError(_("unexpected response '%s'") % l)
98 98 return r.read(l)
99 99
100 100 def lock(self):
101 101 self.call("lock")
102 102 return remotelock(self)
103 103
104 104 def unlock(self):
105 105 self.call("unlock")
106 106
107 107 def heads(self):
108 108 d = self.call("heads")
109 109 try:
110 110 return map(bin, d[:-1].split(" "))
111 111 except:
112 112 raise hg.RepoError(_("unexpected response '%s'") % (d[:400] + "..."))
113 113
114 114 def branches(self, nodes):
115 115 n = " ".join(map(hex, nodes))
116 116 d = self.call("branches", nodes=n)
117 117 try:
118 118 br = [ tuple(map(bin, b.split(" "))) for b in d.splitlines() ]
119 119 return br
120 120 except:
121 121 raise hg.RepoError(_("unexpected response '%s'") % (d[:400] + "..."))
122 122
123 123 def between(self, pairs):
124 124 n = "\n".join(["-".join(map(hex, p)) for p in pairs])
125 125 d = self.call("between", pairs=n)
126 126 try:
127 127 p = [ l and map(bin, l.split(" ")) or [] for l in d.splitlines() ]
128 128 return p
129 129 except:
130 130 raise hg.RepoError(_("unexpected response '%s'") % (d[:400] + "..."))
131 131
132 132 def changegroup(self, nodes, kind):
133 133 n = " ".join(map(hex, nodes))
134 134 f = self.do_cmd("changegroup", roots=n)
135 135 return self.pipei
136 136
137 137 def addchangegroup(self, cg):
138 138 d = self.call("addchangegroup")
139 139 if d:
140 140 raise hg.RepoError(_("push refused: %s"), d)
141 141
142 142 while 1:
143 143 d = cg.read(4096)
144 144 if not d: break
145 145 self.pipeo.write(d)
146 146 self.readerr()
147 147
148 148 self.pipeo.flush()
149 149
150 150 self.readerr()
151 151 l = int(self.pipei.readline())
152 152 r = self.pipei.read(l)
153 153 if not r:
154 154 return 1
155 155 return int(r)
@@ -1,845 +1,865 b''
1 1 """
2 2 util.py - Mercurial utility functions and platform specfic implementations
3 3
4 4 Copyright 2005 K. Thananchayan <thananck@yahoo.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 This contains helper routines that are independent of the SCM core and hide
10 10 platform-specific details from the core.
11 11 """
12 12
13 13 import os, errno
14 14 from i18n import gettext as _
15 15 from demandload import *
16 16 demandload(globals(), "cStringIO errno popen2 re shutil sys tempfile")
17 17 demandload(globals(), "threading time")
18 18
19 19 def pipefilter(s, cmd):
20 20 '''filter string S through command CMD, returning its output'''
21 21 (pout, pin) = popen2.popen2(cmd, -1, 'b')
22 22 def writer():
23 23 try:
24 24 pin.write(s)
25 25 pin.close()
26 26 except IOError, inst:
27 27 if inst.errno != errno.EPIPE:
28 28 raise
29 29
30 30 # we should use select instead on UNIX, but this will work on most
31 31 # systems, including Windows
32 32 w = threading.Thread(target=writer)
33 33 w.start()
34 34 f = pout.read()
35 35 pout.close()
36 36 w.join()
37 37 return f
38 38
39 39 def tempfilter(s, cmd):
40 40 '''filter string S through a pair of temporary files with CMD.
41 41 CMD is used as a template to create the real command to be run,
42 42 with the strings INFILE and OUTFILE replaced by the real names of
43 43 the temporary files generated.'''
44 44 inname, outname = None, None
45 45 try:
46 46 infd, inname = tempfile.mkstemp(prefix='hgfin')
47 47 fp = os.fdopen(infd, 'wb')
48 48 fp.write(s)
49 49 fp.close()
50 50 outfd, outname = tempfile.mkstemp(prefix='hgfout')
51 51 os.close(outfd)
52 52 cmd = cmd.replace('INFILE', inname)
53 53 cmd = cmd.replace('OUTFILE', outname)
54 54 code = os.system(cmd)
55 55 if code: raise Abort(_("command '%s' failed: %s") %
56 56 (cmd, explain_exit(code)))
57 57 return open(outname, 'rb').read()
58 58 finally:
59 59 try:
60 60 if inname: os.unlink(inname)
61 61 except: pass
62 62 try:
63 63 if outname: os.unlink(outname)
64 64 except: pass
65 65
66 66 filtertable = {
67 67 'tempfile:': tempfilter,
68 68 'pipe:': pipefilter,
69 69 }
70 70
71 71 def filter(s, cmd):
72 72 "filter a string through a command that transforms its input to its output"
73 73 for name, fn in filtertable.iteritems():
74 74 if cmd.startswith(name):
75 75 return fn(s, cmd[len(name):].lstrip())
76 76 return pipefilter(s, cmd)
77 77
78 78 def find_in_path(name, path, default=None):
79 79 '''find name in search path. path can be string (will be split
80 80 with os.pathsep), or iterable thing that returns strings. if name
81 81 found, return path to name. else return default.'''
82 82 if isinstance(path, str):
83 83 path = path.split(os.pathsep)
84 84 for p in path:
85 85 p_name = os.path.join(p, name)
86 86 if os.path.exists(p_name):
87 87 return p_name
88 88 return default
89 89
90 90 def patch(strip, patchname, ui):
91 91 """apply the patch <patchname> to the working directory.
92 92 a list of patched files is returned"""
93 93 patcher = find_in_path('gpatch', os.environ.get('PATH', ''), 'patch')
94 94 fp = os.popen('"%s" -p%d < "%s"' % (patcher, strip, patchname))
95 95 files = {}
96 96 for line in fp:
97 97 line = line.rstrip()
98 98 ui.status("%s\n" % line)
99 99 if line.startswith('patching file '):
100 100 pf = parse_patch_output(line)
101 101 files.setdefault(pf, 1)
102 102 code = fp.close()
103 103 if code:
104 104 raise Abort(_("patch command failed: %s") % explain_exit(code)[0])
105 105 return files.keys()
106 106
107 107 def binary(s):
108 108 """return true if a string is binary data using diff's heuristic"""
109 109 if s and '\0' in s[:4096]:
110 110 return True
111 111 return False
112 112
113 113 def unique(g):
114 114 """return the uniq elements of iterable g"""
115 115 seen = {}
116 116 for f in g:
117 117 if f not in seen:
118 118 seen[f] = 1
119 119 yield f
120 120
121 121 class Abort(Exception):
122 122 """Raised if a command needs to print an error and exit."""
123 123
124 124 def always(fn): return True
125 125 def never(fn): return False
126 126
127 127 def patkind(name, dflt_pat='glob'):
128 128 """Split a string into an optional pattern kind prefix and the
129 129 actual pattern."""
130 130 for prefix in 're', 'glob', 'path', 'relglob', 'relpath', 'relre':
131 131 if name.startswith(prefix + ':'): return name.split(':', 1)
132 132 return dflt_pat, name
133 133
134 134 def globre(pat, head='^', tail='$'):
135 135 "convert a glob pattern into a regexp"
136 136 i, n = 0, len(pat)
137 137 res = ''
138 138 group = False
139 139 def peek(): return i < n and pat[i]
140 140 while i < n:
141 141 c = pat[i]
142 142 i = i+1
143 143 if c == '*':
144 144 if peek() == '*':
145 145 i += 1
146 146 res += '.*'
147 147 else:
148 148 res += '[^/]*'
149 149 elif c == '?':
150 150 res += '.'
151 151 elif c == '[':
152 152 j = i
153 153 if j < n and pat[j] in '!]':
154 154 j += 1
155 155 while j < n and pat[j] != ']':
156 156 j += 1
157 157 if j >= n:
158 158 res += '\\['
159 159 else:
160 160 stuff = pat[i:j].replace('\\','\\\\')
161 161 i = j + 1
162 162 if stuff[0] == '!':
163 163 stuff = '^' + stuff[1:]
164 164 elif stuff[0] == '^':
165 165 stuff = '\\' + stuff
166 166 res = '%s[%s]' % (res, stuff)
167 167 elif c == '{':
168 168 group = True
169 169 res += '(?:'
170 170 elif c == '}' and group:
171 171 res += ')'
172 172 group = False
173 173 elif c == ',' and group:
174 174 res += '|'
175 175 elif c == '\\':
176 176 p = peek()
177 177 if p:
178 178 i += 1
179 179 res += re.escape(p)
180 180 else:
181 181 res += re.escape(c)
182 182 else:
183 183 res += re.escape(c)
184 184 return head + res + tail
185 185
186 186 _globchars = {'[': 1, '{': 1, '*': 1, '?': 1}
187 187
188 188 def pathto(n1, n2):
189 189 '''return the relative path from one place to another.
190 190 this returns a path in the form used by the local filesystem, not hg.'''
191 191 if not n1: return localpath(n2)
192 192 a, b = n1.split('/'), n2.split('/')
193 193 a.reverse()
194 194 b.reverse()
195 195 while a and b and a[-1] == b[-1]:
196 196 a.pop()
197 197 b.pop()
198 198 b.reverse()
199 199 return os.sep.join((['..'] * len(a)) + b)
200 200
201 201 def canonpath(root, cwd, myname):
202 202 """return the canonical path of myname, given cwd and root"""
203 203 if root == os.sep:
204 204 rootsep = os.sep
205 205 else:
206 206 rootsep = root + os.sep
207 207 name = myname
208 208 if not os.path.isabs(name):
209 209 name = os.path.join(root, cwd, name)
210 210 name = os.path.normpath(name)
211 211 if name.startswith(rootsep):
212 212 name = name[len(rootsep):]
213 213 audit_path(name)
214 214 return pconvert(name)
215 215 elif name == root:
216 216 return ''
217 217 else:
218 218 # Determine whether `name' is in the hierarchy at or beneath `root',
219 219 # by iterating name=dirname(name) until that causes no change (can't
220 220 # check name == '/', because that doesn't work on windows). For each
221 221 # `name', compare dev/inode numbers. If they match, the list `rel'
222 222 # holds the reversed list of components making up the relative file
223 223 # name we want.
224 224 root_st = os.stat(root)
225 225 rel = []
226 226 while True:
227 227 try:
228 228 name_st = os.stat(name)
229 229 except OSError:
230 230 break
231 231 if os.path.samestat(name_st, root_st):
232 232 rel.reverse()
233 233 name = os.path.join(*rel)
234 234 audit_path(name)
235 235 return pconvert(name)
236 236 dirname, basename = os.path.split(name)
237 237 rel.append(basename)
238 238 if dirname == name:
239 239 break
240 240 name = dirname
241 241
242 242 raise Abort('%s not under root' % myname)
243 243
244 244 def matcher(canonroot, cwd='', names=['.'], inc=[], exc=[], head='', src=None):
245 245 return _matcher(canonroot, cwd, names, inc, exc, head, 'glob', src)
246 246
247 247 def cmdmatcher(canonroot, cwd='', names=['.'], inc=[], exc=[], head='', src=None):
248 248 if os.name == 'nt':
249 249 dflt_pat = 'glob'
250 250 else:
251 251 dflt_pat = 'relpath'
252 252 return _matcher(canonroot, cwd, names, inc, exc, head, dflt_pat, src)
253 253
254 254 def _matcher(canonroot, cwd, names, inc, exc, head, dflt_pat, src):
255 255 """build a function to match a set of file patterns
256 256
257 257 arguments:
258 258 canonroot - the canonical root of the tree you're matching against
259 259 cwd - the current working directory, if relevant
260 260 names - patterns to find
261 261 inc - patterns to include
262 262 exc - patterns to exclude
263 263 head - a regex to prepend to patterns to control whether a match is rooted
264 264
265 265 a pattern is one of:
266 266 'glob:<rooted glob>'
267 267 're:<rooted regexp>'
268 268 'path:<rooted path>'
269 269 'relglob:<relative glob>'
270 270 'relpath:<relative path>'
271 271 'relre:<relative regexp>'
272 272 '<rooted path or regexp>'
273 273
274 274 returns:
275 275 a 3-tuple containing
276 276 - list of explicit non-pattern names passed in
277 277 - a bool match(filename) function
278 278 - a bool indicating if any patterns were passed in
279 279
280 280 todo:
281 281 make head regex a rooted bool
282 282 """
283 283
284 284 def contains_glob(name):
285 285 for c in name:
286 286 if c in _globchars: return True
287 287 return False
288 288
289 289 def regex(kind, name, tail):
290 290 '''convert a pattern into a regular expression'''
291 291 if kind == 're':
292 292 return name
293 293 elif kind == 'path':
294 294 return '^' + re.escape(name) + '(?:/|$)'
295 295 elif kind == 'relglob':
296 296 return head + globre(name, '(?:|.*/)', tail)
297 297 elif kind == 'relpath':
298 298 return head + re.escape(name) + tail
299 299 elif kind == 'relre':
300 300 if name.startswith('^'):
301 301 return name
302 302 return '.*' + name
303 303 return head + globre(name, '', tail)
304 304
305 305 def matchfn(pats, tail):
306 306 """build a matching function from a set of patterns"""
307 307 if not pats:
308 308 return
309 309 matches = []
310 310 for k, p in pats:
311 311 try:
312 312 pat = '(?:%s)' % regex(k, p, tail)
313 313 matches.append(re.compile(pat).match)
314 314 except re.error:
315 315 if src: raise Abort("%s: invalid pattern (%s): %s" % (src, k, p))
316 316 else: raise Abort("invalid pattern (%s): %s" % (k, p))
317 317
318 318 def buildfn(text):
319 319 for m in matches:
320 320 r = m(text)
321 321 if r:
322 322 return r
323 323
324 324 return buildfn
325 325
326 326 def globprefix(pat):
327 327 '''return the non-glob prefix of a path, e.g. foo/* -> foo'''
328 328 root = []
329 329 for p in pat.split(os.sep):
330 330 if contains_glob(p): break
331 331 root.append(p)
332 332 return '/'.join(root)
333 333
334 334 pats = []
335 335 files = []
336 336 roots = []
337 337 for kind, name in [patkind(p, dflt_pat) for p in names]:
338 338 if kind in ('glob', 'relpath'):
339 339 name = canonpath(canonroot, cwd, name)
340 340 if name == '':
341 341 kind, name = 'glob', '**'
342 342 if kind in ('glob', 'path', 're'):
343 343 pats.append((kind, name))
344 344 if kind == 'glob':
345 345 root = globprefix(name)
346 346 if root: roots.append(root)
347 347 elif kind == 'relpath':
348 348 files.append((kind, name))
349 349 roots.append(name)
350 350
351 351 patmatch = matchfn(pats, '$') or always
352 352 filematch = matchfn(files, '(?:/|$)') or always
353 353 incmatch = always
354 354 if inc:
355 355 incmatch = matchfn(map(patkind, inc), '(?:/|$)')
356 356 excmatch = lambda fn: False
357 357 if exc:
358 358 excmatch = matchfn(map(patkind, exc), '(?:/|$)')
359 359
360 360 return (roots,
361 361 lambda fn: (incmatch(fn) and not excmatch(fn) and
362 362 (fn.endswith('/') or
363 363 (not pats and not files) or
364 364 (pats and patmatch(fn)) or
365 365 (files and filematch(fn)))),
366 366 (inc or exc or (pats and pats != [('glob', '**')])) and True)
367 367
368 368 def system(cmd, environ={}, cwd=None, onerr=None, errprefix=None):
369 369 '''enhanced shell command execution.
370 370 run with environment maybe modified, maybe in different dir.
371 371
372 372 if command fails and onerr is None, return status. if ui object,
373 373 print error message and return status, else raise onerr object as
374 374 exception.'''
375 375 oldenv = {}
376 376 for k in environ:
377 377 oldenv[k] = os.environ.get(k)
378 378 if cwd is not None:
379 379 oldcwd = os.getcwd()
380 380 try:
381 381 for k, v in environ.iteritems():
382 382 os.environ[k] = str(v)
383 383 if cwd is not None and oldcwd != cwd:
384 384 os.chdir(cwd)
385 385 rc = os.system(cmd)
386 386 if rc and onerr:
387 387 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
388 388 explain_exit(rc)[0])
389 389 if errprefix:
390 390 errmsg = '%s: %s' % (errprefix, errmsg)
391 391 try:
392 392 onerr.warn(errmsg + '\n')
393 393 except AttributeError:
394 394 raise onerr(errmsg)
395 395 return rc
396 396 finally:
397 397 for k, v in oldenv.iteritems():
398 398 if v is None:
399 399 del os.environ[k]
400 400 else:
401 401 os.environ[k] = v
402 402 if cwd is not None and oldcwd != cwd:
403 403 os.chdir(oldcwd)
404 404
405 405 def rename(src, dst):
406 406 """forcibly rename a file"""
407 407 try:
408 408 os.rename(src, dst)
409 except:
410 os.unlink(dst)
409 except OSError, err:
410 # on windows, rename to existing file is not allowed, so we
411 # must delete destination first. but if file is open, unlink
412 # schedules it for delete but does not delete it. rename
413 # happens immediately even for open files, so we create
414 # temporary file, delete it, rename destination to that name,
415 # then delete that. then rename is safe to do.
416 fd, temp = tempfile.mkstemp(dir=os.path.dirname(dst) or '.')
417 os.close(fd)
418 os.unlink(temp)
419 os.rename(dst, temp)
420 os.unlink(temp)
411 421 os.rename(src, dst)
412 422
413 423 def unlink(f):
414 424 """unlink and remove the directory if it is empty"""
415 425 os.unlink(f)
416 426 # try removing directories that might now be empty
417 427 try:
418 428 os.removedirs(os.path.dirname(f))
419 429 except OSError:
420 430 pass
421 431
422 432 def copyfiles(src, dst, hardlink=None):
423 433 """Copy a directory tree using hardlinks if possible"""
424 434
425 435 if hardlink is None:
426 436 hardlink = (os.stat(src).st_dev ==
427 437 os.stat(os.path.dirname(dst)).st_dev)
428 438
429 439 if os.path.isdir(src):
430 440 os.mkdir(dst)
431 441 for name in os.listdir(src):
432 442 srcname = os.path.join(src, name)
433 443 dstname = os.path.join(dst, name)
434 444 copyfiles(srcname, dstname, hardlink)
435 445 else:
436 446 if hardlink:
437 447 try:
438 448 os_link(src, dst)
439 449 except (IOError, OSError):
440 450 hardlink = False
441 451 shutil.copy(src, dst)
442 452 else:
443 453 shutil.copy(src, dst)
444 454
445 455 def audit_path(path):
446 456 """Abort if path contains dangerous components"""
447 457 parts = os.path.normcase(path).split(os.sep)
448 458 if (os.path.splitdrive(path)[0] or parts[0] in ('.hg', '')
449 459 or os.pardir in parts):
450 460 raise Abort(_("path contains illegal component: %s\n") % path)
451 461
452 def opener(base, audit=True):
453 """
454 return a function that opens files relative to base
455
456 this function is used to hide the details of COW semantics and
457 remote file access from higher level code.
458 """
459 p = base
460 audit_p = audit
461
462 def mktempcopy(name):
463 d, fn = os.path.split(name)
464 fd, temp = tempfile.mkstemp(prefix=fn, dir=d)
465 fp = os.fdopen(fd, "wb")
466 try:
467 fp.write(file(name, "rb").read())
468 except:
469 try: os.unlink(temp)
470 except: pass
471 raise
472 fp.close()
473 st = os.lstat(name)
474 os.chmod(temp, st.st_mode)
475 return temp
476
477 class atomictempfile(file):
478 """the file will only be copied when rename is called"""
479 def __init__(self, name, mode):
480 self.__name = name
481 self.temp = mktempcopy(name)
482 file.__init__(self, self.temp, mode)
483 def rename(self):
484 if not self.closed:
485 file.close(self)
486 rename(self.temp, self.__name)
487 def __del__(self):
488 if not self.closed:
489 try:
490 os.unlink(self.temp)
491 except: pass
492 file.close(self)
493
494 class atomicfile(atomictempfile):
495 """the file will only be copied on close"""
496 def __init__(self, name, mode):
497 atomictempfile.__init__(self, name, mode)
498 def close(self):
499 self.rename()
500 def __del__(self):
501 self.rename()
502
503 def o(path, mode="r", text=False, atomic=False, atomictemp=False):
504 if audit_p:
505 audit_path(path)
506 f = os.path.join(p, path)
507
508 if not text:
509 mode += "b" # for that other OS
510
511 if mode[0] != "r":
512 try:
513 nlink = nlinks(f)
514 except OSError:
515 d = os.path.dirname(f)
516 if not os.path.isdir(d):
517 os.makedirs(d)
518 else:
519 if atomic:
520 return atomicfile(f, mode)
521 elif atomictemp:
522 return atomictempfile(f, mode)
523 if nlink > 1:
524 rename(mktempcopy(f), f)
525 return file(f, mode)
526
527 return o
528
529 462 def _makelock_file(info, pathname):
530 463 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
531 464 os.write(ld, info)
532 465 os.close(ld)
533 466
534 467 def _readlock_file(pathname):
535 return file(pathname).read()
468 return posixfile(pathname).read()
536 469
537 470 def nlinks(pathname):
538 471 """Return number of hardlinks for the given file."""
539 472 return os.stat(pathname).st_nlink
540 473
541 474 if hasattr(os, 'link'):
542 475 os_link = os.link
543 476 else:
544 477 def os_link(src, dst):
545 478 raise OSError(0, _("Hardlinks not supported"))
546 479
480 def fstat(fp):
481 '''stat file object that may not have fileno method.'''
482 try:
483 return os.fstat(fp.fileno())
484 except AttributeError:
485 return os.stat(fp.name)
486
487 posixfile = file
488
547 489 # Platform specific variants
548 490 if os.name == 'nt':
549 491 demandload(globals(), "msvcrt")
550 492 nulldev = 'NUL:'
551 493
552 494 class winstdout:
553 495 '''stdout on windows misbehaves if sent through a pipe'''
554 496
555 497 def __init__(self, fp):
556 498 self.fp = fp
557 499
558 500 def __getattr__(self, key):
559 501 return getattr(self.fp, key)
560 502
561 503 def close(self):
562 504 try:
563 505 self.fp.close()
564 506 except: pass
565 507
566 508 def write(self, s):
567 509 try:
568 510 return self.fp.write(s)
569 511 except IOError, inst:
570 512 if inst.errno != 0: raise
571 513 self.close()
572 514 raise IOError(errno.EPIPE, 'Broken pipe')
573 515
574 516 sys.stdout = winstdout(sys.stdout)
575 517
576 518 def system_rcpath():
577 519 try:
578 520 return system_rcpath_win32()
579 521 except:
580 522 return [r'c:\mercurial\mercurial.ini']
581 523
582 524 def os_rcpath():
583 525 '''return default os-specific hgrc search path'''
584 526 return system_rcpath() + [os.path.join(os.path.expanduser('~'),
585 527 'mercurial.ini')]
586 528
587 529 def parse_patch_output(output_line):
588 530 """parses the output produced by patch and returns the file name"""
589 531 pf = output_line[14:]
590 532 if pf[0] == '`':
591 533 pf = pf[1:-1] # Remove the quotes
592 534 return pf
593 535
594 536 def testpid(pid):
595 537 '''return False if pid dead, True if running or not known'''
596 538 return True
597 539
598 540 def is_exec(f, last):
599 541 return last
600 542
601 543 def set_exec(f, mode):
602 544 pass
603 545
604 546 def set_binary(fd):
605 547 msvcrt.setmode(fd.fileno(), os.O_BINARY)
606 548
607 549 def pconvert(path):
608 550 return path.replace("\\", "/")
609 551
610 552 def localpath(path):
611 553 return path.replace('/', '\\')
612 554
613 555 def normpath(path):
614 556 return pconvert(os.path.normpath(path))
615 557
616 558 makelock = _makelock_file
617 559 readlock = _readlock_file
618 560
619 561 def explain_exit(code):
620 562 return _("exited with status %d") % code, code
621 563
622 564 try:
623 565 # override functions with win32 versions if possible
624 566 from util_win32 import *
625 567 except ImportError:
626 568 pass
627 569
628 570 else:
629 571 nulldev = '/dev/null'
630 572
631 573 def rcfiles(path):
632 574 rcs = [os.path.join(path, 'hgrc')]
633 575 rcdir = os.path.join(path, 'hgrc.d')
634 576 try:
635 577 rcs.extend([os.path.join(rcdir, f) for f in os.listdir(rcdir)
636 578 if f.endswith(".rc")])
637 579 except OSError, inst: pass
638 580 return rcs
639 581
640 582 def os_rcpath():
641 583 '''return default os-specific hgrc search path'''
642 584 path = []
643 585 if len(sys.argv) > 0:
644 586 path.extend(rcfiles(os.path.dirname(sys.argv[0]) +
645 587 '/../etc/mercurial'))
646 588 path.extend(rcfiles('/etc/mercurial'))
647 589 path.append(os.path.expanduser('~/.hgrc'))
648 590 path = [os.path.normpath(f) for f in path]
649 591 return path
650 592
651 593 def parse_patch_output(output_line):
652 594 """parses the output produced by patch and returns the file name"""
653 595 pf = output_line[14:]
654 596 if pf.startswith("'") and pf.endswith("'") and pf.find(" ") >= 0:
655 597 pf = pf[1:-1] # Remove the quotes
656 598 return pf
657 599
658 600 def is_exec(f, last):
659 601 """check whether a file is executable"""
660 602 return (os.stat(f).st_mode & 0100 != 0)
661 603
662 604 def set_exec(f, mode):
663 605 s = os.stat(f).st_mode
664 606 if (s & 0100 != 0) == mode:
665 607 return
666 608 if mode:
667 609 # Turn on +x for every +r bit when making a file executable
668 610 # and obey umask.
669 611 umask = os.umask(0)
670 612 os.umask(umask)
671 613 os.chmod(f, s | (s & 0444) >> 2 & ~umask)
672 614 else:
673 615 os.chmod(f, s & 0666)
674 616
675 617 def set_binary(fd):
676 618 pass
677 619
678 620 def pconvert(path):
679 621 return path
680 622
681 623 def localpath(path):
682 624 return path
683 625
684 626 normpath = os.path.normpath
685 627
686 628 def makelock(info, pathname):
687 629 try:
688 630 os.symlink(info, pathname)
689 631 except OSError, why:
690 632 if why.errno == errno.EEXIST:
691 633 raise
692 634 else:
693 635 _makelock_file(info, pathname)
694 636
695 637 def readlock(pathname):
696 638 try:
697 639 return os.readlink(pathname)
698 640 except OSError, why:
699 641 if why.errno == errno.EINVAL:
700 642 return _readlock_file(pathname)
701 643 else:
702 644 raise
703 645
704 646 def testpid(pid):
705 647 '''return False if pid dead, True if running or not sure'''
706 648 try:
707 649 os.kill(pid, 0)
708 650 return True
709 651 except OSError, inst:
710 652 return inst.errno != errno.ESRCH
711 653
712 654 def explain_exit(code):
713 655 """return a 2-tuple (desc, code) describing a process's status"""
714 656 if os.WIFEXITED(code):
715 657 val = os.WEXITSTATUS(code)
716 658 return _("exited with status %d") % val, val
717 659 elif os.WIFSIGNALED(code):
718 660 val = os.WTERMSIG(code)
719 661 return _("killed by signal %d") % val, val
720 662 elif os.WIFSTOPPED(code):
721 663 val = os.WSTOPSIG(code)
722 664 return _("stopped by signal %d") % val, val
723 665 raise ValueError(_("invalid exit code"))
724 666
667 def opener(base, audit=True):
668 """
669 return a function that opens files relative to base
670
671 this function is used to hide the details of COW semantics and
672 remote file access from higher level code.
673 """
674 p = base
675 audit_p = audit
676
677 def mktempcopy(name):
678 d, fn = os.path.split(name)
679 fd, temp = tempfile.mkstemp(prefix=fn, dir=d)
680 os.close(fd)
681 fp = posixfile(temp, "wb")
682 try:
683 fp.write(posixfile(name, "rb").read())
684 except:
685 try: os.unlink(temp)
686 except: pass
687 raise
688 fp.close()
689 st = os.lstat(name)
690 os.chmod(temp, st.st_mode)
691 return temp
692
693 class atomictempfile(posixfile):
694 """the file will only be copied when rename is called"""
695 def __init__(self, name, mode):
696 self.__name = name
697 self.temp = mktempcopy(name)
698 posixfile.__init__(self, self.temp, mode)
699 def rename(self):
700 if not self.closed:
701 posixfile.close(self)
702 rename(self.temp, self.__name)
703 def __del__(self):
704 if not self.closed:
705 try:
706 os.unlink(self.temp)
707 except: pass
708 posixfile.close(self)
709
710 class atomicfile(atomictempfile):
711 """the file will only be copied on close"""
712 def __init__(self, name, mode):
713 atomictempfile.__init__(self, name, mode)
714 def close(self):
715 self.rename()
716 def __del__(self):
717 self.rename()
718
719 def o(path, mode="r", text=False, atomic=False, atomictemp=False):
720 if audit_p:
721 audit_path(path)
722 f = os.path.join(p, path)
723
724 if not text:
725 mode += "b" # for that other OS
726
727 if mode[0] != "r":
728 try:
729 nlink = nlinks(f)
730 except OSError:
731 d = os.path.dirname(f)
732 if not os.path.isdir(d):
733 os.makedirs(d)
734 else:
735 if atomic:
736 return atomicfile(f, mode)
737 elif atomictemp:
738 return atomictempfile(f, mode)
739 if nlink > 1:
740 rename(mktempcopy(f), f)
741 return posixfile(f, mode)
742
743 return o
744
725 745 class chunkbuffer(object):
726 746 """Allow arbitrary sized chunks of data to be efficiently read from an
727 747 iterator over chunks of arbitrary size."""
728 748
729 749 def __init__(self, in_iter, targetsize = 2**16):
730 750 """in_iter is the iterator that's iterating over the input chunks.
731 751 targetsize is how big a buffer to try to maintain."""
732 752 self.in_iter = iter(in_iter)
733 753 self.buf = ''
734 754 self.targetsize = int(targetsize)
735 755 if self.targetsize <= 0:
736 756 raise ValueError(_("targetsize must be greater than 0, was %d") %
737 757 targetsize)
738 758 self.iterempty = False
739 759
740 760 def fillbuf(self):
741 761 """Ignore target size; read every chunk from iterator until empty."""
742 762 if not self.iterempty:
743 763 collector = cStringIO.StringIO()
744 764 collector.write(self.buf)
745 765 for ch in self.in_iter:
746 766 collector.write(ch)
747 767 self.buf = collector.getvalue()
748 768 self.iterempty = True
749 769
750 770 def read(self, l):
751 771 """Read L bytes of data from the iterator of chunks of data.
752 772 Returns less than L bytes if the iterator runs dry."""
753 773 if l > len(self.buf) and not self.iterempty:
754 774 # Clamp to a multiple of self.targetsize
755 775 targetsize = self.targetsize * ((l // self.targetsize) + 1)
756 776 collector = cStringIO.StringIO()
757 777 collector.write(self.buf)
758 778 collected = len(self.buf)
759 779 for chunk in self.in_iter:
760 780 collector.write(chunk)
761 781 collected += len(chunk)
762 782 if collected >= targetsize:
763 783 break
764 784 if collected < targetsize:
765 785 self.iterempty = True
766 786 self.buf = collector.getvalue()
767 787 s, self.buf = self.buf[:l], buffer(self.buf, l)
768 788 return s
769 789
770 790 def filechunkiter(f, size = 65536):
771 791 """Create a generator that produces all the data in the file size
772 792 (default 65536) bytes at a time. Chunks may be less than size
773 793 bytes if the chunk is the last chunk in the file, or the file is a
774 794 socket or some other type of file that sometimes reads less data
775 795 than is requested."""
776 796 s = f.read(size)
777 797 while len(s) > 0:
778 798 yield s
779 799 s = f.read(size)
780 800
781 801 def makedate():
782 802 lt = time.localtime()
783 803 if lt[8] == 1 and time.daylight:
784 804 tz = time.altzone
785 805 else:
786 806 tz = time.timezone
787 807 return time.mktime(lt), tz
788 808
789 809 def datestr(date=None, format='%a %b %d %H:%M:%S %Y', timezone=True):
790 810 """represent a (unixtime, offset) tuple as a localized time.
791 811 unixtime is seconds since the epoch, and offset is the time zone's
792 812 number of seconds away from UTC. if timezone is false, do not
793 813 append time zone to string."""
794 814 t, tz = date or makedate()
795 815 s = time.strftime(format, time.gmtime(float(t) - tz))
796 816 if timezone:
797 817 s += " %+03d%02d" % (-tz / 3600, ((-tz % 3600) / 60))
798 818 return s
799 819
800 820 def shortuser(user):
801 821 """Return a short representation of a user name or email address."""
802 822 f = user.find('@')
803 823 if f >= 0:
804 824 user = user[:f]
805 825 f = user.find('<')
806 826 if f >= 0:
807 827 user = user[f+1:]
808 828 return user
809 829
810 830 def walkrepos(path):
811 831 '''yield every hg repository under path, recursively.'''
812 832 def errhandler(err):
813 833 if err.filename == path:
814 834 raise err
815 835
816 836 for root, dirs, files in os.walk(path, onerror=errhandler):
817 837 for d in dirs:
818 838 if d == '.hg':
819 839 yield root
820 840 dirs[:] = []
821 841 break
822 842
823 843 _rcpath = None
824 844
825 845 def rcpath():
826 846 '''return hgrc search path. if env var HGRCPATH is set, use it.
827 847 for each item in path, if directory, use files ending in .rc,
828 848 else use item.
829 849 make HGRCPATH empty to only look in .hg/hgrc of current repo.
830 850 if no HGRCPATH, use default os-specific path.'''
831 851 global _rcpath
832 852 if _rcpath is None:
833 853 if 'HGRCPATH' in os.environ:
834 854 _rcpath = []
835 855 for p in os.environ['HGRCPATH'].split(os.pathsep):
836 856 if not p: continue
837 857 if os.path.isdir(p):
838 858 for f in os.listdir(p):
839 859 if f.endswith('.rc'):
840 860 _rcpath.append(os.path.join(p, f))
841 861 else:
842 862 _rcpath.append(p)
843 863 else:
844 864 _rcpath = os_rcpath()
845 865 return _rcpath
@@ -1,171 +1,280 b''
1 1 # util_win32.py - utility functions that use win32 API
2 2 #
3 3 # Copyright 2005 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of
7 7 # the GNU General Public License, incorporated herein by reference.
8 8
9 9 # Mark Hammond's win32all package allows better functionality on
10 10 # Windows. this module overrides definitions in util.py. if not
11 11 # available, import of this module will fail, and generic code will be
12 12 # used.
13 13
14 14 import win32api
15 15
16 16 from demandload import *
17 17 from i18n import gettext as _
18 18 demandload(globals(), 'errno os pywintypes win32con win32file win32process')
19 demandload(globals(), 'winerror')
19 demandload(globals(), 'cStringIO winerror')
20 20
21 class WinError(OSError):
21 class WinError:
22 22 winerror_map = {
23 23 winerror.ERROR_ACCESS_DENIED: errno.EACCES,
24 24 winerror.ERROR_ACCOUNT_DISABLED: errno.EACCES,
25 25 winerror.ERROR_ACCOUNT_RESTRICTION: errno.EACCES,
26 26 winerror.ERROR_ALREADY_ASSIGNED: errno.EBUSY,
27 27 winerror.ERROR_ALREADY_EXISTS: errno.EEXIST,
28 28 winerror.ERROR_ARITHMETIC_OVERFLOW: errno.ERANGE,
29 29 winerror.ERROR_BAD_COMMAND: errno.EIO,
30 30 winerror.ERROR_BAD_DEVICE: errno.ENODEV,
31 31 winerror.ERROR_BAD_DRIVER_LEVEL: errno.ENXIO,
32 32 winerror.ERROR_BAD_EXE_FORMAT: errno.ENOEXEC,
33 33 winerror.ERROR_BAD_FORMAT: errno.ENOEXEC,
34 34 winerror.ERROR_BAD_LENGTH: errno.EINVAL,
35 35 winerror.ERROR_BAD_PATHNAME: errno.ENOENT,
36 36 winerror.ERROR_BAD_PIPE: errno.EPIPE,
37 37 winerror.ERROR_BAD_UNIT: errno.ENODEV,
38 38 winerror.ERROR_BAD_USERNAME: errno.EINVAL,
39 39 winerror.ERROR_BROKEN_PIPE: errno.EPIPE,
40 40 winerror.ERROR_BUFFER_OVERFLOW: errno.ENAMETOOLONG,
41 41 winerror.ERROR_BUSY: errno.EBUSY,
42 42 winerror.ERROR_BUSY_DRIVE: errno.EBUSY,
43 43 winerror.ERROR_CALL_NOT_IMPLEMENTED: errno.ENOSYS,
44 44 winerror.ERROR_CANNOT_MAKE: errno.EACCES,
45 45 winerror.ERROR_CANTOPEN: errno.EIO,
46 46 winerror.ERROR_CANTREAD: errno.EIO,
47 47 winerror.ERROR_CANTWRITE: errno.EIO,
48 48 winerror.ERROR_CRC: errno.EIO,
49 49 winerror.ERROR_CURRENT_DIRECTORY: errno.EACCES,
50 50 winerror.ERROR_DEVICE_IN_USE: errno.EBUSY,
51 51 winerror.ERROR_DEV_NOT_EXIST: errno.ENODEV,
52 52 winerror.ERROR_DIRECTORY: errno.EINVAL,
53 53 winerror.ERROR_DIR_NOT_EMPTY: errno.ENOTEMPTY,
54 54 winerror.ERROR_DISK_CHANGE: errno.EIO,
55 55 winerror.ERROR_DISK_FULL: errno.ENOSPC,
56 56 winerror.ERROR_DRIVE_LOCKED: errno.EBUSY,
57 57 winerror.ERROR_ENVVAR_NOT_FOUND: errno.EINVAL,
58 58 winerror.ERROR_EXE_MARKED_INVALID: errno.ENOEXEC,
59 59 winerror.ERROR_FILENAME_EXCED_RANGE: errno.ENAMETOOLONG,
60 60 winerror.ERROR_FILE_EXISTS: errno.EEXIST,
61 61 winerror.ERROR_FILE_INVALID: errno.ENODEV,
62 62 winerror.ERROR_FILE_NOT_FOUND: errno.ENOENT,
63 63 winerror.ERROR_GEN_FAILURE: errno.EIO,
64 64 winerror.ERROR_HANDLE_DISK_FULL: errno.ENOSPC,
65 65 winerror.ERROR_INSUFFICIENT_BUFFER: errno.ENOMEM,
66 66 winerror.ERROR_INVALID_ACCESS: errno.EACCES,
67 67 winerror.ERROR_INVALID_ADDRESS: errno.EFAULT,
68 68 winerror.ERROR_INVALID_BLOCK: errno.EFAULT,
69 69 winerror.ERROR_INVALID_DATA: errno.EINVAL,
70 70 winerror.ERROR_INVALID_DRIVE: errno.ENODEV,
71 71 winerror.ERROR_INVALID_EXE_SIGNATURE: errno.ENOEXEC,
72 72 winerror.ERROR_INVALID_FLAGS: errno.EINVAL,
73 73 winerror.ERROR_INVALID_FUNCTION: errno.ENOSYS,
74 74 winerror.ERROR_INVALID_HANDLE: errno.EBADF,
75 75 winerror.ERROR_INVALID_LOGON_HOURS: errno.EACCES,
76 76 winerror.ERROR_INVALID_NAME: errno.EINVAL,
77 77 winerror.ERROR_INVALID_OWNER: errno.EINVAL,
78 78 winerror.ERROR_INVALID_PARAMETER: errno.EINVAL,
79 79 winerror.ERROR_INVALID_PASSWORD: errno.EPERM,
80 80 winerror.ERROR_INVALID_PRIMARY_GROUP: errno.EINVAL,
81 81 winerror.ERROR_INVALID_SIGNAL_NUMBER: errno.EINVAL,
82 82 winerror.ERROR_INVALID_TARGET_HANDLE: errno.EIO,
83 83 winerror.ERROR_INVALID_WORKSTATION: errno.EACCES,
84 84 winerror.ERROR_IO_DEVICE: errno.EIO,
85 85 winerror.ERROR_IO_INCOMPLETE: errno.EINTR,
86 86 winerror.ERROR_LOCKED: errno.EBUSY,
87 87 winerror.ERROR_LOCK_VIOLATION: errno.EACCES,
88 88 winerror.ERROR_LOGON_FAILURE: errno.EACCES,
89 89 winerror.ERROR_MAPPED_ALIGNMENT: errno.EINVAL,
90 90 winerror.ERROR_META_EXPANSION_TOO_LONG: errno.E2BIG,
91 91 winerror.ERROR_MORE_DATA: errno.EPIPE,
92 92 winerror.ERROR_NEGATIVE_SEEK: errno.ESPIPE,
93 93 winerror.ERROR_NOACCESS: errno.EFAULT,
94 94 winerror.ERROR_NONE_MAPPED: errno.EINVAL,
95 95 winerror.ERROR_NOT_ENOUGH_MEMORY: errno.ENOMEM,
96 96 winerror.ERROR_NOT_READY: errno.EAGAIN,
97 97 winerror.ERROR_NOT_SAME_DEVICE: errno.EXDEV,
98 98 winerror.ERROR_NO_DATA: errno.EPIPE,
99 99 winerror.ERROR_NO_MORE_SEARCH_HANDLES: errno.EIO,
100 100 winerror.ERROR_NO_PROC_SLOTS: errno.EAGAIN,
101 101 winerror.ERROR_NO_SUCH_PRIVILEGE: errno.EACCES,
102 102 winerror.ERROR_OPEN_FAILED: errno.EIO,
103 103 winerror.ERROR_OPEN_FILES: errno.EBUSY,
104 104 winerror.ERROR_OPERATION_ABORTED: errno.EINTR,
105 105 winerror.ERROR_OUTOFMEMORY: errno.ENOMEM,
106 106 winerror.ERROR_PASSWORD_EXPIRED: errno.EACCES,
107 107 winerror.ERROR_PATH_BUSY: errno.EBUSY,
108 winerror.ERROR_PATH_NOT_FOUND: errno.ENOTDIR,
108 winerror.ERROR_PATH_NOT_FOUND: errno.ENOENT,
109 109 winerror.ERROR_PIPE_BUSY: errno.EBUSY,
110 110 winerror.ERROR_PIPE_CONNECTED: errno.EPIPE,
111 111 winerror.ERROR_PIPE_LISTENING: errno.EPIPE,
112 112 winerror.ERROR_PIPE_NOT_CONNECTED: errno.EPIPE,
113 113 winerror.ERROR_PRIVILEGE_NOT_HELD: errno.EACCES,
114 114 winerror.ERROR_READ_FAULT: errno.EIO,
115 115 winerror.ERROR_SEEK: errno.EIO,
116 116 winerror.ERROR_SEEK_ON_DEVICE: errno.ESPIPE,
117 117 winerror.ERROR_SHARING_BUFFER_EXCEEDED: errno.ENFILE,
118 118 winerror.ERROR_SHARING_VIOLATION: errno.EACCES,
119 119 winerror.ERROR_STACK_OVERFLOW: errno.ENOMEM,
120 120 winerror.ERROR_SWAPERROR: errno.ENOENT,
121 121 winerror.ERROR_TOO_MANY_MODULES: errno.EMFILE,
122 122 winerror.ERROR_TOO_MANY_OPEN_FILES: errno.EMFILE,
123 123 winerror.ERROR_UNRECOGNIZED_MEDIA: errno.ENXIO,
124 124 winerror.ERROR_UNRECOGNIZED_VOLUME: errno.ENODEV,
125 125 winerror.ERROR_WAIT_NO_CHILDREN: errno.ECHILD,
126 126 winerror.ERROR_WRITE_FAULT: errno.EIO,
127 127 winerror.ERROR_WRITE_PROTECT: errno.EROFS,
128 128 }
129 129
130 130 def __init__(self, err):
131 131 self.win_errno, self.win_function, self.win_strerror = err
132 if self.win_strerror.endswith('.'):
133 self.win_strerror = self.win_strerror[:-1]
134
135 class WinIOError(WinError, IOError):
136 def __init__(self, err, filename=None):
137 WinError.__init__(self, err)
138 IOError.__init__(self, self.winerror_map.get(self.win_errno, 0),
139 self.win_strerror)
140 self.filename = filename
141
142 class WinOSError(WinError, OSError):
143 def __init__(self, err):
144 WinError.__init__(self, err)
132 145 OSError.__init__(self, self.winerror_map.get(self.win_errno, 0),
133 146 self.win_strerror)
134 147
135 148 def os_link(src, dst):
136 149 # NB will only succeed on NTFS
137 150 try:
138 151 win32file.CreateHardLink(dst, src)
139 152 except pywintypes.error, details:
140 raise WinError(details)
153 raise WinOSError(details)
141 154
142 155 def nlinks(pathname):
143 156 """Return number of hardlinks for the given file."""
144 157 try:
145 158 fh = win32file.CreateFile(pathname,
146 159 win32file.GENERIC_READ, win32file.FILE_SHARE_READ,
147 160 None, win32file.OPEN_EXISTING, 0, None)
148 161 res = win32file.GetFileInformationByHandle(fh)
149 162 fh.Close()
150 163 return res[7]
151 164 except pywintypes.error:
152 165 return os.stat(pathname).st_nlink
153 166
154 167 def testpid(pid):
155 168 '''return True if pid is still running or unable to
156 169 determine, False otherwise'''
157 170 try:
158 171 handle = win32api.OpenProcess(
159 172 win32con.PROCESS_QUERY_INFORMATION, False, pid)
160 173 if handle:
161 174 status = win32process.GetExitCodeProcess(handle)
162 175 return status == win32con.STILL_ACTIVE
163 176 except pywintypes.error, details:
164 177 return details[0] != winerror.ERROR_INVALID_PARAMETER
165 178 return True
166 179
167 180 def system_rcpath_win32():
168 181 '''return default os-specific hgrc search path'''
169 182 proc = win32api.GetCurrentProcess()
170 183 filename = win32process.GetModuleFileNameEx(proc, 0)
171 184 return [os.path.join(os.path.dirname(filename), 'mercurial.ini')]
185
186 class posixfile(object):
187 '''file object with posix-like semantics. on windows, normal
188 files can not be deleted or renamed if they are open. must open
189 with win32file.FILE_SHARE_DELETE. this flag does not exist on
190 windows <= nt.'''
191
192 # tried to use win32file._open_osfhandle to pass fd to os.fdopen,
193 # but does not work at all. wrap win32 file api instead.
194
195 def __init__(self, name, mode='rb'):
196 access = 0
197 if 'r' in mode or '+' in mode:
198 access |= win32file.GENERIC_READ
199 if 'w' in mode or 'a' in mode:
200 access |= win32file.GENERIC_WRITE
201 if 'r' in mode:
202 creation = win32file.OPEN_EXISTING
203 elif 'a' in mode:
204 creation = win32file.OPEN_ALWAYS
205 else:
206 creation = win32file.CREATE_ALWAYS
207 try:
208 self.handle = win32file.CreateFile(name,
209 access,
210 win32file.FILE_SHARE_READ |
211 win32file.FILE_SHARE_WRITE |
212 win32file.FILE_SHARE_DELETE,
213 None,
214 creation,
215 win32file.FILE_ATTRIBUTE_NORMAL,
216 0)
217 except pywintypes.error, err:
218 raise WinIOError(err, name)
219 self.closed = False
220 self.name = name
221 self.mode = mode
222
223 def read(self, count=-1):
224 try:
225 cs = cStringIO.StringIO()
226 while count:
227 wincount = int(count)
228 if wincount == -1:
229 wincount = 1048576
230 val, data = win32file.ReadFile(self.handle, wincount)
231 if not data: break
232 cs.write(data)
233 if count != -1:
234 count -= len(data)
235 return cs.getvalue()
236 except pywintypes.error, err:
237 raise WinIOError(err)
238
239 def write(self, data):
240 try:
241 if 'a' in self.mode:
242 win32file.SetFilePointer(self.handle, 0, win32file.FILE_END)
243 nwrit = 0
244 while nwrit < len(data):
245 val, nwrit = win32file.WriteFile(self.handle, data)
246 data = data[nwrit:]
247 except pywintypes.error, err:
248 raise WinIOError(err)
249
250 def seek(self, pos, whence=0):
251 try:
252 win32file.SetFilePointer(self.handle, int(pos), whence)
253 except pywintypes.error, err:
254 raise WinIOError(err)
255
256 def tell(self):
257 try:
258 return win32file.SetFilePointer(self.handle, 0,
259 win32file.FILE_CURRENT)
260 except pywintypes.error, err:
261 raise WinIOError(err)
262
263 def close(self):
264 if not self.closed:
265 self.handle = None
266 self.closed = True
267
268 def flush(self):
269 try:
270 win32file.FlushFileBuffers(self.handle)
271 except pywintypes.error, err:
272 raise WinIOError(err)
273
274 def truncate(self, pos=0):
275 try:
276 win32file.SetFilePointer(self.handle, int(pos),
277 win32file.FILE_BEGIN)
278 win32file.SetEndOfFile(self.handle)
279 except pywintypes.error, err:
280 raise WinIOError(err)
General Comments 0
You need to be logged in to leave comments. Login now