##// END OF EJS Templates
Fix Windows os.popen bug with interleaved stdout/stderr output...
Patrick Mezard -
r5481:003d1f17 default
parent child Browse files
Show More
@@ -1,274 +1,273
1 1 # CVS conversion code inspired by hg-cvs-import and git-cvsimport
2 2
3 3 import os, locale, re, socket
4 4 from mercurial import util
5 5
6 6 from common import NoRepo, commit, converter_source
7 7
8 8 class convert_cvs(converter_source):
9 9 def __init__(self, ui, path, rev=None):
10 10 super(convert_cvs, self).__init__(ui, path, rev=rev)
11 11
12 12 cvs = os.path.join(path, "CVS")
13 13 if not os.path.exists(cvs):
14 14 raise NoRepo("couldn't open CVS repo %s" % path)
15 15
16 16 self.changeset = {}
17 17 self.files = {}
18 18 self.tags = {}
19 19 self.lastbranch = {}
20 20 self.parent = {}
21 21 self.socket = None
22 22 self.cvsroot = file(os.path.join(cvs, "Root")).read()[:-1]
23 23 self.cvsrepo = file(os.path.join(cvs, "Repository")).read()[:-1]
24 24 self.encoding = locale.getpreferredencoding()
25 25 self._parse()
26 26 self._connect()
27 27
28 28 def _parse(self):
29 29 if self.changeset:
30 30 return
31 31
32 32 maxrev = 0
33 33 cmd = 'cvsps -A -u --cvs-direct -q'
34 34 if self.rev:
35 35 # TODO: handle tags
36 36 try:
37 37 # patchset number?
38 38 maxrev = int(self.rev)
39 39 except ValueError:
40 40 try:
41 41 # date
42 42 util.parsedate(self.rev, ['%Y/%m/%d %H:%M:%S'])
43 43 cmd = '%s -d "1970/01/01 00:00:01" -d "%s"' % (cmd, self.rev)
44 44 except util.Abort:
45 45 raise util.Abort('revision %s is not a patchset number or date' % self.rev)
46 cmd += " 2>&1"
47 46
48 47 d = os.getcwd()
49 48 try:
50 49 os.chdir(self.path)
51 50 id = None
52 51 state = 0
53 for l in os.popen(cmd):
52 for l in util.popen(cmd):
54 53 if state == 0: # header
55 54 if l.startswith("PatchSet"):
56 55 id = l[9:-2]
57 56 if maxrev and int(id) > maxrev:
58 57 state = 3
59 58 elif l.startswith("Date"):
60 59 date = util.parsedate(l[6:-1], ["%Y/%m/%d %H:%M:%S"])
61 60 date = util.datestr(date)
62 61 elif l.startswith("Branch"):
63 62 branch = l[8:-1]
64 63 self.parent[id] = self.lastbranch.get(branch, 'bad')
65 64 self.lastbranch[branch] = id
66 65 elif l.startswith("Ancestor branch"):
67 66 ancestor = l[17:-1]
68 67 self.parent[id] = self.lastbranch[ancestor]
69 68 elif l.startswith("Author"):
70 69 author = self.recode(l[8:-1])
71 70 elif l.startswith("Tag:") or l.startswith("Tags:"):
72 71 t = l[l.index(':')+1:]
73 72 t = [ut.strip() for ut in t.split(',')]
74 73 if (len(t) > 1) or (t[0] and (t[0] != "(none)")):
75 74 self.tags.update(dict.fromkeys(t, id))
76 75 elif l.startswith("Log:"):
77 76 state = 1
78 77 log = ""
79 78 elif state == 1: # log
80 79 if l == "Members: \n":
81 80 files = {}
82 81 log = self.recode(log[:-1])
83 82 state = 2
84 83 else:
85 84 log += l
86 85 elif state == 2:
87 86 if l == "\n": #
88 87 state = 0
89 88 p = [self.parent[id]]
90 89 if id == "1":
91 90 p = []
92 91 if branch == "HEAD":
93 92 branch = ""
94 93 c = commit(author=author, date=date, parents=p,
95 94 desc=log, branch=branch)
96 95 self.changeset[id] = c
97 96 self.files[id] = files
98 97 else:
99 98 colon = l.rfind(':')
100 99 file = l[1:colon]
101 100 rev = l[colon+1:-2]
102 101 rev = rev.split("->")[1]
103 102 files[file] = rev
104 103 elif state == 3:
105 104 continue
106 105
107 106 self.heads = self.lastbranch.values()
108 107 finally:
109 108 os.chdir(d)
110 109
111 110 def _connect(self):
112 111 root = self.cvsroot
113 112 conntype = None
114 113 user, host = None, None
115 114 cmd = ['cvs', 'server']
116 115
117 116 self.ui.status("connecting to %s\n" % root)
118 117
119 118 if root.startswith(":pserver:"):
120 119 root = root[9:]
121 120 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)',
122 121 root)
123 122 if m:
124 123 conntype = "pserver"
125 124 user, passw, serv, port, root = m.groups()
126 125 if not user:
127 126 user = "anonymous"
128 127 if not port:
129 128 port = 2401
130 129 else:
131 130 port = int(port)
132 131 format0 = ":pserver:%s@%s:%s" % (user, serv, root)
133 132 format1 = ":pserver:%s@%s:%d%s" % (user, serv, port, root)
134 133
135 134 if not passw:
136 135 passw = "A"
137 136 pf = open(os.path.join(os.environ["HOME"], ".cvspass"))
138 137 for line in pf.read().splitlines():
139 138 part1, part2 = line.split(' ', 1)
140 139 if part1 == '/1':
141 140 # /1 :pserver:user@example.com:2401/cvsroot/foo Ah<Z
142 141 part1, part2 = part2.split(' ', 1)
143 142 format = format1
144 143 else:
145 144 # :pserver:user@example.com:/cvsroot/foo Ah<Z
146 145 format = format0
147 146 if part1 == format:
148 147 passw = part2
149 148 break
150 149 pf.close()
151 150
152 151 sck = socket.socket()
153 152 sck.connect((serv, port))
154 153 sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw,
155 154 "END AUTH REQUEST", ""]))
156 155 if sck.recv(128) != "I LOVE YOU\n":
157 156 raise util.Abort("CVS pserver authentication failed")
158 157
159 158 self.writep = self.readp = sck.makefile('r+')
160 159
161 160 if not conntype and root.startswith(":local:"):
162 161 conntype = "local"
163 162 root = root[7:]
164 163
165 164 if not conntype:
166 165 # :ext:user@host/home/user/path/to/cvsroot
167 166 if root.startswith(":ext:"):
168 167 root = root[5:]
169 168 m = re.match(r'(?:([^@:/]+)@)?([^:/]+):?(.*)', root)
170 169 # Do not take Windows path "c:\foo\bar" for a connection strings
171 170 if os.path.isdir(root) or not m:
172 171 conntype = "local"
173 172 else:
174 173 conntype = "rsh"
175 174 user, host, root = m.group(1), m.group(2), m.group(3)
176 175
177 176 if conntype != "pserver":
178 177 if conntype == "rsh":
179 178 rsh = os.environ.get("CVS_RSH" or "rsh")
180 179 if user:
181 180 cmd = [rsh, '-l', user, host] + cmd
182 181 else:
183 182 cmd = [rsh, host] + cmd
184 183
185 184 # popen2 does not support argument lists under Windows
186 185 cmd = [util.shellquote(arg) for arg in cmd]
187 186 cmd = util.quotecommand(' '.join(cmd))
188 187 self.writep, self.readp = os.popen2(cmd, 'b')
189 188
190 189 self.realroot = root
191 190
192 191 self.writep.write("Root %s\n" % root)
193 192 self.writep.write("Valid-responses ok error Valid-requests Mode"
194 193 " M Mbinary E Checked-in Created Updated"
195 194 " Merged Removed\n")
196 195 self.writep.write("valid-requests\n")
197 196 self.writep.flush()
198 197 r = self.readp.readline()
199 198 if not r.startswith("Valid-requests"):
200 199 raise util.Abort("server sucks")
201 200 if "UseUnchanged" in r:
202 201 self.writep.write("UseUnchanged\n")
203 202 self.writep.flush()
204 203 r = self.readp.readline()
205 204
206 205 def getheads(self):
207 206 return self.heads
208 207
209 208 def _getfile(self, name, rev):
210 209 if rev.endswith("(DEAD)"):
211 210 raise IOError
212 211
213 212 args = ("-N -P -kk -r %s --" % rev).split()
214 213 args.append(self.cvsrepo + '/' + name)
215 214 for x in args:
216 215 self.writep.write("Argument %s\n" % x)
217 216 self.writep.write("Directory .\n%s\nco\n" % self.realroot)
218 217 self.writep.flush()
219 218
220 219 data = ""
221 220 while 1:
222 221 line = self.readp.readline()
223 222 if line.startswith("Created ") or line.startswith("Updated "):
224 223 self.readp.readline() # path
225 224 self.readp.readline() # entries
226 225 mode = self.readp.readline()[:-1]
227 226 count = int(self.readp.readline()[:-1])
228 227 data = self.readp.read(count)
229 228 elif line.startswith(" "):
230 229 data += line[1:]
231 230 elif line.startswith("M "):
232 231 pass
233 232 elif line.startswith("Mbinary "):
234 233 count = int(self.readp.readline()[:-1])
235 234 data = self.readp.read(count)
236 235 else:
237 236 if line == "ok\n":
238 237 return (data, "x" in mode and "x" or "")
239 238 elif line.startswith("E "):
240 239 self.ui.warn("cvs server: %s\n" % line[2:])
241 240 elif line.startswith("Remove"):
242 241 l = self.readp.readline()
243 242 l = self.readp.readline()
244 243 if l != "ok\n":
245 244 raise util.Abort("unknown CVS response: %s" % l)
246 245 else:
247 246 raise util.Abort("unknown CVS response: %s" % line)
248 247
249 248 def getfile(self, file, rev):
250 249 data, mode = self._getfile(file, rev)
251 250 self.modecache[(file, rev)] = mode
252 251 return data
253 252
254 253 def getmode(self, file, rev):
255 254 return self.modecache[(file, rev)]
256 255
257 256 def getchanges(self, rev):
258 257 self.modecache = {}
259 258 files = self.files[rev]
260 259 cl = files.items()
261 260 cl.sort()
262 261 return (cl, {})
263 262
264 263 def getcommit(self, rev):
265 264 return self.changeset[rev]
266 265
267 266 def gettags(self):
268 267 return self.tags
269 268
270 269 def getchangedfiles(self, rev, i):
271 270 files = self.files[rev].keys()
272 271 files.sort()
273 272 return files
274 273
@@ -1,140 +1,140
1 1 # darcs support for the convert extension
2 2
3 3 from common import NoRepo, commit, converter_source
4 4 from mercurial.i18n import _
5 5 from mercurial import util
6 6 import os, shutil, tempfile
7 7
8 8 # The naming drift of ElementTree is fun!
9 9
10 10 try: from xml.etree.cElementTree import ElementTree
11 11 except ImportError:
12 12 try: from xml.etree.ElementTree import ElementTree
13 13 except ImportError:
14 14 try: from elementtree.cElementTree import ElementTree
15 15 except ImportError:
16 16 try: from elementtree.ElementTree import ElementTree
17 17 except ImportError: ElementTree = None
18 18
19 19
20 20 class darcs_source(converter_source):
21 21 def __init__(self, ui, path, rev=None):
22 22 super(darcs_source, self).__init__(ui, path, rev=rev)
23 23
24 24 if not os.path.exists(os.path.join(path, '_darcs', 'inventory')):
25 25 raise NoRepo("couldn't open darcs repo %s" % path)
26 26
27 27 if ElementTree is None:
28 28 raise util.Abort(_("Python ElementTree module is not available"))
29 29
30 30 self.path = os.path.realpath(path)
31 31
32 32 self.lastrev = None
33 33 self.changes = {}
34 34 self.parents = {}
35 35 self.tags = {}
36 36
37 37 def before(self):
38 38 self.tmppath = tempfile.mkdtemp(
39 39 prefix='convert-' + os.path.basename(self.path) + '-')
40 40 output, status = self.run('init', repodir=self.tmppath)
41 41 self.checkexit(status)
42 42
43 43 tree = self.xml('changes', '--xml-output', '--summary')
44 44 tagname = None
45 45 child = None
46 46 for elt in tree.findall('patch'):
47 47 node = elt.get('hash')
48 48 name = elt.findtext('name', '')
49 49 if name.startswith('TAG '):
50 50 tagname = name[4:].strip()
51 51 elif tagname is not None:
52 52 self.tags[tagname] = node
53 53 tagname = None
54 54 self.changes[node] = elt
55 55 self.parents[child] = [node]
56 56 child = node
57 57 self.parents[child] = []
58 58
59 59 def after(self):
60 60 self.ui.debug('cleaning up %s\n' % self.tmppath)
61 61 shutil.rmtree(self.tmppath, ignore_errors=True)
62 62
63 63 def _run(self, cmd, *args, **kwargs):
64 64 cmdline = ['darcs', cmd, '--repodir', kwargs.get('repodir', self.path)]
65 65 cmdline += args
66 66 cmdline = [util.shellquote(arg) for arg in cmdline]
67 67 cmdline += ['<', util.nulldev]
68 cmdline = util.quotecommand(' '.join(cmdline))
68 cmdline = ' '.join(cmdline)
69 69 self.ui.debug(cmdline, '\n')
70 return os.popen(cmdline, 'r')
70 return util.popen(cmdline)
71 71
72 72 def run(self, cmd, *args, **kwargs):
73 73 fp = self._run(cmd, *args, **kwargs)
74 74 output = fp.read()
75 75 return output, fp.close()
76 76
77 77 def checkexit(self, status, output=''):
78 78 if status:
79 79 if output:
80 80 self.ui.warn(_('darcs error:\n'))
81 81 self.ui.warn(output)
82 82 msg = util.explain_exit(status)[0]
83 83 raise util.Abort(_('darcs %s') % msg)
84 84
85 85 def xml(self, cmd, *opts):
86 86 etree = ElementTree()
87 87 fp = self._run(cmd, *opts)
88 88 etree.parse(fp)
89 89 self.checkexit(fp.close())
90 90 return etree.getroot()
91 91
92 92 def getheads(self):
93 93 return self.parents[None]
94 94
95 95 def getcommit(self, rev):
96 96 elt = self.changes[rev]
97 97 date = util.strdate(elt.get('local_date'), '%a %b %d %H:%M:%S %Z %Y')
98 98 desc = elt.findtext('name') + '\n' + elt.findtext('comment', '')
99 99 return commit(author=elt.get('author'), date=util.datestr(date),
100 100 desc=desc.strip(), parents=self.parents[rev])
101 101
102 102 def pull(self, rev):
103 103 output, status = self.run('pull', self.path, '--all',
104 104 '--match', 'hash %s' % rev,
105 105 '--no-test', '--no-posthook',
106 106 '--external-merge', '/bin/false',
107 107 repodir=self.tmppath)
108 108 if status:
109 109 if output.find('We have conflicts in') == -1:
110 110 self.checkexit(status, output)
111 111 output, status = self.run('revert', '--all', repodir=self.tmppath)
112 112 self.checkexit(status, output)
113 113
114 114 def getchanges(self, rev):
115 115 self.pull(rev)
116 116 copies = {}
117 117 changes = []
118 118 for elt in self.changes[rev].find('summary').getchildren():
119 119 if elt.tag in ('add_directory', 'remove_directory'):
120 120 continue
121 121 if elt.tag == 'move':
122 122 changes.append((elt.get('from'), rev))
123 123 copies[elt.get('from')] = elt.get('to')
124 124 else:
125 125 changes.append((elt.text.strip(), rev))
126 126 changes.sort()
127 127 self.lastrev = rev
128 128 return changes, copies
129 129
130 130 def getfile(self, name, rev):
131 131 if rev != self.lastrev:
132 132 raise util.Abort(_('internal calling inconsistency'))
133 133 return open(os.path.join(self.tmppath, name), 'rb').read()
134 134
135 135 def getmode(self, name, rev):
136 136 mode = os.lstat(os.path.join(self.tmppath, name)).st_mode
137 137 return (mode & 0111) and 'x' or ''
138 138
139 139 def gettags(self):
140 140 return self.tags
@@ -1,142 +1,140
1 1 # git support for the convert extension
2 2
3 3 import os
4 4 from mercurial import util
5 5
6 6 from common import NoRepo, commit, converter_source
7 7
8 8 class convert_git(converter_source):
9 9 # Windows does not support GIT_DIR= construct while other systems
10 10 # cannot remove environment variable. Just assume none have
11 11 # both issues.
12 12 if hasattr(os, 'unsetenv'):
13 13 def gitcmd(self, s):
14 14 prevgitdir = os.environ.get('GIT_DIR')
15 15 os.environ['GIT_DIR'] = self.path
16 16 try:
17 return os.popen(s)
17 return util.popen(s)
18 18 finally:
19 19 if prevgitdir is None:
20 20 del os.environ['GIT_DIR']
21 21 else:
22 22 os.environ['GIT_DIR'] = prevgitdir
23 23 else:
24 24 def gitcmd(self, s):
25 return os.popen('GIT_DIR=%s %s' % (self.path, s))
25 return util.popen('GIT_DIR=%s %s' % (self.path, s))
26 26
27 27 def __init__(self, ui, path, rev=None):
28 28 super(convert_git, self).__init__(ui, path, rev=rev)
29 29
30 30 if os.path.isdir(path + "/.git"):
31 31 path += "/.git"
32 32 if not os.path.exists(path + "/objects"):
33 33 raise NoRepo("couldn't open GIT repo %s" % path)
34 34 self.path = path
35 35
36 36 def getheads(self):
37 37 if not self.rev:
38 38 return self.gitcmd('git-rev-parse --branches').read().splitlines()
39 39 else:
40 40 fh = self.gitcmd("git-rev-parse --verify %s" % self.rev)
41 41 return [fh.read()[:-1]]
42 42
43 43 def catfile(self, rev, type):
44 44 if rev == "0" * 40: raise IOError()
45 fh = self.gitcmd("git-cat-file %s %s 2>%s" % (type, rev,
46 util.nulldev))
45 fh = self.gitcmd("git-cat-file %s %s" % (type, rev))
47 46 return fh.read()
48 47
49 48 def getfile(self, name, rev):
50 49 return self.catfile(rev, "blob")
51 50
52 51 def getmode(self, name, rev):
53 52 return self.modecache[(name, rev)]
54 53
55 54 def getchanges(self, version):
56 55 self.modecache = {}
57 56 fh = self.gitcmd("git-diff-tree --root -m -r %s" % version)
58 57 changes = []
59 58 seen = {}
60 59 for l in fh:
61 60 if "\t" not in l:
62 61 continue
63 62 m, f = l[:-1].split("\t")
64 63 if f in seen:
65 64 continue
66 65 seen[f] = 1
67 66 m = m.split()
68 67 h = m[3]
69 68 p = (m[1] == "100755")
70 69 s = (m[1] == "120000")
71 70 self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
72 71 changes.append((f, h))
73 72 return (changes, {})
74 73
75 74 def getcommit(self, version):
76 75 c = self.catfile(version, "commit") # read the commit hash
77 76 end = c.find("\n\n")
78 77 message = c[end+2:]
79 78 message = self.recode(message)
80 79 l = c[:end].splitlines()
81 80 manifest = l[0].split()[1]
82 81 parents = []
83 82 for e in l[1:]:
84 83 n, v = e.split(" ", 1)
85 84 if n == "author":
86 85 p = v.split()
87 86 tm, tz = p[-2:]
88 87 author = " ".join(p[:-2])
89 88 if author[0] == "<": author = author[1:-1]
90 89 author = self.recode(author)
91 90 if n == "committer":
92 91 p = v.split()
93 92 tm, tz = p[-2:]
94 93 committer = " ".join(p[:-2])
95 94 if committer[0] == "<": committer = committer[1:-1]
96 95 committer = self.recode(committer)
97 96 message += "\ncommitter: %s\n" % committer
98 97 if n == "parent": parents.append(v)
99 98
100 99 tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
101 100 tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
102 101 date = tm + " " + str(tz)
103 102 author = author or "unknown"
104 103
105 104 c = commit(parents=parents, date=date, author=author, desc=message,
106 105 rev=version)
107 106 return c
108 107
109 108 def gettags(self):
110 109 tags = {}
111 fh = self.gitcmd('git-ls-remote --tags "%s" 2>%s' % (self.path,
112 util.nulldev))
110 fh = self.gitcmd('git-ls-remote --tags "%s"' % self.path)
113 111 prefix = 'refs/tags/'
114 112 for line in fh:
115 113 line = line.strip()
116 114 if not line.endswith("^{}"):
117 115 continue
118 116 node, tag = line.split(None, 1)
119 117 if not tag.startswith(prefix):
120 118 continue
121 119 tag = tag[len(prefix):-3]
122 120 tags[tag] = node
123 121
124 122 return tags
125 123
126 124 def getchangedfiles(self, version, i):
127 125 changes = []
128 126 if i is None:
129 127 fh = self.gitcmd("git-diff-tree --root -m -r %s" % version)
130 128 for l in fh:
131 129 if "\t" not in l:
132 130 continue
133 131 m, f = l[:-1].split("\t")
134 132 changes.append(f)
135 133 fh.close()
136 134 else:
137 135 fh = self.gitcmd('git-diff-tree --name-only --root -r %s "%s^%s" --'
138 136 % (version, version, i+1))
139 137 changes = [f.rstrip('\n') for f in fh]
140 138 fh.close()
141 139
142 140 return changes
@@ -1,1348 +1,1348
1 1 # patch.py - patch file parsing routines
2 2 #
3 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 9 from i18n import _
10 10 from node import *
11 11 import base85, cmdutil, mdiff, util, context, revlog, diffhelpers
12 12 import cStringIO, email.Parser, os, popen2, re, sha, errno
13 13 import sys, tempfile, zlib
14 14
15 15 class PatchError(Exception):
16 16 pass
17 17
18 18 class NoHunks(PatchError):
19 19 pass
20 20
21 21 # helper functions
22 22
23 23 def copyfile(src, dst, basedir=None):
24 24 if not basedir:
25 25 basedir = os.getcwd()
26 26
27 27 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
28 28 if os.path.exists(absdst):
29 29 raise util.Abort(_("cannot create %s: destination already exists") %
30 30 dst)
31 31
32 32 targetdir = os.path.dirname(absdst)
33 33 if not os.path.isdir(targetdir):
34 34 os.makedirs(targetdir)
35 35
36 36 util.copyfile(abssrc, absdst)
37 37
38 38 # public functions
39 39
40 40 def extract(ui, fileobj):
41 41 '''extract patch from data read from fileobj.
42 42
43 43 patch can be a normal patch or contained in an email message.
44 44
45 45 return tuple (filename, message, user, date, node, p1, p2).
46 46 Any item in the returned tuple can be None. If filename is None,
47 47 fileobj did not contain a patch. Caller must unlink filename when done.'''
48 48
49 49 # attempt to detect the start of a patch
50 50 # (this heuristic is borrowed from quilt)
51 51 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
52 52 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
53 53 '(---|\*\*\*)[ \t])', re.MULTILINE)
54 54
55 55 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
56 56 tmpfp = os.fdopen(fd, 'w')
57 57 try:
58 58 msg = email.Parser.Parser().parse(fileobj)
59 59
60 60 subject = msg['Subject']
61 61 user = msg['From']
62 62 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
63 63 # should try to parse msg['Date']
64 64 date = None
65 65 nodeid = None
66 66 branch = None
67 67 parents = []
68 68
69 69 if subject:
70 70 if subject.startswith('[PATCH'):
71 71 pend = subject.find(']')
72 72 if pend >= 0:
73 73 subject = subject[pend+1:].lstrip()
74 74 subject = subject.replace('\n\t', ' ')
75 75 ui.debug('Subject: %s\n' % subject)
76 76 if user:
77 77 ui.debug('From: %s\n' % user)
78 78 diffs_seen = 0
79 79 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
80 80 message = ''
81 81 for part in msg.walk():
82 82 content_type = part.get_content_type()
83 83 ui.debug('Content-Type: %s\n' % content_type)
84 84 if content_type not in ok_types:
85 85 continue
86 86 payload = part.get_payload(decode=True)
87 87 m = diffre.search(payload)
88 88 if m:
89 89 hgpatch = False
90 90 ignoretext = False
91 91
92 92 ui.debug(_('found patch at byte %d\n') % m.start(0))
93 93 diffs_seen += 1
94 94 cfp = cStringIO.StringIO()
95 95 for line in payload[:m.start(0)].splitlines():
96 96 if line.startswith('# HG changeset patch'):
97 97 ui.debug(_('patch generated by hg export\n'))
98 98 hgpatch = True
99 99 # drop earlier commit message content
100 100 cfp.seek(0)
101 101 cfp.truncate()
102 102 subject = None
103 103 elif hgpatch:
104 104 if line.startswith('# User '):
105 105 user = line[7:]
106 106 ui.debug('From: %s\n' % user)
107 107 elif line.startswith("# Date "):
108 108 date = line[7:]
109 109 elif line.startswith("# Branch "):
110 110 branch = line[9:]
111 111 elif line.startswith("# Node ID "):
112 112 nodeid = line[10:]
113 113 elif line.startswith("# Parent "):
114 114 parents.append(line[10:])
115 115 elif line == '---' and gitsendmail:
116 116 ignoretext = True
117 117 if not line.startswith('# ') and not ignoretext:
118 118 cfp.write(line)
119 119 cfp.write('\n')
120 120 message = cfp.getvalue()
121 121 if tmpfp:
122 122 tmpfp.write(payload)
123 123 if not payload.endswith('\n'):
124 124 tmpfp.write('\n')
125 125 elif not diffs_seen and message and content_type == 'text/plain':
126 126 message += '\n' + payload
127 127 except:
128 128 tmpfp.close()
129 129 os.unlink(tmpname)
130 130 raise
131 131
132 132 if subject and not message.startswith(subject):
133 133 message = '%s\n%s' % (subject, message)
134 134 tmpfp.close()
135 135 if not diffs_seen:
136 136 os.unlink(tmpname)
137 137 return None, message, user, date, branch, None, None, None
138 138 p1 = parents and parents.pop(0) or None
139 139 p2 = parents and parents.pop(0) or None
140 140 return tmpname, message, user, date, branch, nodeid, p1, p2
141 141
142 142 GP_PATCH = 1 << 0 # we have to run patch
143 143 GP_FILTER = 1 << 1 # there's some copy/rename operation
144 144 GP_BINARY = 1 << 2 # there's a binary patch
145 145
146 146 def readgitpatch(fp, firstline=None):
147 147 """extract git-style metadata about patches from <patchname>"""
148 148 class gitpatch:
149 149 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
150 150 def __init__(self, path):
151 151 self.path = path
152 152 self.oldpath = None
153 153 self.mode = None
154 154 self.op = 'MODIFY'
155 155 self.lineno = 0
156 156 self.binary = False
157 157
158 158 def reader(fp, firstline):
159 159 if firstline is not None:
160 160 yield firstline
161 161 for line in fp:
162 162 yield line
163 163
164 164 # Filter patch for git information
165 165 gitre = re.compile('diff --git a/(.*) b/(.*)')
166 166 gp = None
167 167 gitpatches = []
168 168 # Can have a git patch with only metadata, causing patch to complain
169 169 dopatch = 0
170 170
171 171 lineno = 0
172 172 for line in reader(fp, firstline):
173 173 lineno += 1
174 174 if line.startswith('diff --git'):
175 175 m = gitre.match(line)
176 176 if m:
177 177 if gp:
178 178 gitpatches.append(gp)
179 179 src, dst = m.group(1, 2)
180 180 gp = gitpatch(dst)
181 181 gp.lineno = lineno
182 182 elif gp:
183 183 if line.startswith('--- '):
184 184 if gp.op in ('COPY', 'RENAME'):
185 185 dopatch |= GP_FILTER
186 186 gitpatches.append(gp)
187 187 gp = None
188 188 dopatch |= GP_PATCH
189 189 continue
190 190 if line.startswith('rename from '):
191 191 gp.op = 'RENAME'
192 192 gp.oldpath = line[12:].rstrip()
193 193 elif line.startswith('rename to '):
194 194 gp.path = line[10:].rstrip()
195 195 elif line.startswith('copy from '):
196 196 gp.op = 'COPY'
197 197 gp.oldpath = line[10:].rstrip()
198 198 elif line.startswith('copy to '):
199 199 gp.path = line[8:].rstrip()
200 200 elif line.startswith('deleted file'):
201 201 gp.op = 'DELETE'
202 202 elif line.startswith('new file mode '):
203 203 gp.op = 'ADD'
204 204 gp.mode = int(line.rstrip()[-6:], 8)
205 205 elif line.startswith('new mode '):
206 206 gp.mode = int(line.rstrip()[-6:], 8)
207 207 elif line.startswith('GIT binary patch'):
208 208 dopatch |= GP_BINARY
209 209 gp.binary = True
210 210 if gp:
211 211 gitpatches.append(gp)
212 212
213 213 if not gitpatches:
214 214 dopatch = GP_PATCH
215 215
216 216 return (dopatch, gitpatches)
217 217
218 218 def patch(patchname, ui, strip=1, cwd=None, files={}):
219 219 """apply <patchname> to the working directory.
220 220 returns whether patch was applied with fuzz factor."""
221 221 patcher = ui.config('ui', 'patch')
222 222 args = []
223 223 try:
224 224 if patcher:
225 225 return externalpatch(patcher, args, patchname, ui, strip, cwd,
226 226 files)
227 227 else:
228 228 try:
229 229 return internalpatch(patchname, ui, strip, cwd, files)
230 230 except NoHunks:
231 231 patcher = util.find_exe('gpatch') or util.find_exe('patch')
232 232 ui.debug('no valid hunks found; trying with %r instead\n' %
233 233 patcher)
234 234 if util.needbinarypatch():
235 235 args.append('--binary')
236 236 return externalpatch(patcher, args, patchname, ui, strip, cwd,
237 237 files)
238 238 except PatchError, err:
239 239 s = str(err)
240 240 if s:
241 241 raise util.Abort(s)
242 242 else:
243 243 raise util.Abort(_('patch failed to apply'))
244 244
245 245 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
246 246 """use <patcher> to apply <patchname> to the working directory.
247 247 returns whether patch was applied with fuzz factor."""
248 248
249 249 fuzz = False
250 250 if cwd:
251 251 args.append('-d %s' % util.shellquote(cwd))
252 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
252 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
253 253 util.shellquote(patchname)))
254 254
255 255 for line in fp:
256 256 line = line.rstrip()
257 257 ui.note(line + '\n')
258 258 if line.startswith('patching file '):
259 259 pf = util.parse_patch_output(line)
260 260 printed_file = False
261 261 files.setdefault(pf, (None, None))
262 262 elif line.find('with fuzz') >= 0:
263 263 fuzz = True
264 264 if not printed_file:
265 265 ui.warn(pf + '\n')
266 266 printed_file = True
267 267 ui.warn(line + '\n')
268 268 elif line.find('saving rejects to file') >= 0:
269 269 ui.warn(line + '\n')
270 270 elif line.find('FAILED') >= 0:
271 271 if not printed_file:
272 272 ui.warn(pf + '\n')
273 273 printed_file = True
274 274 ui.warn(line + '\n')
275 275 code = fp.close()
276 276 if code:
277 277 raise PatchError(_("patch command failed: %s") %
278 278 util.explain_exit(code)[0])
279 279 return fuzz
280 280
281 281 def internalpatch(patchobj, ui, strip, cwd, files={}):
282 282 """use builtin patch to apply <patchobj> to the working directory.
283 283 returns whether patch was applied with fuzz factor."""
284 284 try:
285 285 fp = file(patchobj, 'rb')
286 286 except TypeError:
287 287 fp = patchobj
288 288 if cwd:
289 289 curdir = os.getcwd()
290 290 os.chdir(cwd)
291 291 try:
292 292 ret = applydiff(ui, fp, files, strip=strip)
293 293 finally:
294 294 if cwd:
295 295 os.chdir(curdir)
296 296 if ret < 0:
297 297 raise PatchError
298 298 return ret > 0
299 299
300 300 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
301 301 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
302 302 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
303 303
304 304 class patchfile:
305 305 def __init__(self, ui, fname):
306 306 self.fname = fname
307 307 self.ui = ui
308 308 try:
309 309 fp = file(fname, 'rb')
310 310 self.lines = fp.readlines()
311 311 self.exists = True
312 312 except IOError:
313 313 dirname = os.path.dirname(fname)
314 314 if dirname and not os.path.isdir(dirname):
315 315 dirs = dirname.split(os.path.sep)
316 316 d = ""
317 317 for x in dirs:
318 318 d = os.path.join(d, x)
319 319 if not os.path.isdir(d):
320 320 os.mkdir(d)
321 321 self.lines = []
322 322 self.exists = False
323 323
324 324 self.hash = {}
325 325 self.dirty = 0
326 326 self.offset = 0
327 327 self.rej = []
328 328 self.fileprinted = False
329 329 self.printfile(False)
330 330 self.hunks = 0
331 331
332 332 def printfile(self, warn):
333 333 if self.fileprinted:
334 334 return
335 335 if warn or self.ui.verbose:
336 336 self.fileprinted = True
337 337 s = _("patching file %s\n") % self.fname
338 338 if warn:
339 339 self.ui.warn(s)
340 340 else:
341 341 self.ui.note(s)
342 342
343 343
344 344 def findlines(self, l, linenum):
345 345 # looks through the hash and finds candidate lines. The
346 346 # result is a list of line numbers sorted based on distance
347 347 # from linenum
348 348 def sorter(a, b):
349 349 vala = abs(a - linenum)
350 350 valb = abs(b - linenum)
351 351 return cmp(vala, valb)
352 352
353 353 try:
354 354 cand = self.hash[l]
355 355 except:
356 356 return []
357 357
358 358 if len(cand) > 1:
359 359 # resort our list of potentials forward then back.
360 360 cand.sort(cmp=sorter)
361 361 return cand
362 362
363 363 def hashlines(self):
364 364 self.hash = {}
365 365 for x in xrange(len(self.lines)):
366 366 s = self.lines[x]
367 367 self.hash.setdefault(s, []).append(x)
368 368
369 369 def write_rej(self):
370 370 # our rejects are a little different from patch(1). This always
371 371 # creates rejects in the same form as the original patch. A file
372 372 # header is inserted so that you can run the reject through patch again
373 373 # without having to type the filename.
374 374
375 375 if not self.rej:
376 376 return
377 377 if self.hunks != 1:
378 378 hunkstr = "s"
379 379 else:
380 380 hunkstr = ""
381 381
382 382 fname = self.fname + ".rej"
383 383 self.ui.warn(
384 384 _("%d out of %d hunk%s FAILED -- saving rejects to file %s\n") %
385 385 (len(self.rej), self.hunks, hunkstr, fname))
386 386 try: os.unlink(fname)
387 387 except:
388 388 pass
389 389 fp = file(fname, 'wb')
390 390 base = os.path.basename(self.fname)
391 391 fp.write("--- %s\n+++ %s\n" % (base, base))
392 392 for x in self.rej:
393 393 for l in x.hunk:
394 394 fp.write(l)
395 395 if l[-1] != '\n':
396 396 fp.write("\n\ No newline at end of file\n")
397 397
398 398 def write(self, dest=None):
399 399 if self.dirty:
400 400 if not dest:
401 401 dest = self.fname
402 402 st = None
403 403 try:
404 404 st = os.lstat(dest)
405 405 except OSError, inst:
406 406 if inst.errno != errno.ENOENT:
407 407 raise
408 408 if st and st.st_nlink > 1:
409 409 os.unlink(dest)
410 410 fp = file(dest, 'wb')
411 411 if st and st.st_nlink > 1:
412 412 os.chmod(dest, st.st_mode)
413 413 fp.writelines(self.lines)
414 414 fp.close()
415 415
416 416 def close(self):
417 417 self.write()
418 418 self.write_rej()
419 419
420 420 def apply(self, h, reverse):
421 421 if not h.complete():
422 422 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
423 423 (h.number, h.desc, len(h.a), h.lena, len(h.b),
424 424 h.lenb))
425 425
426 426 self.hunks += 1
427 427 if reverse:
428 428 h.reverse()
429 429
430 430 if self.exists and h.createfile():
431 431 self.ui.warn(_("file %s already exists\n") % self.fname)
432 432 self.rej.append(h)
433 433 return -1
434 434
435 435 if isinstance(h, binhunk):
436 436 if h.rmfile():
437 437 os.unlink(self.fname)
438 438 else:
439 439 self.lines[:] = h.new()
440 440 self.offset += len(h.new())
441 441 self.dirty = 1
442 442 return 0
443 443
444 444 # fast case first, no offsets, no fuzz
445 445 old = h.old()
446 446 # patch starts counting at 1 unless we are adding the file
447 447 if h.starta == 0:
448 448 start = 0
449 449 else:
450 450 start = h.starta + self.offset - 1
451 451 orig_start = start
452 452 if diffhelpers.testhunk(old, self.lines, start) == 0:
453 453 if h.rmfile():
454 454 os.unlink(self.fname)
455 455 else:
456 456 self.lines[start : start + h.lena] = h.new()
457 457 self.offset += h.lenb - h.lena
458 458 self.dirty = 1
459 459 return 0
460 460
461 461 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
462 462 self.hashlines()
463 463 if h.hunk[-1][0] != ' ':
464 464 # if the hunk tried to put something at the bottom of the file
465 465 # override the start line and use eof here
466 466 search_start = len(self.lines)
467 467 else:
468 468 search_start = orig_start
469 469
470 470 for fuzzlen in xrange(3):
471 471 for toponly in [ True, False ]:
472 472 old = h.old(fuzzlen, toponly)
473 473
474 474 cand = self.findlines(old[0][1:], search_start)
475 475 for l in cand:
476 476 if diffhelpers.testhunk(old, self.lines, l) == 0:
477 477 newlines = h.new(fuzzlen, toponly)
478 478 self.lines[l : l + len(old)] = newlines
479 479 self.offset += len(newlines) - len(old)
480 480 self.dirty = 1
481 481 if fuzzlen:
482 482 fuzzstr = "with fuzz %d " % fuzzlen
483 483 f = self.ui.warn
484 484 self.printfile(True)
485 485 else:
486 486 fuzzstr = ""
487 487 f = self.ui.note
488 488 offset = l - orig_start - fuzzlen
489 489 if offset == 1:
490 490 linestr = "line"
491 491 else:
492 492 linestr = "lines"
493 493 f(_("Hunk #%d succeeded at %d %s(offset %d %s).\n") %
494 494 (h.number, l+1, fuzzstr, offset, linestr))
495 495 return fuzzlen
496 496 self.printfile(True)
497 497 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
498 498 self.rej.append(h)
499 499 return -1
500 500
501 501 class hunk:
502 502 def __init__(self, desc, num, lr, context):
503 503 self.number = num
504 504 self.desc = desc
505 505 self.hunk = [ desc ]
506 506 self.a = []
507 507 self.b = []
508 508 if context:
509 509 self.read_context_hunk(lr)
510 510 else:
511 511 self.read_unified_hunk(lr)
512 512
513 513 def read_unified_hunk(self, lr):
514 514 m = unidesc.match(self.desc)
515 515 if not m:
516 516 raise PatchError(_("bad hunk #%d") % self.number)
517 517 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
518 518 if self.lena == None:
519 519 self.lena = 1
520 520 else:
521 521 self.lena = int(self.lena)
522 522 if self.lenb == None:
523 523 self.lenb = 1
524 524 else:
525 525 self.lenb = int(self.lenb)
526 526 self.starta = int(self.starta)
527 527 self.startb = int(self.startb)
528 528 diffhelpers.addlines(lr.fp, self.hunk, self.lena, self.lenb, self.a, self.b)
529 529 # if we hit eof before finishing out the hunk, the last line will
530 530 # be zero length. Lets try to fix it up.
531 531 while len(self.hunk[-1]) == 0:
532 532 del self.hunk[-1]
533 533 del self.a[-1]
534 534 del self.b[-1]
535 535 self.lena -= 1
536 536 self.lenb -= 1
537 537
538 538 def read_context_hunk(self, lr):
539 539 self.desc = lr.readline()
540 540 m = contextdesc.match(self.desc)
541 541 if not m:
542 542 raise PatchError(_("bad hunk #%d") % self.number)
543 543 foo, self.starta, foo2, aend, foo3 = m.groups()
544 544 self.starta = int(self.starta)
545 545 if aend == None:
546 546 aend = self.starta
547 547 self.lena = int(aend) - self.starta
548 548 if self.starta:
549 549 self.lena += 1
550 550 for x in xrange(self.lena):
551 551 l = lr.readline()
552 552 if l.startswith('---'):
553 553 lr.push(l)
554 554 break
555 555 s = l[2:]
556 556 if l.startswith('- ') or l.startswith('! '):
557 557 u = '-' + s
558 558 elif l.startswith(' '):
559 559 u = ' ' + s
560 560 else:
561 561 raise PatchError(_("bad hunk #%d old text line %d") %
562 562 (self.number, x))
563 563 self.a.append(u)
564 564 self.hunk.append(u)
565 565
566 566 l = lr.readline()
567 567 if l.startswith('\ '):
568 568 s = self.a[-1][:-1]
569 569 self.a[-1] = s
570 570 self.hunk[-1] = s
571 571 l = lr.readline()
572 572 m = contextdesc.match(l)
573 573 if not m:
574 574 raise PatchError(_("bad hunk #%d") % self.number)
575 575 foo, self.startb, foo2, bend, foo3 = m.groups()
576 576 self.startb = int(self.startb)
577 577 if bend == None:
578 578 bend = self.startb
579 579 self.lenb = int(bend) - self.startb
580 580 if self.startb:
581 581 self.lenb += 1
582 582 hunki = 1
583 583 for x in xrange(self.lenb):
584 584 l = lr.readline()
585 585 if l.startswith('\ '):
586 586 s = self.b[-1][:-1]
587 587 self.b[-1] = s
588 588 self.hunk[hunki-1] = s
589 589 continue
590 590 if not l:
591 591 lr.push(l)
592 592 break
593 593 s = l[2:]
594 594 if l.startswith('+ ') or l.startswith('! '):
595 595 u = '+' + s
596 596 elif l.startswith(' '):
597 597 u = ' ' + s
598 598 elif len(self.b) == 0:
599 599 # this can happen when the hunk does not add any lines
600 600 lr.push(l)
601 601 break
602 602 else:
603 603 raise PatchError(_("bad hunk #%d old text line %d") %
604 604 (self.number, x))
605 605 self.b.append(s)
606 606 while True:
607 607 if hunki >= len(self.hunk):
608 608 h = ""
609 609 else:
610 610 h = self.hunk[hunki]
611 611 hunki += 1
612 612 if h == u:
613 613 break
614 614 elif h.startswith('-'):
615 615 continue
616 616 else:
617 617 self.hunk.insert(hunki-1, u)
618 618 break
619 619
620 620 if not self.a:
621 621 # this happens when lines were only added to the hunk
622 622 for x in self.hunk:
623 623 if x.startswith('-') or x.startswith(' '):
624 624 self.a.append(x)
625 625 if not self.b:
626 626 # this happens when lines were only deleted from the hunk
627 627 for x in self.hunk:
628 628 if x.startswith('+') or x.startswith(' '):
629 629 self.b.append(x[1:])
630 630 # @@ -start,len +start,len @@
631 631 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
632 632 self.startb, self.lenb)
633 633 self.hunk[0] = self.desc
634 634
635 635 def reverse(self):
636 636 origlena = self.lena
637 637 origstarta = self.starta
638 638 self.lena = self.lenb
639 639 self.starta = self.startb
640 640 self.lenb = origlena
641 641 self.startb = origstarta
642 642 self.a = []
643 643 self.b = []
644 644 # self.hunk[0] is the @@ description
645 645 for x in xrange(1, len(self.hunk)):
646 646 o = self.hunk[x]
647 647 if o.startswith('-'):
648 648 n = '+' + o[1:]
649 649 self.b.append(o[1:])
650 650 elif o.startswith('+'):
651 651 n = '-' + o[1:]
652 652 self.a.append(n)
653 653 else:
654 654 n = o
655 655 self.b.append(o[1:])
656 656 self.a.append(o)
657 657 self.hunk[x] = o
658 658
659 659 def fix_newline(self):
660 660 diffhelpers.fix_newline(self.hunk, self.a, self.b)
661 661
662 662 def complete(self):
663 663 return len(self.a) == self.lena and len(self.b) == self.lenb
664 664
665 665 def createfile(self):
666 666 return self.starta == 0 and self.lena == 0
667 667
668 668 def rmfile(self):
669 669 return self.startb == 0 and self.lenb == 0
670 670
671 671 def fuzzit(self, l, fuzz, toponly):
672 672 # this removes context lines from the top and bottom of list 'l'. It
673 673 # checks the hunk to make sure only context lines are removed, and then
674 674 # returns a new shortened list of lines.
675 675 fuzz = min(fuzz, len(l)-1)
676 676 if fuzz:
677 677 top = 0
678 678 bot = 0
679 679 hlen = len(self.hunk)
680 680 for x in xrange(hlen-1):
681 681 # the hunk starts with the @@ line, so use x+1
682 682 if self.hunk[x+1][0] == ' ':
683 683 top += 1
684 684 else:
685 685 break
686 686 if not toponly:
687 687 for x in xrange(hlen-1):
688 688 if self.hunk[hlen-bot-1][0] == ' ':
689 689 bot += 1
690 690 else:
691 691 break
692 692
693 693 # top and bot now count context in the hunk
694 694 # adjust them if either one is short
695 695 context = max(top, bot, 3)
696 696 if bot < context:
697 697 bot = max(0, fuzz - (context - bot))
698 698 else:
699 699 bot = min(fuzz, bot)
700 700 if top < context:
701 701 top = max(0, fuzz - (context - top))
702 702 else:
703 703 top = min(fuzz, top)
704 704
705 705 return l[top:len(l)-bot]
706 706 return l
707 707
708 708 def old(self, fuzz=0, toponly=False):
709 709 return self.fuzzit(self.a, fuzz, toponly)
710 710
711 711 def newctrl(self):
712 712 res = []
713 713 for x in self.hunk:
714 714 c = x[0]
715 715 if c == ' ' or c == '+':
716 716 res.append(x)
717 717 return res
718 718
719 719 def new(self, fuzz=0, toponly=False):
720 720 return self.fuzzit(self.b, fuzz, toponly)
721 721
722 722 class binhunk:
723 723 'A binary patch file. Only understands literals so far.'
724 724 def __init__(self, gitpatch):
725 725 self.gitpatch = gitpatch
726 726 self.text = None
727 727 self.hunk = ['GIT binary patch\n']
728 728
729 729 def createfile(self):
730 730 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
731 731
732 732 def rmfile(self):
733 733 return self.gitpatch.op == 'DELETE'
734 734
735 735 def complete(self):
736 736 return self.text is not None
737 737
738 738 def new(self):
739 739 return [self.text]
740 740
741 741 def extract(self, fp):
742 742 line = fp.readline()
743 743 self.hunk.append(line)
744 744 while line and not line.startswith('literal '):
745 745 line = fp.readline()
746 746 self.hunk.append(line)
747 747 if not line:
748 748 raise PatchError(_('could not extract binary patch'))
749 749 size = int(line[8:].rstrip())
750 750 dec = []
751 751 line = fp.readline()
752 752 self.hunk.append(line)
753 753 while len(line) > 1:
754 754 l = line[0]
755 755 if l <= 'Z' and l >= 'A':
756 756 l = ord(l) - ord('A') + 1
757 757 else:
758 758 l = ord(l) - ord('a') + 27
759 759 dec.append(base85.b85decode(line[1:-1])[:l])
760 760 line = fp.readline()
761 761 self.hunk.append(line)
762 762 text = zlib.decompress(''.join(dec))
763 763 if len(text) != size:
764 764 raise PatchError(_('binary patch is %d bytes, not %d') %
765 765 len(text), size)
766 766 self.text = text
767 767
768 768 def parsefilename(str):
769 769 # --- filename \t|space stuff
770 770 s = str[4:]
771 771 i = s.find('\t')
772 772 if i < 0:
773 773 i = s.find(' ')
774 774 if i < 0:
775 775 return s
776 776 return s[:i]
777 777
778 778 def selectfile(afile_orig, bfile_orig, hunk, strip, reverse):
779 779 def pathstrip(path, count=1):
780 780 pathlen = len(path)
781 781 i = 0
782 782 if count == 0:
783 783 return path.rstrip()
784 784 while count > 0:
785 785 i = path.find('/', i)
786 786 if i == -1:
787 787 raise PatchError(_("unable to strip away %d dirs from %s") %
788 788 (count, path))
789 789 i += 1
790 790 # consume '//' in the path
791 791 while i < pathlen - 1 and path[i] == '/':
792 792 i += 1
793 793 count -= 1
794 794 return path[i:].rstrip()
795 795
796 796 nulla = afile_orig == "/dev/null"
797 797 nullb = bfile_orig == "/dev/null"
798 798 afile = pathstrip(afile_orig, strip)
799 799 gooda = os.path.exists(afile) and not nulla
800 800 bfile = pathstrip(bfile_orig, strip)
801 801 if afile == bfile:
802 802 goodb = gooda
803 803 else:
804 804 goodb = os.path.exists(bfile) and not nullb
805 805 createfunc = hunk.createfile
806 806 if reverse:
807 807 createfunc = hunk.rmfile
808 808 if not goodb and not gooda and not createfunc():
809 809 raise PatchError(_("unable to find %s or %s for patching") %
810 810 (afile, bfile))
811 811 if gooda and goodb:
812 812 fname = bfile
813 813 if afile in bfile:
814 814 fname = afile
815 815 elif gooda:
816 816 fname = afile
817 817 elif not nullb:
818 818 fname = bfile
819 819 if afile in bfile:
820 820 fname = afile
821 821 elif not nulla:
822 822 fname = afile
823 823 return fname
824 824
825 825 class linereader:
826 826 # simple class to allow pushing lines back into the input stream
827 827 def __init__(self, fp):
828 828 self.fp = fp
829 829 self.buf = []
830 830
831 831 def push(self, line):
832 832 self.buf.append(line)
833 833
834 834 def readline(self):
835 835 if self.buf:
836 836 l = self.buf[0]
837 837 del self.buf[0]
838 838 return l
839 839 return self.fp.readline()
840 840
841 841 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False,
842 842 rejmerge=None, updatedir=None):
843 843 """reads a patch from fp and tries to apply it. The dict 'changed' is
844 844 filled in with all of the filenames changed by the patch. Returns 0
845 845 for a clean patch, -1 if any rejects were found and 1 if there was
846 846 any fuzz."""
847 847
848 848 def scangitpatch(fp, firstline, cwd=None):
849 849 '''git patches can modify a file, then copy that file to
850 850 a new file, but expect the source to be the unmodified form.
851 851 So we scan the patch looking for that case so we can do
852 852 the copies ahead of time.'''
853 853
854 854 pos = 0
855 855 try:
856 856 pos = fp.tell()
857 857 except IOError:
858 858 fp = cStringIO.StringIO(fp.read())
859 859
860 860 (dopatch, gitpatches) = readgitpatch(fp, firstline)
861 861 for gp in gitpatches:
862 862 if gp.op in ('COPY', 'RENAME'):
863 863 copyfile(gp.oldpath, gp.path, basedir=cwd)
864 864
865 865 fp.seek(pos)
866 866
867 867 return fp, dopatch, gitpatches
868 868
869 869 current_hunk = None
870 870 current_file = None
871 871 afile = ""
872 872 bfile = ""
873 873 state = None
874 874 hunknum = 0
875 875 rejects = 0
876 876
877 877 git = False
878 878 gitre = re.compile('diff --git (a/.*) (b/.*)')
879 879
880 880 # our states
881 881 BFILE = 1
882 882 err = 0
883 883 context = None
884 884 lr = linereader(fp)
885 885 dopatch = True
886 886 gitworkdone = False
887 887
888 888 while True:
889 889 newfile = False
890 890 x = lr.readline()
891 891 if not x:
892 892 break
893 893 if current_hunk:
894 894 if x.startswith('\ '):
895 895 current_hunk.fix_newline()
896 896 ret = current_file.apply(current_hunk, reverse)
897 897 if ret >= 0:
898 898 changed.setdefault(current_file.fname, (None, None))
899 899 if ret > 0:
900 900 err = 1
901 901 current_hunk = None
902 902 gitworkdone = False
903 903 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
904 904 ((context or context == None) and x.startswith('***************')))):
905 905 try:
906 906 if context == None and x.startswith('***************'):
907 907 context = True
908 908 current_hunk = hunk(x, hunknum + 1, lr, context)
909 909 except PatchError, err:
910 910 ui.debug(err)
911 911 current_hunk = None
912 912 continue
913 913 hunknum += 1
914 914 if not current_file:
915 915 if sourcefile:
916 916 current_file = patchfile(ui, sourcefile)
917 917 else:
918 918 current_file = selectfile(afile, bfile, current_hunk,
919 919 strip, reverse)
920 920 current_file = patchfile(ui, current_file)
921 921 elif state == BFILE and x.startswith('GIT binary patch'):
922 922 current_hunk = binhunk(changed[bfile[2:]][1])
923 923 if not current_file:
924 924 if sourcefile:
925 925 current_file = patchfile(ui, sourcefile)
926 926 else:
927 927 current_file = selectfile(afile, bfile, current_hunk,
928 928 strip, reverse)
929 929 current_file = patchfile(ui, current_file)
930 930 hunknum += 1
931 931 current_hunk.extract(fp)
932 932 elif x.startswith('diff --git'):
933 933 # check for git diff, scanning the whole patch file if needed
934 934 m = gitre.match(x)
935 935 if m:
936 936 afile, bfile = m.group(1, 2)
937 937 if not git:
938 938 git = True
939 939 fp, dopatch, gitpatches = scangitpatch(fp, x)
940 940 for gp in gitpatches:
941 941 changed[gp.path] = (gp.op, gp)
942 942 # else error?
943 943 # copy/rename + modify should modify target, not source
944 944 if changed.get(bfile[2:], (None, None))[0] in ('COPY',
945 945 'RENAME'):
946 946 afile = bfile
947 947 gitworkdone = True
948 948 newfile = True
949 949 elif x.startswith('---'):
950 950 # check for a unified diff
951 951 l2 = lr.readline()
952 952 if not l2.startswith('+++'):
953 953 lr.push(l2)
954 954 continue
955 955 newfile = True
956 956 context = False
957 957 afile = parsefilename(x)
958 958 bfile = parsefilename(l2)
959 959 elif x.startswith('***'):
960 960 # check for a context diff
961 961 l2 = lr.readline()
962 962 if not l2.startswith('---'):
963 963 lr.push(l2)
964 964 continue
965 965 l3 = lr.readline()
966 966 lr.push(l3)
967 967 if not l3.startswith("***************"):
968 968 lr.push(l2)
969 969 continue
970 970 newfile = True
971 971 context = True
972 972 afile = parsefilename(x)
973 973 bfile = parsefilename(l2)
974 974
975 975 if newfile:
976 976 if current_file:
977 977 current_file.close()
978 978 if rejmerge:
979 979 rejmerge(current_file)
980 980 rejects += len(current_file.rej)
981 981 state = BFILE
982 982 current_file = None
983 983 hunknum = 0
984 984 if current_hunk:
985 985 if current_hunk.complete():
986 986 ret = current_file.apply(current_hunk, reverse)
987 987 if ret >= 0:
988 988 changed.setdefault(current_file.fname, (None, None))
989 989 if ret > 0:
990 990 err = 1
991 991 else:
992 992 fname = current_file and current_file.fname or None
993 993 raise PatchError(_("malformed patch %s %s") % (fname,
994 994 current_hunk.desc))
995 995 if current_file:
996 996 current_file.close()
997 997 if rejmerge:
998 998 rejmerge(current_file)
999 999 rejects += len(current_file.rej)
1000 1000 if updatedir and git:
1001 1001 updatedir(gitpatches)
1002 1002 if rejects:
1003 1003 return -1
1004 1004 if hunknum == 0 and dopatch and not gitworkdone:
1005 1005 raise NoHunks
1006 1006 return err
1007 1007
1008 1008 def diffopts(ui, opts={}, untrusted=False):
1009 1009 def get(key, name=None):
1010 1010 return (opts.get(key) or
1011 1011 ui.configbool('diff', name or key, None, untrusted=untrusted))
1012 1012 return mdiff.diffopts(
1013 1013 text=opts.get('text'),
1014 1014 git=get('git'),
1015 1015 nodates=get('nodates'),
1016 1016 showfunc=get('show_function', 'showfunc'),
1017 1017 ignorews=get('ignore_all_space', 'ignorews'),
1018 1018 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1019 1019 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
1020 1020
1021 1021 def updatedir(ui, repo, patches):
1022 1022 '''Update dirstate after patch application according to metadata'''
1023 1023 if not patches:
1024 1024 return
1025 1025 copies = []
1026 1026 removes = {}
1027 1027 cfiles = patches.keys()
1028 1028 cwd = repo.getcwd()
1029 1029 if cwd:
1030 1030 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1031 1031 for f in patches:
1032 1032 ctype, gp = patches[f]
1033 1033 if ctype == 'RENAME':
1034 1034 copies.append((gp.oldpath, gp.path))
1035 1035 removes[gp.oldpath] = 1
1036 1036 elif ctype == 'COPY':
1037 1037 copies.append((gp.oldpath, gp.path))
1038 1038 elif ctype == 'DELETE':
1039 1039 removes[gp.path] = 1
1040 1040 for src, dst in copies:
1041 1041 repo.copy(src, dst)
1042 1042 removes = removes.keys()
1043 1043 if removes:
1044 1044 removes.sort()
1045 1045 repo.remove(removes, True)
1046 1046 for f in patches:
1047 1047 ctype, gp = patches[f]
1048 1048 if gp and gp.mode:
1049 1049 x = gp.mode & 0100 != 0
1050 1050 l = gp.mode & 020000 != 0
1051 1051 dst = os.path.join(repo.root, gp.path)
1052 1052 # patch won't create empty files
1053 1053 if ctype == 'ADD' and not os.path.exists(dst):
1054 1054 repo.wwrite(gp.path, '', x and 'x' or '')
1055 1055 else:
1056 1056 util.set_link(dst, l)
1057 1057 if not l:
1058 1058 util.set_exec(dst, x)
1059 1059 cmdutil.addremove(repo, cfiles)
1060 1060 files = patches.keys()
1061 1061 files.extend([r for r in removes if r not in files])
1062 1062 files.sort()
1063 1063
1064 1064 return files
1065 1065
1066 1066 def b85diff(to, tn):
1067 1067 '''print base85-encoded binary diff'''
1068 1068 def gitindex(text):
1069 1069 if not text:
1070 1070 return '0' * 40
1071 1071 l = len(text)
1072 1072 s = sha.new('blob %d\0' % l)
1073 1073 s.update(text)
1074 1074 return s.hexdigest()
1075 1075
1076 1076 def fmtline(line):
1077 1077 l = len(line)
1078 1078 if l <= 26:
1079 1079 l = chr(ord('A') + l - 1)
1080 1080 else:
1081 1081 l = chr(l - 26 + ord('a') - 1)
1082 1082 return '%c%s\n' % (l, base85.b85encode(line, True))
1083 1083
1084 1084 def chunk(text, csize=52):
1085 1085 l = len(text)
1086 1086 i = 0
1087 1087 while i < l:
1088 1088 yield text[i:i+csize]
1089 1089 i += csize
1090 1090
1091 1091 tohash = gitindex(to)
1092 1092 tnhash = gitindex(tn)
1093 1093 if tohash == tnhash:
1094 1094 return ""
1095 1095
1096 1096 # TODO: deltas
1097 1097 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1098 1098 (tohash, tnhash, len(tn))]
1099 1099 for l in chunk(zlib.compress(tn)):
1100 1100 ret.append(fmtline(l))
1101 1101 ret.append('\n')
1102 1102 return ''.join(ret)
1103 1103
1104 1104 def diff(repo, node1=None, node2=None, files=None, match=util.always,
1105 1105 fp=None, changes=None, opts=None):
1106 1106 '''print diff of changes to files between two nodes, or node and
1107 1107 working directory.
1108 1108
1109 1109 if node1 is None, use first dirstate parent instead.
1110 1110 if node2 is None, compare node1 with working directory.'''
1111 1111
1112 1112 if opts is None:
1113 1113 opts = mdiff.defaultopts
1114 1114 if fp is None:
1115 1115 fp = repo.ui
1116 1116
1117 1117 if not node1:
1118 1118 node1 = repo.dirstate.parents()[0]
1119 1119
1120 1120 ccache = {}
1121 1121 def getctx(r):
1122 1122 if r not in ccache:
1123 1123 ccache[r] = context.changectx(repo, r)
1124 1124 return ccache[r]
1125 1125
1126 1126 flcache = {}
1127 1127 def getfilectx(f, ctx):
1128 1128 flctx = ctx.filectx(f, filelog=flcache.get(f))
1129 1129 if f not in flcache:
1130 1130 flcache[f] = flctx._filelog
1131 1131 return flctx
1132 1132
1133 1133 # reading the data for node1 early allows it to play nicely
1134 1134 # with repo.status and the revlog cache.
1135 1135 ctx1 = context.changectx(repo, node1)
1136 1136 # force manifest reading
1137 1137 man1 = ctx1.manifest()
1138 1138 date1 = util.datestr(ctx1.date())
1139 1139
1140 1140 if not changes:
1141 1141 changes = repo.status(node1, node2, files, match=match)[:5]
1142 1142 modified, added, removed, deleted, unknown = changes
1143 1143
1144 1144 if not modified and not added and not removed:
1145 1145 return
1146 1146
1147 1147 if node2:
1148 1148 ctx2 = context.changectx(repo, node2)
1149 1149 execf2 = ctx2.manifest().execf
1150 1150 linkf2 = ctx2.manifest().linkf
1151 1151 else:
1152 1152 ctx2 = context.workingctx(repo)
1153 1153 execf2 = util.execfunc(repo.root, None)
1154 1154 linkf2 = util.linkfunc(repo.root, None)
1155 1155 if execf2 is None:
1156 1156 mc = ctx2.parents()[0].manifest().copy()
1157 1157 execf2 = mc.execf
1158 1158 linkf2 = mc.linkf
1159 1159
1160 1160 # returns False if there was no rename between ctx1 and ctx2
1161 1161 # returns None if the file was created between ctx1 and ctx2
1162 1162 # returns the (file, node) present in ctx1 that was renamed to f in ctx2
1163 1163 # This will only really work if c1 is the Nth 1st parent of c2.
1164 1164 def renamed(c1, c2, man, f):
1165 1165 startrev = c1.rev()
1166 1166 c = c2
1167 1167 crev = c.rev()
1168 1168 if crev is None:
1169 1169 crev = repo.changelog.count()
1170 1170 orig = f
1171 1171 files = (f,)
1172 1172 while crev > startrev:
1173 1173 if f in files:
1174 1174 try:
1175 1175 src = getfilectx(f, c).renamed()
1176 1176 except revlog.LookupError:
1177 1177 return None
1178 1178 if src:
1179 1179 f = src[0]
1180 1180 crev = c.parents()[0].rev()
1181 1181 # try to reuse
1182 1182 c = getctx(crev)
1183 1183 files = c.files()
1184 1184 if f not in man:
1185 1185 return None
1186 1186 if f == orig:
1187 1187 return False
1188 1188 return f
1189 1189
1190 1190 if repo.ui.quiet:
1191 1191 r = None
1192 1192 else:
1193 1193 hexfunc = repo.ui.debugflag and hex or short
1194 1194 r = [hexfunc(node) for node in [node1, node2] if node]
1195 1195
1196 1196 if opts.git:
1197 1197 copied = {}
1198 1198 c1, c2 = ctx1, ctx2
1199 1199 files = added
1200 1200 man = man1
1201 1201 if node2 and ctx1.rev() >= ctx2.rev():
1202 1202 # renamed() starts at c2 and walks back in history until c1.
1203 1203 # Since ctx1.rev() >= ctx2.rev(), invert ctx2 and ctx1 to
1204 1204 # detect (inverted) copies.
1205 1205 c1, c2 = ctx2, ctx1
1206 1206 files = removed
1207 1207 man = ctx2.manifest()
1208 1208 for f in files:
1209 1209 src = renamed(c1, c2, man, f)
1210 1210 if src:
1211 1211 copied[f] = src
1212 1212 if ctx1 == c2:
1213 1213 # invert the copied dict
1214 1214 copied = dict([(v, k) for (k, v) in copied.iteritems()])
1215 1215 # If we've renamed file foo to bar (copied['bar'] = 'foo'),
1216 1216 # avoid showing a diff for foo if we're going to show
1217 1217 # the rename to bar.
1218 1218 srcs = [x[1] for x in copied.iteritems() if x[0] in added]
1219 1219
1220 1220 all = modified + added + removed
1221 1221 all.sort()
1222 1222 gone = {}
1223 1223
1224 1224 for f in all:
1225 1225 to = None
1226 1226 tn = None
1227 1227 dodiff = True
1228 1228 header = []
1229 1229 if f in man1:
1230 1230 to = getfilectx(f, ctx1).data()
1231 1231 if f not in removed:
1232 1232 tn = getfilectx(f, ctx2).data()
1233 1233 if opts.git:
1234 1234 def gitmode(x, l):
1235 1235 return l and '120000' or (x and '100755' or '100644')
1236 1236 def addmodehdr(header, omode, nmode):
1237 1237 if omode != nmode:
1238 1238 header.append('old mode %s\n' % omode)
1239 1239 header.append('new mode %s\n' % nmode)
1240 1240
1241 1241 a, b = f, f
1242 1242 if f in added:
1243 1243 mode = gitmode(execf2(f), linkf2(f))
1244 1244 if f in copied:
1245 1245 a = copied[f]
1246 1246 omode = gitmode(man1.execf(a), man1.linkf(a))
1247 1247 addmodehdr(header, omode, mode)
1248 1248 if a in removed and a not in gone:
1249 1249 op = 'rename'
1250 1250 gone[a] = 1
1251 1251 else:
1252 1252 op = 'copy'
1253 1253 header.append('%s from %s\n' % (op, a))
1254 1254 header.append('%s to %s\n' % (op, f))
1255 1255 to = getfilectx(a, ctx1).data()
1256 1256 else:
1257 1257 header.append('new file mode %s\n' % mode)
1258 1258 if util.binary(tn):
1259 1259 dodiff = 'binary'
1260 1260 elif f in removed:
1261 1261 if f in srcs:
1262 1262 dodiff = False
1263 1263 else:
1264 1264 mode = gitmode(man1.execf(f), man1.linkf(f))
1265 1265 header.append('deleted file mode %s\n' % mode)
1266 1266 else:
1267 1267 omode = gitmode(man1.execf(f), man1.linkf(f))
1268 1268 nmode = gitmode(execf2(f), linkf2(f))
1269 1269 addmodehdr(header, omode, nmode)
1270 1270 if util.binary(to) or util.binary(tn):
1271 1271 dodiff = 'binary'
1272 1272 r = None
1273 1273 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
1274 1274 if dodiff:
1275 1275 if dodiff == 'binary':
1276 1276 text = b85diff(to, tn)
1277 1277 else:
1278 1278 text = mdiff.unidiff(to, date1,
1279 1279 # ctx2 date may be dynamic
1280 1280 tn, util.datestr(ctx2.date()),
1281 1281 f, r, opts=opts)
1282 1282 if text or len(header) > 1:
1283 1283 fp.write(''.join(header))
1284 1284 fp.write(text)
1285 1285
1286 1286 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1287 1287 opts=None):
1288 1288 '''export changesets as hg patches.'''
1289 1289
1290 1290 total = len(revs)
1291 1291 revwidth = max([len(str(rev)) for rev in revs])
1292 1292
1293 1293 def single(rev, seqno, fp):
1294 1294 ctx = repo.changectx(rev)
1295 1295 node = ctx.node()
1296 1296 parents = [p.node() for p in ctx.parents() if p]
1297 1297 branch = ctx.branch()
1298 1298 if switch_parent:
1299 1299 parents.reverse()
1300 1300 prev = (parents and parents[0]) or nullid
1301 1301
1302 1302 if not fp:
1303 1303 fp = cmdutil.make_file(repo, template, node, total=total,
1304 1304 seqno=seqno, revwidth=revwidth)
1305 1305 if fp != sys.stdout and hasattr(fp, 'name'):
1306 1306 repo.ui.note("%s\n" % fp.name)
1307 1307
1308 1308 fp.write("# HG changeset patch\n")
1309 1309 fp.write("# User %s\n" % ctx.user())
1310 1310 fp.write("# Date %d %d\n" % ctx.date())
1311 1311 if branch and (branch != 'default'):
1312 1312 fp.write("# Branch %s\n" % branch)
1313 1313 fp.write("# Node ID %s\n" % hex(node))
1314 1314 fp.write("# Parent %s\n" % hex(prev))
1315 1315 if len(parents) > 1:
1316 1316 fp.write("# Parent %s\n" % hex(parents[1]))
1317 1317 fp.write(ctx.description().rstrip())
1318 1318 fp.write("\n\n")
1319 1319
1320 1320 diff(repo, prev, node, fp=fp, opts=opts)
1321 1321 if fp not in (sys.stdout, repo.ui):
1322 1322 fp.close()
1323 1323
1324 1324 for seqno, rev in enumerate(revs):
1325 1325 single(rev, seqno+1, fp)
1326 1326
1327 1327 def diffstat(patchlines):
1328 1328 if not util.find_exe('diffstat'):
1329 1329 return
1330 1330 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
1331 1331 try:
1332 1332 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
1333 1333 try:
1334 1334 for line in patchlines: print >> p.tochild, line
1335 1335 p.tochild.close()
1336 1336 if p.wait(): return
1337 1337 fp = os.fdopen(fd, 'r')
1338 1338 stat = []
1339 1339 for line in fp: stat.append(line.lstrip())
1340 1340 last = stat.pop()
1341 1341 stat.insert(0, last)
1342 1342 stat = ''.join(stat)
1343 1343 if stat.startswith('0 files'): raise ValueError
1344 1344 return stat
1345 1345 except: raise
1346 1346 finally:
1347 1347 try: os.unlink(name)
1348 1348 except: pass
@@ -1,1690 +1,1700
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 Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
6 6 Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
7 7
8 8 This software may be used and distributed according to the terms
9 9 of the GNU General Public License, incorporated herein by reference.
10 10
11 11 This contains helper routines that are independent of the SCM core and hide
12 12 platform-specific details from the core.
13 13 """
14 14
15 15 from i18n import _
16 16 import cStringIO, errno, getpass, popen2, re, shutil, sys, tempfile, strutil
17 17 import os, stat, threading, time, calendar, ConfigParser, locale, glob, osutil
18 18
19 19 try:
20 20 set = set
21 21 frozenset = frozenset
22 22 except NameError:
23 23 from sets import Set as set, ImmutableSet as frozenset
24 24
25 25 try:
26 26 _encoding = os.environ.get("HGENCODING")
27 27 if sys.platform == 'darwin' and not _encoding:
28 28 # On darwin, getpreferredencoding ignores the locale environment and
29 29 # always returns mac-roman. We override this if the environment is
30 30 # not C (has been customized by the user).
31 31 locale.setlocale(locale.LC_CTYPE, '')
32 32 _encoding = locale.getlocale()[1]
33 33 if not _encoding:
34 34 _encoding = locale.getpreferredencoding() or 'ascii'
35 35 except locale.Error:
36 36 _encoding = 'ascii'
37 37 _encodingmode = os.environ.get("HGENCODINGMODE", "strict")
38 38 _fallbackencoding = 'ISO-8859-1'
39 39
40 40 def tolocal(s):
41 41 """
42 42 Convert a string from internal UTF-8 to local encoding
43 43
44 44 All internal strings should be UTF-8 but some repos before the
45 45 implementation of locale support may contain latin1 or possibly
46 46 other character sets. We attempt to decode everything strictly
47 47 using UTF-8, then Latin-1, and failing that, we use UTF-8 and
48 48 replace unknown characters.
49 49 """
50 50 for e in ('UTF-8', _fallbackencoding):
51 51 try:
52 52 u = s.decode(e) # attempt strict decoding
53 53 return u.encode(_encoding, "replace")
54 54 except LookupError, k:
55 55 raise Abort(_("%s, please check your locale settings") % k)
56 56 except UnicodeDecodeError:
57 57 pass
58 58 u = s.decode("utf-8", "replace") # last ditch
59 59 return u.encode(_encoding, "replace")
60 60
61 61 def fromlocal(s):
62 62 """
63 63 Convert a string from the local character encoding to UTF-8
64 64
65 65 We attempt to decode strings using the encoding mode set by
66 66 HGENCODINGMODE, which defaults to 'strict'. In this mode, unknown
67 67 characters will cause an error message. Other modes include
68 68 'replace', which replaces unknown characters with a special
69 69 Unicode character, and 'ignore', which drops the character.
70 70 """
71 71 try:
72 72 return s.decode(_encoding, _encodingmode).encode("utf-8")
73 73 except UnicodeDecodeError, inst:
74 74 sub = s[max(0, inst.start-10):inst.start+10]
75 75 raise Abort("decoding near '%s': %s!" % (sub, inst))
76 76 except LookupError, k:
77 77 raise Abort(_("%s, please check your locale settings") % k)
78 78
79 79 def locallen(s):
80 80 """Find the length in characters of a local string"""
81 81 return len(s.decode(_encoding, "replace"))
82 82
83 83 def localsub(s, a, b=None):
84 84 try:
85 85 u = s.decode(_encoding, _encodingmode)
86 86 if b is not None:
87 87 u = u[a:b]
88 88 else:
89 89 u = u[:a]
90 90 return u.encode(_encoding, _encodingmode)
91 91 except UnicodeDecodeError, inst:
92 92 sub = s[max(0, inst.start-10), inst.start+10]
93 93 raise Abort(_("decoding near '%s': %s!") % (sub, inst))
94 94
95 95 # used by parsedate
96 96 defaultdateformats = (
97 97 '%Y-%m-%d %H:%M:%S',
98 98 '%Y-%m-%d %I:%M:%S%p',
99 99 '%Y-%m-%d %H:%M',
100 100 '%Y-%m-%d %I:%M%p',
101 101 '%Y-%m-%d',
102 102 '%m-%d',
103 103 '%m/%d',
104 104 '%m/%d/%y',
105 105 '%m/%d/%Y',
106 106 '%a %b %d %H:%M:%S %Y',
107 107 '%a %b %d %I:%M:%S%p %Y',
108 108 '%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822"
109 109 '%b %d %H:%M:%S %Y',
110 110 '%b %d %I:%M:%S%p %Y',
111 111 '%b %d %H:%M:%S',
112 112 '%b %d %I:%M:%S%p',
113 113 '%b %d %H:%M',
114 114 '%b %d %I:%M%p',
115 115 '%b %d %Y',
116 116 '%b %d',
117 117 '%H:%M:%S',
118 118 '%I:%M:%SP',
119 119 '%H:%M',
120 120 '%I:%M%p',
121 121 )
122 122
123 123 extendeddateformats = defaultdateformats + (
124 124 "%Y",
125 125 "%Y-%m",
126 126 "%b",
127 127 "%b %Y",
128 128 )
129 129
130 130 class SignalInterrupt(Exception):
131 131 """Exception raised on SIGTERM and SIGHUP."""
132 132
133 133 # differences from SafeConfigParser:
134 134 # - case-sensitive keys
135 135 # - allows values that are not strings (this means that you may not
136 136 # be able to save the configuration to a file)
137 137 class configparser(ConfigParser.SafeConfigParser):
138 138 def optionxform(self, optionstr):
139 139 return optionstr
140 140
141 141 def set(self, section, option, value):
142 142 return ConfigParser.ConfigParser.set(self, section, option, value)
143 143
144 144 def _interpolate(self, section, option, rawval, vars):
145 145 if not isinstance(rawval, basestring):
146 146 return rawval
147 147 return ConfigParser.SafeConfigParser._interpolate(self, section,
148 148 option, rawval, vars)
149 149
150 150 def cachefunc(func):
151 151 '''cache the result of function calls'''
152 152 # XXX doesn't handle keywords args
153 153 cache = {}
154 154 if func.func_code.co_argcount == 1:
155 155 # we gain a small amount of time because
156 156 # we don't need to pack/unpack the list
157 157 def f(arg):
158 158 if arg not in cache:
159 159 cache[arg] = func(arg)
160 160 return cache[arg]
161 161 else:
162 162 def f(*args):
163 163 if args not in cache:
164 164 cache[args] = func(*args)
165 165 return cache[args]
166 166
167 167 return f
168 168
169 169 def pipefilter(s, cmd):
170 170 '''filter string S through command CMD, returning its output'''
171 171 (pin, pout) = os.popen2(cmd, 'b')
172 172 def writer():
173 173 try:
174 174 pin.write(s)
175 175 pin.close()
176 176 except IOError, inst:
177 177 if inst.errno != errno.EPIPE:
178 178 raise
179 179
180 180 # we should use select instead on UNIX, but this will work on most
181 181 # systems, including Windows
182 182 w = threading.Thread(target=writer)
183 183 w.start()
184 184 f = pout.read()
185 185 pout.close()
186 186 w.join()
187 187 return f
188 188
189 189 def tempfilter(s, cmd):
190 190 '''filter string S through a pair of temporary files with CMD.
191 191 CMD is used as a template to create the real command to be run,
192 192 with the strings INFILE and OUTFILE replaced by the real names of
193 193 the temporary files generated.'''
194 194 inname, outname = None, None
195 195 try:
196 196 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
197 197 fp = os.fdopen(infd, 'wb')
198 198 fp.write(s)
199 199 fp.close()
200 200 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
201 201 os.close(outfd)
202 202 cmd = cmd.replace('INFILE', inname)
203 203 cmd = cmd.replace('OUTFILE', outname)
204 204 code = os.system(cmd)
205 205 if sys.platform == 'OpenVMS' and code & 1:
206 206 code = 0
207 207 if code: raise Abort(_("command '%s' failed: %s") %
208 208 (cmd, explain_exit(code)))
209 209 return open(outname, 'rb').read()
210 210 finally:
211 211 try:
212 212 if inname: os.unlink(inname)
213 213 except: pass
214 214 try:
215 215 if outname: os.unlink(outname)
216 216 except: pass
217 217
218 218 filtertable = {
219 219 'tempfile:': tempfilter,
220 220 'pipe:': pipefilter,
221 221 }
222 222
223 223 def filter(s, cmd):
224 224 "filter a string through a command that transforms its input to its output"
225 225 for name, fn in filtertable.iteritems():
226 226 if cmd.startswith(name):
227 227 return fn(s, cmd[len(name):].lstrip())
228 228 return pipefilter(s, cmd)
229 229
230 230 def binary(s):
231 231 """return true if a string is binary data using diff's heuristic"""
232 232 if s and '\0' in s[:4096]:
233 233 return True
234 234 return False
235 235
236 236 def unique(g):
237 237 """return the uniq elements of iterable g"""
238 238 seen = {}
239 239 l = []
240 240 for f in g:
241 241 if f not in seen:
242 242 seen[f] = 1
243 243 l.append(f)
244 244 return l
245 245
246 246 class Abort(Exception):
247 247 """Raised if a command needs to print an error and exit."""
248 248
249 249 class UnexpectedOutput(Abort):
250 250 """Raised to print an error with part of output and exit."""
251 251
252 252 def always(fn): return True
253 253 def never(fn): return False
254 254
255 255 def expand_glob(pats):
256 256 '''On Windows, expand the implicit globs in a list of patterns'''
257 257 if os.name != 'nt':
258 258 return list(pats)
259 259 ret = []
260 260 for p in pats:
261 261 kind, name = patkind(p, None)
262 262 if kind is None:
263 263 globbed = glob.glob(name)
264 264 if globbed:
265 265 ret.extend(globbed)
266 266 continue
267 267 # if we couldn't expand the glob, just keep it around
268 268 ret.append(p)
269 269 return ret
270 270
271 271 def patkind(name, dflt_pat='glob'):
272 272 """Split a string into an optional pattern kind prefix and the
273 273 actual pattern."""
274 274 for prefix in 're', 'glob', 'path', 'relglob', 'relpath', 'relre':
275 275 if name.startswith(prefix + ':'): return name.split(':', 1)
276 276 return dflt_pat, name
277 277
278 278 def globre(pat, head='^', tail='$'):
279 279 "convert a glob pattern into a regexp"
280 280 i, n = 0, len(pat)
281 281 res = ''
282 282 group = False
283 283 def peek(): return i < n and pat[i]
284 284 while i < n:
285 285 c = pat[i]
286 286 i = i+1
287 287 if c == '*':
288 288 if peek() == '*':
289 289 i += 1
290 290 res += '.*'
291 291 else:
292 292 res += '[^/]*'
293 293 elif c == '?':
294 294 res += '.'
295 295 elif c == '[':
296 296 j = i
297 297 if j < n and pat[j] in '!]':
298 298 j += 1
299 299 while j < n and pat[j] != ']':
300 300 j += 1
301 301 if j >= n:
302 302 res += '\\['
303 303 else:
304 304 stuff = pat[i:j].replace('\\','\\\\')
305 305 i = j + 1
306 306 if stuff[0] == '!':
307 307 stuff = '^' + stuff[1:]
308 308 elif stuff[0] == '^':
309 309 stuff = '\\' + stuff
310 310 res = '%s[%s]' % (res, stuff)
311 311 elif c == '{':
312 312 group = True
313 313 res += '(?:'
314 314 elif c == '}' and group:
315 315 res += ')'
316 316 group = False
317 317 elif c == ',' and group:
318 318 res += '|'
319 319 elif c == '\\':
320 320 p = peek()
321 321 if p:
322 322 i += 1
323 323 res += re.escape(p)
324 324 else:
325 325 res += re.escape(c)
326 326 else:
327 327 res += re.escape(c)
328 328 return head + res + tail
329 329
330 330 _globchars = {'[': 1, '{': 1, '*': 1, '?': 1}
331 331
332 332 def pathto(root, n1, n2):
333 333 '''return the relative path from one place to another.
334 334 root should use os.sep to separate directories
335 335 n1 should use os.sep to separate directories
336 336 n2 should use "/" to separate directories
337 337 returns an os.sep-separated path.
338 338
339 339 If n1 is a relative path, it's assumed it's
340 340 relative to root.
341 341 n2 should always be relative to root.
342 342 '''
343 343 if not n1: return localpath(n2)
344 344 if os.path.isabs(n1):
345 345 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
346 346 return os.path.join(root, localpath(n2))
347 347 n2 = '/'.join((pconvert(root), n2))
348 348 a, b = n1.split(os.sep), n2.split('/')
349 349 a.reverse()
350 350 b.reverse()
351 351 while a and b and a[-1] == b[-1]:
352 352 a.pop()
353 353 b.pop()
354 354 b.reverse()
355 355 return os.sep.join((['..'] * len(a)) + b)
356 356
357 357 def canonpath(root, cwd, myname):
358 358 """return the canonical path of myname, given cwd and root"""
359 359 if root == os.sep:
360 360 rootsep = os.sep
361 361 elif root.endswith(os.sep):
362 362 rootsep = root
363 363 else:
364 364 rootsep = root + os.sep
365 365 name = myname
366 366 if not os.path.isabs(name):
367 367 name = os.path.join(root, cwd, name)
368 368 name = os.path.normpath(name)
369 369 audit_path = path_auditor(root)
370 370 if name != rootsep and name.startswith(rootsep):
371 371 name = name[len(rootsep):]
372 372 audit_path(name)
373 373 return pconvert(name)
374 374 elif name == root:
375 375 return ''
376 376 else:
377 377 # Determine whether `name' is in the hierarchy at or beneath `root',
378 378 # by iterating name=dirname(name) until that causes no change (can't
379 379 # check name == '/', because that doesn't work on windows). For each
380 380 # `name', compare dev/inode numbers. If they match, the list `rel'
381 381 # holds the reversed list of components making up the relative file
382 382 # name we want.
383 383 root_st = os.stat(root)
384 384 rel = []
385 385 while True:
386 386 try:
387 387 name_st = os.stat(name)
388 388 except OSError:
389 389 break
390 390 if samestat(name_st, root_st):
391 391 if not rel:
392 392 # name was actually the same as root (maybe a symlink)
393 393 return ''
394 394 rel.reverse()
395 395 name = os.path.join(*rel)
396 396 audit_path(name)
397 397 return pconvert(name)
398 398 dirname, basename = os.path.split(name)
399 399 rel.append(basename)
400 400 if dirname == name:
401 401 break
402 402 name = dirname
403 403
404 404 raise Abort('%s not under root' % myname)
405 405
406 406 def matcher(canonroot, cwd='', names=[], inc=[], exc=[], src=None):
407 407 return _matcher(canonroot, cwd, names, inc, exc, 'glob', src)
408 408
409 409 def cmdmatcher(canonroot, cwd='', names=[], inc=[], exc=[], src=None,
410 410 globbed=False, default=None):
411 411 default = default or 'relpath'
412 412 if default == 'relpath' and not globbed:
413 413 names = expand_glob(names)
414 414 return _matcher(canonroot, cwd, names, inc, exc, default, src)
415 415
416 416 def _matcher(canonroot, cwd, names, inc, exc, dflt_pat, src):
417 417 """build a function to match a set of file patterns
418 418
419 419 arguments:
420 420 canonroot - the canonical root of the tree you're matching against
421 421 cwd - the current working directory, if relevant
422 422 names - patterns to find
423 423 inc - patterns to include
424 424 exc - patterns to exclude
425 425 dflt_pat - if a pattern in names has no explicit type, assume this one
426 426 src - where these patterns came from (e.g. .hgignore)
427 427
428 428 a pattern is one of:
429 429 'glob:<glob>' - a glob relative to cwd
430 430 're:<regexp>' - a regular expression
431 431 'path:<path>' - a path relative to canonroot
432 432 'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
433 433 'relpath:<path>' - a path relative to cwd
434 434 'relre:<regexp>' - a regexp that doesn't have to match the start of a name
435 435 '<something>' - one of the cases above, selected by the dflt_pat argument
436 436
437 437 returns:
438 438 a 3-tuple containing
439 439 - list of roots (places where one should start a recursive walk of the fs);
440 440 this often matches the explicit non-pattern names passed in, but also
441 441 includes the initial part of glob: patterns that has no glob characters
442 442 - a bool match(filename) function
443 443 - a bool indicating if any patterns were passed in
444 444 """
445 445
446 446 # a common case: no patterns at all
447 447 if not names and not inc and not exc:
448 448 return [], always, False
449 449
450 450 def contains_glob(name):
451 451 for c in name:
452 452 if c in _globchars: return True
453 453 return False
454 454
455 455 def regex(kind, name, tail):
456 456 '''convert a pattern into a regular expression'''
457 457 if not name:
458 458 return ''
459 459 if kind == 're':
460 460 return name
461 461 elif kind == 'path':
462 462 return '^' + re.escape(name) + '(?:/|$)'
463 463 elif kind == 'relglob':
464 464 return globre(name, '(?:|.*/)', tail)
465 465 elif kind == 'relpath':
466 466 return re.escape(name) + '(?:/|$)'
467 467 elif kind == 'relre':
468 468 if name.startswith('^'):
469 469 return name
470 470 return '.*' + name
471 471 return globre(name, '', tail)
472 472
473 473 def matchfn(pats, tail):
474 474 """build a matching function from a set of patterns"""
475 475 if not pats:
476 476 return
477 477 try:
478 478 pat = '(?:%s)' % '|'.join([regex(k, p, tail) for (k, p) in pats])
479 479 return re.compile(pat).match
480 480 except OverflowError:
481 481 # We're using a Python with a tiny regex engine and we
482 482 # made it explode, so we'll divide the pattern list in two
483 483 # until it works
484 484 l = len(pats)
485 485 if l < 2:
486 486 raise
487 487 a, b = matchfn(pats[:l//2], tail), matchfn(pats[l//2:], tail)
488 488 return lambda s: a(s) or b(s)
489 489 except re.error:
490 490 for k, p in pats:
491 491 try:
492 492 re.compile('(?:%s)' % regex(k, p, tail))
493 493 except re.error:
494 494 if src:
495 495 raise Abort("%s: invalid pattern (%s): %s" %
496 496 (src, k, p))
497 497 else:
498 498 raise Abort("invalid pattern (%s): %s" % (k, p))
499 499 raise Abort("invalid pattern")
500 500
501 501 def globprefix(pat):
502 502 '''return the non-glob prefix of a path, e.g. foo/* -> foo'''
503 503 root = []
504 504 for p in pat.split('/'):
505 505 if contains_glob(p): break
506 506 root.append(p)
507 507 return '/'.join(root) or '.'
508 508
509 509 def normalizepats(names, default):
510 510 pats = []
511 511 roots = []
512 512 anypats = False
513 513 for kind, name in [patkind(p, default) for p in names]:
514 514 if kind in ('glob', 'relpath'):
515 515 name = canonpath(canonroot, cwd, name)
516 516 elif kind in ('relglob', 'path'):
517 517 name = normpath(name)
518 518
519 519 pats.append((kind, name))
520 520
521 521 if kind in ('glob', 're', 'relglob', 'relre'):
522 522 anypats = True
523 523
524 524 if kind == 'glob':
525 525 root = globprefix(name)
526 526 roots.append(root)
527 527 elif kind in ('relpath', 'path'):
528 528 roots.append(name or '.')
529 529 elif kind == 'relglob':
530 530 roots.append('.')
531 531 return roots, pats, anypats
532 532
533 533 roots, pats, anypats = normalizepats(names, dflt_pat)
534 534
535 535 patmatch = matchfn(pats, '$') or always
536 536 incmatch = always
537 537 if inc:
538 538 dummy, inckinds, dummy = normalizepats(inc, 'glob')
539 539 incmatch = matchfn(inckinds, '(?:/|$)')
540 540 excmatch = lambda fn: False
541 541 if exc:
542 542 dummy, exckinds, dummy = normalizepats(exc, 'glob')
543 543 excmatch = matchfn(exckinds, '(?:/|$)')
544 544
545 545 if not names and inc and not exc:
546 546 # common case: hgignore patterns
547 547 match = incmatch
548 548 else:
549 549 match = lambda fn: incmatch(fn) and not excmatch(fn) and patmatch(fn)
550 550
551 551 return (roots, match, (inc or exc or anypats) and True)
552 552
553 553 _hgexecutable = None
554 554
555 555 def hgexecutable():
556 556 """return location of the 'hg' executable.
557 557
558 558 Defaults to $HG or 'hg' in the search path.
559 559 """
560 560 if _hgexecutable is None:
561 561 set_hgexecutable(os.environ.get('HG') or find_exe('hg', 'hg'))
562 562 return _hgexecutable
563 563
564 564 def set_hgexecutable(path):
565 565 """set location of the 'hg' executable"""
566 566 global _hgexecutable
567 567 _hgexecutable = path
568 568
569 569 def system(cmd, environ={}, cwd=None, onerr=None, errprefix=None):
570 570 '''enhanced shell command execution.
571 571 run with environment maybe modified, maybe in different dir.
572 572
573 573 if command fails and onerr is None, return status. if ui object,
574 574 print error message and return status, else raise onerr object as
575 575 exception.'''
576 576 def py2shell(val):
577 577 'convert python object into string that is useful to shell'
578 578 if val in (None, False):
579 579 return '0'
580 580 if val == True:
581 581 return '1'
582 582 return str(val)
583 583 oldenv = {}
584 584 for k in environ:
585 585 oldenv[k] = os.environ.get(k)
586 586 if cwd is not None:
587 587 oldcwd = os.getcwd()
588 588 origcmd = cmd
589 589 if os.name == 'nt':
590 590 cmd = '"%s"' % cmd
591 591 try:
592 592 for k, v in environ.iteritems():
593 593 os.environ[k] = py2shell(v)
594 594 os.environ['HG'] = hgexecutable()
595 595 if cwd is not None and oldcwd != cwd:
596 596 os.chdir(cwd)
597 597 rc = os.system(cmd)
598 598 if sys.platform == 'OpenVMS' and rc & 1:
599 599 rc = 0
600 600 if rc and onerr:
601 601 errmsg = '%s %s' % (os.path.basename(origcmd.split(None, 1)[0]),
602 602 explain_exit(rc)[0])
603 603 if errprefix:
604 604 errmsg = '%s: %s' % (errprefix, errmsg)
605 605 try:
606 606 onerr.warn(errmsg + '\n')
607 607 except AttributeError:
608 608 raise onerr(errmsg)
609 609 return rc
610 610 finally:
611 611 for k, v in oldenv.iteritems():
612 612 if v is None:
613 613 del os.environ[k]
614 614 else:
615 615 os.environ[k] = v
616 616 if cwd is not None and oldcwd != cwd:
617 617 os.chdir(oldcwd)
618 618
619 619 # os.path.lexists is not available on python2.3
620 620 def lexists(filename):
621 621 "test whether a file with this name exists. does not follow symlinks"
622 622 try:
623 623 os.lstat(filename)
624 624 except:
625 625 return False
626 626 return True
627 627
628 628 def rename(src, dst):
629 629 """forcibly rename a file"""
630 630 try:
631 631 os.rename(src, dst)
632 632 except OSError, err: # FIXME: check err (EEXIST ?)
633 633 # on windows, rename to existing file is not allowed, so we
634 634 # must delete destination first. but if file is open, unlink
635 635 # schedules it for delete but does not delete it. rename
636 636 # happens immediately even for open files, so we create
637 637 # temporary file, delete it, rename destination to that name,
638 638 # then delete that. then rename is safe to do.
639 639 fd, temp = tempfile.mkstemp(dir=os.path.dirname(dst) or '.')
640 640 os.close(fd)
641 641 os.unlink(temp)
642 642 os.rename(dst, temp)
643 643 os.unlink(temp)
644 644 os.rename(src, dst)
645 645
646 646 def unlink(f):
647 647 """unlink and remove the directory if it is empty"""
648 648 os.unlink(f)
649 649 # try removing directories that might now be empty
650 650 try:
651 651 os.removedirs(os.path.dirname(f))
652 652 except OSError:
653 653 pass
654 654
655 655 def copyfile(src, dest):
656 656 "copy a file, preserving mode"
657 657 if os.path.islink(src):
658 658 try:
659 659 os.unlink(dest)
660 660 except:
661 661 pass
662 662 os.symlink(os.readlink(src), dest)
663 663 else:
664 664 try:
665 665 shutil.copyfile(src, dest)
666 666 shutil.copymode(src, dest)
667 667 except shutil.Error, inst:
668 668 raise Abort(str(inst))
669 669
670 670 def copyfiles(src, dst, hardlink=None):
671 671 """Copy a directory tree using hardlinks if possible"""
672 672
673 673 if hardlink is None:
674 674 hardlink = (os.stat(src).st_dev ==
675 675 os.stat(os.path.dirname(dst)).st_dev)
676 676
677 677 if os.path.isdir(src):
678 678 os.mkdir(dst)
679 679 for name, kind in osutil.listdir(src):
680 680 srcname = os.path.join(src, name)
681 681 dstname = os.path.join(dst, name)
682 682 copyfiles(srcname, dstname, hardlink)
683 683 else:
684 684 if hardlink:
685 685 try:
686 686 os_link(src, dst)
687 687 except (IOError, OSError):
688 688 hardlink = False
689 689 shutil.copy(src, dst)
690 690 else:
691 691 shutil.copy(src, dst)
692 692
693 693 class path_auditor(object):
694 694 '''ensure that a filesystem path contains no banned components.
695 695 the following properties of a path are checked:
696 696
697 697 - under top-level .hg
698 698 - starts at the root of a windows drive
699 699 - contains ".."
700 700 - traverses a symlink (e.g. a/symlink_here/b)
701 701 - inside a nested repository'''
702 702
703 703 def __init__(self, root):
704 704 self.audited = set()
705 705 self.auditeddir = set()
706 706 self.root = root
707 707
708 708 def __call__(self, path):
709 709 if path in self.audited:
710 710 return
711 711 normpath = os.path.normcase(path)
712 712 parts = normpath.split(os.sep)
713 713 if (os.path.splitdrive(path)[0] or parts[0] in ('.hg', '')
714 714 or os.pardir in parts):
715 715 raise Abort(_("path contains illegal component: %s") % path)
716 716 def check(prefix):
717 717 curpath = os.path.join(self.root, prefix)
718 718 try:
719 719 st = os.lstat(curpath)
720 720 except OSError, err:
721 721 # EINVAL can be raised as invalid path syntax under win32.
722 722 # They must be ignored for patterns can be checked too.
723 723 if err.errno not in (errno.ENOENT, errno.EINVAL):
724 724 raise
725 725 else:
726 726 if stat.S_ISLNK(st.st_mode):
727 727 raise Abort(_('path %r traverses symbolic link %r') %
728 728 (path, prefix))
729 729 elif (stat.S_ISDIR(st.st_mode) and
730 730 os.path.isdir(os.path.join(curpath, '.hg'))):
731 731 raise Abort(_('path %r is inside repo %r') %
732 732 (path, prefix))
733 733
734 734 prefixes = []
735 735 for c in strutil.rfindall(normpath, os.sep):
736 736 prefix = normpath[:c]
737 737 if prefix in self.auditeddir:
738 738 break
739 739 check(prefix)
740 740 prefixes.append(prefix)
741 741
742 742 self.audited.add(path)
743 743 # only add prefixes to the cache after checking everything: we don't
744 744 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
745 745 self.auditeddir.update(prefixes)
746 746
747 747 def _makelock_file(info, pathname):
748 748 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
749 749 os.write(ld, info)
750 750 os.close(ld)
751 751
752 752 def _readlock_file(pathname):
753 753 return posixfile(pathname).read()
754 754
755 755 def nlinks(pathname):
756 756 """Return number of hardlinks for the given file."""
757 757 return os.lstat(pathname).st_nlink
758 758
759 759 if hasattr(os, 'link'):
760 760 os_link = os.link
761 761 else:
762 762 def os_link(src, dst):
763 763 raise OSError(0, _("Hardlinks not supported"))
764 764
765 765 def fstat(fp):
766 766 '''stat file object that may not have fileno method.'''
767 767 try:
768 768 return os.fstat(fp.fileno())
769 769 except AttributeError:
770 770 return os.stat(fp.name)
771 771
772 772 posixfile = file
773 773
774 774 def is_win_9x():
775 775 '''return true if run on windows 95, 98 or me.'''
776 776 try:
777 777 return sys.getwindowsversion()[3] == 1
778 778 except AttributeError:
779 779 return os.name == 'nt' and 'command' in os.environ.get('comspec', '')
780 780
781 781 getuser_fallback = None
782 782
783 783 def getuser():
784 784 '''return name of current user'''
785 785 try:
786 786 return getpass.getuser()
787 787 except ImportError:
788 788 # import of pwd will fail on windows - try fallback
789 789 if getuser_fallback:
790 790 return getuser_fallback()
791 791 # raised if win32api not available
792 792 raise Abort(_('user name not available - set USERNAME '
793 793 'environment variable'))
794 794
795 795 def username(uid=None):
796 796 """Return the name of the user with the given uid.
797 797
798 798 If uid is None, return the name of the current user."""
799 799 try:
800 800 import pwd
801 801 if uid is None:
802 802 uid = os.getuid()
803 803 try:
804 804 return pwd.getpwuid(uid)[0]
805 805 except KeyError:
806 806 return str(uid)
807 807 except ImportError:
808 808 return None
809 809
810 810 def groupname(gid=None):
811 811 """Return the name of the group with the given gid.
812 812
813 813 If gid is None, return the name of the current group."""
814 814 try:
815 815 import grp
816 816 if gid is None:
817 817 gid = os.getgid()
818 818 try:
819 819 return grp.getgrgid(gid)[0]
820 820 except KeyError:
821 821 return str(gid)
822 822 except ImportError:
823 823 return None
824 824
825 825 # File system features
826 826
827 827 def checkfolding(path):
828 828 """
829 829 Check whether the given path is on a case-sensitive filesystem
830 830
831 831 Requires a path (like /foo/.hg) ending with a foldable final
832 832 directory component.
833 833 """
834 834 s1 = os.stat(path)
835 835 d, b = os.path.split(path)
836 836 p2 = os.path.join(d, b.upper())
837 837 if path == p2:
838 838 p2 = os.path.join(d, b.lower())
839 839 try:
840 840 s2 = os.stat(p2)
841 841 if s2 == s1:
842 842 return False
843 843 return True
844 844 except:
845 845 return True
846 846
847 847 def checkexec(path):
848 848 """
849 849 Check whether the given path is on a filesystem with UNIX-like exec flags
850 850
851 851 Requires a directory (like /foo/.hg)
852 852 """
853 853 try:
854 854 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
855 855 fh, fn = tempfile.mkstemp("", "", path)
856 856 os.close(fh)
857 857 m = os.stat(fn).st_mode
858 858 # VFAT on Linux can flip mode but it doesn't persist a FS remount.
859 859 # frequently we can detect it if files are created with exec bit on.
860 860 new_file_has_exec = m & EXECFLAGS
861 861 os.chmod(fn, m ^ EXECFLAGS)
862 862 exec_flags_cannot_flip = (os.stat(fn).st_mode == m)
863 863 os.unlink(fn)
864 864 except (IOError,OSError):
865 865 # we don't care, the user probably won't be able to commit anyway
866 866 return False
867 867 return not (new_file_has_exec or exec_flags_cannot_flip)
868 868
869 869 def execfunc(path, fallback):
870 870 '''return an is_exec() function with default to fallback'''
871 871 if checkexec(path):
872 872 return lambda x: is_exec(os.path.join(path, x))
873 873 return fallback
874 874
875 875 def checklink(path):
876 876 """check whether the given path is on a symlink-capable filesystem"""
877 877 # mktemp is not racy because symlink creation will fail if the
878 878 # file already exists
879 879 name = tempfile.mktemp(dir=path)
880 880 try:
881 881 os.symlink(".", name)
882 882 os.unlink(name)
883 883 return True
884 884 except (OSError, AttributeError):
885 885 return False
886 886
887 887 def linkfunc(path, fallback):
888 888 '''return an is_link() function with default to fallback'''
889 889 if checklink(path):
890 890 return lambda x: os.path.islink(os.path.join(path, x))
891 891 return fallback
892 892
893 893 _umask = os.umask(0)
894 894 os.umask(_umask)
895 895
896 896 def needbinarypatch():
897 897 """return True if patches should be applied in binary mode by default."""
898 898 return os.name == 'nt'
899 899
900 900 # Platform specific variants
901 901 if os.name == 'nt':
902 902 import msvcrt
903 903 nulldev = 'NUL:'
904 904
905 905 class winstdout:
906 906 '''stdout on windows misbehaves if sent through a pipe'''
907 907
908 908 def __init__(self, fp):
909 909 self.fp = fp
910 910
911 911 def __getattr__(self, key):
912 912 return getattr(self.fp, key)
913 913
914 914 def close(self):
915 915 try:
916 916 self.fp.close()
917 917 except: pass
918 918
919 919 def write(self, s):
920 920 try:
921 921 return self.fp.write(s)
922 922 except IOError, inst:
923 923 if inst.errno != 0: raise
924 924 self.close()
925 925 raise IOError(errno.EPIPE, 'Broken pipe')
926 926
927 927 def flush(self):
928 928 try:
929 929 return self.fp.flush()
930 930 except IOError, inst:
931 931 if inst.errno != errno.EINVAL: raise
932 932 self.close()
933 933 raise IOError(errno.EPIPE, 'Broken pipe')
934 934
935 935 sys.stdout = winstdout(sys.stdout)
936 936
937 937 def system_rcpath():
938 938 try:
939 939 return system_rcpath_win32()
940 940 except:
941 941 return [r'c:\mercurial\mercurial.ini']
942 942
943 943 def user_rcpath():
944 944 '''return os-specific hgrc search path to the user dir'''
945 945 try:
946 946 userrc = user_rcpath_win32()
947 947 except:
948 948 userrc = os.path.join(os.path.expanduser('~'), 'mercurial.ini')
949 949 path = [userrc]
950 950 userprofile = os.environ.get('USERPROFILE')
951 951 if userprofile:
952 952 path.append(os.path.join(userprofile, 'mercurial.ini'))
953 953 return path
954 954
955 955 def parse_patch_output(output_line):
956 956 """parses the output produced by patch and returns the file name"""
957 957 pf = output_line[14:]
958 958 if pf[0] == '`':
959 959 pf = pf[1:-1] # Remove the quotes
960 960 return pf
961 961
962 962 def testpid(pid):
963 963 '''return False if pid dead, True if running or not known'''
964 964 return True
965 965
966 966 def set_exec(f, mode):
967 967 pass
968 968
969 969 def set_link(f, mode):
970 970 pass
971 971
972 972 def set_binary(fd):
973 973 msvcrt.setmode(fd.fileno(), os.O_BINARY)
974 974
975 975 def pconvert(path):
976 976 return path.replace("\\", "/")
977 977
978 978 def localpath(path):
979 979 return path.replace('/', '\\')
980 980
981 981 def normpath(path):
982 982 return pconvert(os.path.normpath(path))
983 983
984 984 makelock = _makelock_file
985 985 readlock = _readlock_file
986 986
987 987 def samestat(s1, s2):
988 988 return False
989 989
990 990 # A sequence of backslashes is special iff it precedes a double quote:
991 991 # - if there's an even number of backslashes, the double quote is not
992 992 # quoted (i.e. it ends the quoted region)
993 993 # - if there's an odd number of backslashes, the double quote is quoted
994 994 # - in both cases, every pair of backslashes is unquoted into a single
995 995 # backslash
996 996 # (See http://msdn2.microsoft.com/en-us/library/a1y7w461.aspx )
997 997 # So, to quote a string, we must surround it in double quotes, double
998 998 # the number of backslashes that preceed double quotes and add another
999 999 # backslash before every double quote (being careful with the double
1000 1000 # quote we've appended to the end)
1001 1001 _quotere = None
1002 1002 def shellquote(s):
1003 1003 global _quotere
1004 1004 if _quotere is None:
1005 1005 _quotere = re.compile(r'(\\*)("|\\$)')
1006 1006 return '"%s"' % _quotere.sub(r'\1\1\\\2', s)
1007 1007
1008 1008 def quotecommand(cmd):
1009 1009 """Build a command string suitable for os.popen* calls."""
1010 1010 # The extra quotes are needed because popen* runs the command
1011 1011 # through the current COMSPEC. cmd.exe suppress enclosing quotes.
1012 1012 return '"' + cmd + '"'
1013 1013
1014 def popen(command):
1015 # Work around "popen spawned process may not write to stdout
1016 # under windows"
1017 # http://bugs.python.org/issue1366
1018 command += " 2> %s" % nulldev
1019 return os.popen(quotecommand(command))
1020
1014 1021 def explain_exit(code):
1015 1022 return _("exited with status %d") % code, code
1016 1023
1017 1024 # if you change this stub into a real check, please try to implement the
1018 1025 # username and groupname functions above, too.
1019 1026 def isowner(fp, st=None):
1020 1027 return True
1021 1028
1022 1029 def find_in_path(name, path, default=None):
1023 1030 '''find name in search path. path can be string (will be split
1024 1031 with os.pathsep), or iterable thing that returns strings. if name
1025 1032 found, return path to name. else return default. name is looked up
1026 1033 using cmd.exe rules, using PATHEXT.'''
1027 1034 if isinstance(path, str):
1028 1035 path = path.split(os.pathsep)
1029 1036
1030 1037 pathext = os.environ.get('PATHEXT', '.COM;.EXE;.BAT;.CMD')
1031 1038 pathext = pathext.lower().split(os.pathsep)
1032 1039 isexec = os.path.splitext(name)[1].lower() in pathext
1033 1040
1034 1041 for p in path:
1035 1042 p_name = os.path.join(p, name)
1036 1043
1037 1044 if isexec and os.path.exists(p_name):
1038 1045 return p_name
1039 1046
1040 1047 for ext in pathext:
1041 1048 p_name_ext = p_name + ext
1042 1049 if os.path.exists(p_name_ext):
1043 1050 return p_name_ext
1044 1051 return default
1045 1052
1046 1053 def set_signal_handler():
1047 1054 try:
1048 1055 set_signal_handler_win32()
1049 1056 except NameError:
1050 1057 pass
1051 1058
1052 1059 try:
1053 1060 # override functions with win32 versions if possible
1054 1061 from util_win32 import *
1055 1062 if not is_win_9x():
1056 1063 posixfile = posixfile_nt
1057 1064 except ImportError:
1058 1065 pass
1059 1066
1060 1067 else:
1061 1068 nulldev = '/dev/null'
1062 1069
1063 1070 def rcfiles(path):
1064 1071 rcs = [os.path.join(path, 'hgrc')]
1065 1072 rcdir = os.path.join(path, 'hgrc.d')
1066 1073 try:
1067 1074 rcs.extend([os.path.join(rcdir, f)
1068 1075 for f, kind in osutil.listdir(rcdir)
1069 1076 if f.endswith(".rc")])
1070 1077 except OSError:
1071 1078 pass
1072 1079 return rcs
1073 1080
1074 1081 def system_rcpath():
1075 1082 path = []
1076 1083 # old mod_python does not set sys.argv
1077 1084 if len(getattr(sys, 'argv', [])) > 0:
1078 1085 path.extend(rcfiles(os.path.dirname(sys.argv[0]) +
1079 1086 '/../etc/mercurial'))
1080 1087 path.extend(rcfiles('/etc/mercurial'))
1081 1088 return path
1082 1089
1083 1090 def user_rcpath():
1084 1091 return [os.path.expanduser('~/.hgrc')]
1085 1092
1086 1093 def parse_patch_output(output_line):
1087 1094 """parses the output produced by patch and returns the file name"""
1088 1095 pf = output_line[14:]
1089 1096 if os.sys.platform == 'OpenVMS':
1090 1097 if pf[0] == '`':
1091 1098 pf = pf[1:-1] # Remove the quotes
1092 1099 else:
1093 1100 if pf.startswith("'") and pf.endswith("'") and " " in pf:
1094 1101 pf = pf[1:-1] # Remove the quotes
1095 1102 return pf
1096 1103
1097 1104 def is_exec(f):
1098 1105 """check whether a file is executable"""
1099 1106 return (os.lstat(f).st_mode & 0100 != 0)
1100 1107
1101 1108 def set_exec(f, mode):
1102 1109 s = os.lstat(f).st_mode
1103 1110 if stat.S_ISLNK(s) or (s & 0100 != 0) == mode:
1104 1111 return
1105 1112 if mode:
1106 1113 # Turn on +x for every +r bit when making a file executable
1107 1114 # and obey umask.
1108 1115 os.chmod(f, s | (s & 0444) >> 2 & ~_umask)
1109 1116 else:
1110 1117 os.chmod(f, s & 0666)
1111 1118
1112 1119 def set_link(f, mode):
1113 1120 """make a file a symbolic link/regular file
1114 1121
1115 1122 if a file is changed to a link, its contents become the link data
1116 1123 if a link is changed to a file, its link data become its contents
1117 1124 """
1118 1125
1119 1126 m = os.path.islink(f)
1120 1127 if m == bool(mode):
1121 1128 return
1122 1129
1123 1130 if mode: # switch file to link
1124 1131 data = file(f).read()
1125 1132 os.unlink(f)
1126 1133 os.symlink(data, f)
1127 1134 else:
1128 1135 data = os.readlink(f)
1129 1136 os.unlink(f)
1130 1137 file(f, "w").write(data)
1131 1138
1132 1139 def set_binary(fd):
1133 1140 pass
1134 1141
1135 1142 def pconvert(path):
1136 1143 return path
1137 1144
1138 1145 def localpath(path):
1139 1146 return path
1140 1147
1141 1148 normpath = os.path.normpath
1142 1149 samestat = os.path.samestat
1143 1150
1144 1151 def makelock(info, pathname):
1145 1152 try:
1146 1153 os.symlink(info, pathname)
1147 1154 except OSError, why:
1148 1155 if why.errno == errno.EEXIST:
1149 1156 raise
1150 1157 else:
1151 1158 _makelock_file(info, pathname)
1152 1159
1153 1160 def readlock(pathname):
1154 1161 try:
1155 1162 return os.readlink(pathname)
1156 1163 except OSError, why:
1157 1164 if why.errno in (errno.EINVAL, errno.ENOSYS):
1158 1165 return _readlock_file(pathname)
1159 1166 else:
1160 1167 raise
1161 1168
1162 1169 def shellquote(s):
1163 1170 if os.sys.platform == 'OpenVMS':
1164 1171 return '"%s"' % s
1165 1172 else:
1166 1173 return "'%s'" % s.replace("'", "'\\''")
1167 1174
1168 1175 def quotecommand(cmd):
1169 1176 return cmd
1170 1177
1178 def popen(command):
1179 return os.popen(command)
1180
1171 1181 def testpid(pid):
1172 1182 '''return False if pid dead, True if running or not sure'''
1173 1183 if os.sys.platform == 'OpenVMS':
1174 1184 return True
1175 1185 try:
1176 1186 os.kill(pid, 0)
1177 1187 return True
1178 1188 except OSError, inst:
1179 1189 return inst.errno != errno.ESRCH
1180 1190
1181 1191 def explain_exit(code):
1182 1192 """return a 2-tuple (desc, code) describing a process's status"""
1183 1193 if os.WIFEXITED(code):
1184 1194 val = os.WEXITSTATUS(code)
1185 1195 return _("exited with status %d") % val, val
1186 1196 elif os.WIFSIGNALED(code):
1187 1197 val = os.WTERMSIG(code)
1188 1198 return _("killed by signal %d") % val, val
1189 1199 elif os.WIFSTOPPED(code):
1190 1200 val = os.WSTOPSIG(code)
1191 1201 return _("stopped by signal %d") % val, val
1192 1202 raise ValueError(_("invalid exit code"))
1193 1203
1194 1204 def isowner(fp, st=None):
1195 1205 """Return True if the file object f belongs to the current user.
1196 1206
1197 1207 The return value of a util.fstat(f) may be passed as the st argument.
1198 1208 """
1199 1209 if st is None:
1200 1210 st = fstat(fp)
1201 1211 return st.st_uid == os.getuid()
1202 1212
1203 1213 def find_in_path(name, path, default=None):
1204 1214 '''find name in search path. path can be string (will be split
1205 1215 with os.pathsep), or iterable thing that returns strings. if name
1206 1216 found, return path to name. else return default.'''
1207 1217 if isinstance(path, str):
1208 1218 path = path.split(os.pathsep)
1209 1219 for p in path:
1210 1220 p_name = os.path.join(p, name)
1211 1221 if os.path.exists(p_name):
1212 1222 return p_name
1213 1223 return default
1214 1224
1215 1225 def set_signal_handler():
1216 1226 pass
1217 1227
1218 1228 def find_exe(name, default=None):
1219 1229 '''find path of an executable.
1220 1230 if name contains a path component, return it as is. otherwise,
1221 1231 use normal executable search path.'''
1222 1232
1223 1233 if os.sep in name or sys.platform == 'OpenVMS':
1224 1234 # don't check the executable bit. if the file isn't
1225 1235 # executable, whoever tries to actually run it will give a
1226 1236 # much more useful error message.
1227 1237 return name
1228 1238 return find_in_path(name, os.environ.get('PATH', ''), default=default)
1229 1239
1230 1240 def _buildencodefun():
1231 1241 e = '_'
1232 1242 win_reserved = [ord(x) for x in '\\:*?"<>|']
1233 1243 cmap = dict([ (chr(x), chr(x)) for x in xrange(127) ])
1234 1244 for x in (range(32) + range(126, 256) + win_reserved):
1235 1245 cmap[chr(x)] = "~%02x" % x
1236 1246 for x in range(ord("A"), ord("Z")+1) + [ord(e)]:
1237 1247 cmap[chr(x)] = e + chr(x).lower()
1238 1248 dmap = {}
1239 1249 for k, v in cmap.iteritems():
1240 1250 dmap[v] = k
1241 1251 def decode(s):
1242 1252 i = 0
1243 1253 while i < len(s):
1244 1254 for l in xrange(1, 4):
1245 1255 try:
1246 1256 yield dmap[s[i:i+l]]
1247 1257 i += l
1248 1258 break
1249 1259 except KeyError:
1250 1260 pass
1251 1261 else:
1252 1262 raise KeyError
1253 1263 return (lambda s: "".join([cmap[c] for c in s]),
1254 1264 lambda s: "".join(list(decode(s))))
1255 1265
1256 1266 encodefilename, decodefilename = _buildencodefun()
1257 1267
1258 1268 def encodedopener(openerfn, fn):
1259 1269 def o(path, *args, **kw):
1260 1270 return openerfn(fn(path), *args, **kw)
1261 1271 return o
1262 1272
1263 1273 def mktempcopy(name, emptyok=False):
1264 1274 """Create a temporary file with the same contents from name
1265 1275
1266 1276 The permission bits are copied from the original file.
1267 1277
1268 1278 If the temporary file is going to be truncated immediately, you
1269 1279 can use emptyok=True as an optimization.
1270 1280
1271 1281 Returns the name of the temporary file.
1272 1282 """
1273 1283 d, fn = os.path.split(name)
1274 1284 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
1275 1285 os.close(fd)
1276 1286 # Temporary files are created with mode 0600, which is usually not
1277 1287 # what we want. If the original file already exists, just copy
1278 1288 # its mode. Otherwise, manually obey umask.
1279 1289 try:
1280 1290 st_mode = os.lstat(name).st_mode
1281 1291 except OSError, inst:
1282 1292 if inst.errno != errno.ENOENT:
1283 1293 raise
1284 1294 st_mode = 0666 & ~_umask
1285 1295 os.chmod(temp, st_mode)
1286 1296 if emptyok:
1287 1297 return temp
1288 1298 try:
1289 1299 try:
1290 1300 ifp = posixfile(name, "rb")
1291 1301 except IOError, inst:
1292 1302 if inst.errno == errno.ENOENT:
1293 1303 return temp
1294 1304 if not getattr(inst, 'filename', None):
1295 1305 inst.filename = name
1296 1306 raise
1297 1307 ofp = posixfile(temp, "wb")
1298 1308 for chunk in filechunkiter(ifp):
1299 1309 ofp.write(chunk)
1300 1310 ifp.close()
1301 1311 ofp.close()
1302 1312 except:
1303 1313 try: os.unlink(temp)
1304 1314 except: pass
1305 1315 raise
1306 1316 return temp
1307 1317
1308 1318 class atomictempfile(posixfile):
1309 1319 """file-like object that atomically updates a file
1310 1320
1311 1321 All writes will be redirected to a temporary copy of the original
1312 1322 file. When rename is called, the copy is renamed to the original
1313 1323 name, making the changes visible.
1314 1324 """
1315 1325 def __init__(self, name, mode):
1316 1326 self.__name = name
1317 1327 self.temp = mktempcopy(name, emptyok=('w' in mode))
1318 1328 posixfile.__init__(self, self.temp, mode)
1319 1329
1320 1330 def rename(self):
1321 1331 if not self.closed:
1322 1332 posixfile.close(self)
1323 1333 rename(self.temp, localpath(self.__name))
1324 1334
1325 1335 def __del__(self):
1326 1336 if not self.closed:
1327 1337 try:
1328 1338 os.unlink(self.temp)
1329 1339 except: pass
1330 1340 posixfile.close(self)
1331 1341
1332 1342 class opener(object):
1333 1343 """Open files relative to a base directory
1334 1344
1335 1345 This class is used to hide the details of COW semantics and
1336 1346 remote file access from higher level code.
1337 1347 """
1338 1348 def __init__(self, base, audit=True):
1339 1349 self.base = base
1340 1350 if audit:
1341 1351 self.audit_path = path_auditor(base)
1342 1352 else:
1343 1353 self.audit_path = always
1344 1354
1345 1355 def __getattr__(self, name):
1346 1356 if name == '_can_symlink':
1347 1357 self._can_symlink = checklink(self.base)
1348 1358 return self._can_symlink
1349 1359 raise AttributeError(name)
1350 1360
1351 1361 def __call__(self, path, mode="r", text=False, atomictemp=False):
1352 1362 self.audit_path(path)
1353 1363 f = os.path.join(self.base, path)
1354 1364
1355 1365 if not text and "b" not in mode:
1356 1366 mode += "b" # for that other OS
1357 1367
1358 1368 if mode[0] != "r":
1359 1369 try:
1360 1370 nlink = nlinks(f)
1361 1371 except OSError:
1362 1372 nlink = 0
1363 1373 d = os.path.dirname(f)
1364 1374 if not os.path.isdir(d):
1365 1375 os.makedirs(d)
1366 1376 if atomictemp:
1367 1377 return atomictempfile(f, mode)
1368 1378 if nlink > 1:
1369 1379 rename(mktempcopy(f), f)
1370 1380 return posixfile(f, mode)
1371 1381
1372 1382 def symlink(self, src, dst):
1373 1383 self.audit_path(dst)
1374 1384 linkname = os.path.join(self.base, dst)
1375 1385 try:
1376 1386 os.unlink(linkname)
1377 1387 except OSError:
1378 1388 pass
1379 1389
1380 1390 dirname = os.path.dirname(linkname)
1381 1391 if not os.path.exists(dirname):
1382 1392 os.makedirs(dirname)
1383 1393
1384 1394 if self._can_symlink:
1385 1395 try:
1386 1396 os.symlink(src, linkname)
1387 1397 except OSError, err:
1388 1398 raise OSError(err.errno, _('could not symlink to %r: %s') %
1389 1399 (src, err.strerror), linkname)
1390 1400 else:
1391 1401 f = self(dst, "w")
1392 1402 f.write(src)
1393 1403 f.close()
1394 1404
1395 1405 class chunkbuffer(object):
1396 1406 """Allow arbitrary sized chunks of data to be efficiently read from an
1397 1407 iterator over chunks of arbitrary size."""
1398 1408
1399 1409 def __init__(self, in_iter):
1400 1410 """in_iter is the iterator that's iterating over the input chunks.
1401 1411 targetsize is how big a buffer to try to maintain."""
1402 1412 self.iter = iter(in_iter)
1403 1413 self.buf = ''
1404 1414 self.targetsize = 2**16
1405 1415
1406 1416 def read(self, l):
1407 1417 """Read L bytes of data from the iterator of chunks of data.
1408 1418 Returns less than L bytes if the iterator runs dry."""
1409 1419 if l > len(self.buf) and self.iter:
1410 1420 # Clamp to a multiple of self.targetsize
1411 1421 targetsize = max(l, self.targetsize)
1412 1422 collector = cStringIO.StringIO()
1413 1423 collector.write(self.buf)
1414 1424 collected = len(self.buf)
1415 1425 for chunk in self.iter:
1416 1426 collector.write(chunk)
1417 1427 collected += len(chunk)
1418 1428 if collected >= targetsize:
1419 1429 break
1420 1430 if collected < targetsize:
1421 1431 self.iter = False
1422 1432 self.buf = collector.getvalue()
1423 1433 if len(self.buf) == l:
1424 1434 s, self.buf = str(self.buf), ''
1425 1435 else:
1426 1436 s, self.buf = self.buf[:l], buffer(self.buf, l)
1427 1437 return s
1428 1438
1429 1439 def filechunkiter(f, size=65536, limit=None):
1430 1440 """Create a generator that produces the data in the file size
1431 1441 (default 65536) bytes at a time, up to optional limit (default is
1432 1442 to read all data). Chunks may be less than size bytes if the
1433 1443 chunk is the last chunk in the file, or the file is a socket or
1434 1444 some other type of file that sometimes reads less data than is
1435 1445 requested."""
1436 1446 assert size >= 0
1437 1447 assert limit is None or limit >= 0
1438 1448 while True:
1439 1449 if limit is None: nbytes = size
1440 1450 else: nbytes = min(limit, size)
1441 1451 s = nbytes and f.read(nbytes)
1442 1452 if not s: break
1443 1453 if limit: limit -= len(s)
1444 1454 yield s
1445 1455
1446 1456 def makedate():
1447 1457 lt = time.localtime()
1448 1458 if lt[8] == 1 and time.daylight:
1449 1459 tz = time.altzone
1450 1460 else:
1451 1461 tz = time.timezone
1452 1462 return time.mktime(lt), tz
1453 1463
1454 1464 def datestr(date=None, format='%a %b %d %H:%M:%S %Y', timezone=True, timezone_format=" %+03d%02d"):
1455 1465 """represent a (unixtime, offset) tuple as a localized time.
1456 1466 unixtime is seconds since the epoch, and offset is the time zone's
1457 1467 number of seconds away from UTC. if timezone is false, do not
1458 1468 append time zone to string."""
1459 1469 t, tz = date or makedate()
1460 1470 s = time.strftime(format, time.gmtime(float(t) - tz))
1461 1471 if timezone:
1462 1472 s += timezone_format % (-tz / 3600, ((-tz % 3600) / 60))
1463 1473 return s
1464 1474
1465 1475 def strdate(string, format, defaults=[]):
1466 1476 """parse a localized time string and return a (unixtime, offset) tuple.
1467 1477 if the string cannot be parsed, ValueError is raised."""
1468 1478 def timezone(string):
1469 1479 tz = string.split()[-1]
1470 1480 if tz[0] in "+-" and len(tz) == 5 and tz[1:].isdigit():
1471 1481 tz = int(tz)
1472 1482 offset = - 3600 * (tz / 100) - 60 * (tz % 100)
1473 1483 return offset
1474 1484 if tz == "GMT" or tz == "UTC":
1475 1485 return 0
1476 1486 return None
1477 1487
1478 1488 # NOTE: unixtime = localunixtime + offset
1479 1489 offset, date = timezone(string), string
1480 1490 if offset != None:
1481 1491 date = " ".join(string.split()[:-1])
1482 1492
1483 1493 # add missing elements from defaults
1484 1494 for part in defaults:
1485 1495 found = [True for p in part if ("%"+p) in format]
1486 1496 if not found:
1487 1497 date += "@" + defaults[part]
1488 1498 format += "@%" + part[0]
1489 1499
1490 1500 timetuple = time.strptime(date, format)
1491 1501 localunixtime = int(calendar.timegm(timetuple))
1492 1502 if offset is None:
1493 1503 # local timezone
1494 1504 unixtime = int(time.mktime(timetuple))
1495 1505 offset = unixtime - localunixtime
1496 1506 else:
1497 1507 unixtime = localunixtime + offset
1498 1508 return unixtime, offset
1499 1509
1500 1510 def parsedate(string, formats=None, defaults=None):
1501 1511 """parse a localized time string and return a (unixtime, offset) tuple.
1502 1512 The date may be a "unixtime offset" string or in one of the specified
1503 1513 formats."""
1504 1514 if not string:
1505 1515 return 0, 0
1506 1516 if not formats:
1507 1517 formats = defaultdateformats
1508 1518 string = string.strip()
1509 1519 try:
1510 1520 when, offset = map(int, string.split(' '))
1511 1521 except ValueError:
1512 1522 # fill out defaults
1513 1523 if not defaults:
1514 1524 defaults = {}
1515 1525 now = makedate()
1516 1526 for part in "d mb yY HI M S".split():
1517 1527 if part not in defaults:
1518 1528 if part[0] in "HMS":
1519 1529 defaults[part] = "00"
1520 1530 elif part[0] in "dm":
1521 1531 defaults[part] = "1"
1522 1532 else:
1523 1533 defaults[part] = datestr(now, "%" + part[0], False)
1524 1534
1525 1535 for format in formats:
1526 1536 try:
1527 1537 when, offset = strdate(string, format, defaults)
1528 1538 except ValueError:
1529 1539 pass
1530 1540 else:
1531 1541 break
1532 1542 else:
1533 1543 raise Abort(_('invalid date: %r ') % string)
1534 1544 # validate explicit (probably user-specified) date and
1535 1545 # time zone offset. values must fit in signed 32 bits for
1536 1546 # current 32-bit linux runtimes. timezones go from UTC-12
1537 1547 # to UTC+14
1538 1548 if abs(when) > 0x7fffffff:
1539 1549 raise Abort(_('date exceeds 32 bits: %d') % when)
1540 1550 if offset < -50400 or offset > 43200:
1541 1551 raise Abort(_('impossible time zone offset: %d') % offset)
1542 1552 return when, offset
1543 1553
1544 1554 def matchdate(date):
1545 1555 """Return a function that matches a given date match specifier
1546 1556
1547 1557 Formats include:
1548 1558
1549 1559 '{date}' match a given date to the accuracy provided
1550 1560
1551 1561 '<{date}' on or before a given date
1552 1562
1553 1563 '>{date}' on or after a given date
1554 1564
1555 1565 """
1556 1566
1557 1567 def lower(date):
1558 1568 return parsedate(date, extendeddateformats)[0]
1559 1569
1560 1570 def upper(date):
1561 1571 d = dict(mb="12", HI="23", M="59", S="59")
1562 1572 for days in "31 30 29".split():
1563 1573 try:
1564 1574 d["d"] = days
1565 1575 return parsedate(date, extendeddateformats, d)[0]
1566 1576 except:
1567 1577 pass
1568 1578 d["d"] = "28"
1569 1579 return parsedate(date, extendeddateformats, d)[0]
1570 1580
1571 1581 if date[0] == "<":
1572 1582 when = upper(date[1:])
1573 1583 return lambda x: x <= when
1574 1584 elif date[0] == ">":
1575 1585 when = lower(date[1:])
1576 1586 return lambda x: x >= when
1577 1587 elif date[0] == "-":
1578 1588 try:
1579 1589 days = int(date[1:])
1580 1590 except ValueError:
1581 1591 raise Abort(_("invalid day spec: %s") % date[1:])
1582 1592 when = makedate()[0] - days * 3600 * 24
1583 1593 return lambda x: x >= when
1584 1594 elif " to " in date:
1585 1595 a, b = date.split(" to ")
1586 1596 start, stop = lower(a), upper(b)
1587 1597 return lambda x: x >= start and x <= stop
1588 1598 else:
1589 1599 start, stop = lower(date), upper(date)
1590 1600 return lambda x: x >= start and x <= stop
1591 1601
1592 1602 def shortuser(user):
1593 1603 """Return a short representation of a user name or email address."""
1594 1604 f = user.find('@')
1595 1605 if f >= 0:
1596 1606 user = user[:f]
1597 1607 f = user.find('<')
1598 1608 if f >= 0:
1599 1609 user = user[f+1:]
1600 1610 f = user.find(' ')
1601 1611 if f >= 0:
1602 1612 user = user[:f]
1603 1613 f = user.find('.')
1604 1614 if f >= 0:
1605 1615 user = user[:f]
1606 1616 return user
1607 1617
1608 1618 def ellipsis(text, maxlength=400):
1609 1619 """Trim string to at most maxlength (default: 400) characters."""
1610 1620 if len(text) <= maxlength:
1611 1621 return text
1612 1622 else:
1613 1623 return "%s..." % (text[:maxlength-3])
1614 1624
1615 1625 def walkrepos(path):
1616 1626 '''yield every hg repository under path, recursively.'''
1617 1627 def errhandler(err):
1618 1628 if err.filename == path:
1619 1629 raise err
1620 1630
1621 1631 for root, dirs, files in os.walk(path, onerror=errhandler):
1622 1632 for d in dirs:
1623 1633 if d == '.hg':
1624 1634 yield root
1625 1635 dirs[:] = []
1626 1636 break
1627 1637
1628 1638 _rcpath = None
1629 1639
1630 1640 def os_rcpath():
1631 1641 '''return default os-specific hgrc search path'''
1632 1642 path = system_rcpath()
1633 1643 path.extend(user_rcpath())
1634 1644 path = [os.path.normpath(f) for f in path]
1635 1645 return path
1636 1646
1637 1647 def rcpath():
1638 1648 '''return hgrc search path. if env var HGRCPATH is set, use it.
1639 1649 for each item in path, if directory, use files ending in .rc,
1640 1650 else use item.
1641 1651 make HGRCPATH empty to only look in .hg/hgrc of current repo.
1642 1652 if no HGRCPATH, use default os-specific path.'''
1643 1653 global _rcpath
1644 1654 if _rcpath is None:
1645 1655 if 'HGRCPATH' in os.environ:
1646 1656 _rcpath = []
1647 1657 for p in os.environ['HGRCPATH'].split(os.pathsep):
1648 1658 if not p: continue
1649 1659 if os.path.isdir(p):
1650 1660 for f, kind in osutil.listdir(p):
1651 1661 if f.endswith('.rc'):
1652 1662 _rcpath.append(os.path.join(p, f))
1653 1663 else:
1654 1664 _rcpath.append(p)
1655 1665 else:
1656 1666 _rcpath = os_rcpath()
1657 1667 return _rcpath
1658 1668
1659 1669 def bytecount(nbytes):
1660 1670 '''return byte count formatted as readable string, with units'''
1661 1671
1662 1672 units = (
1663 1673 (100, 1<<30, _('%.0f GB')),
1664 1674 (10, 1<<30, _('%.1f GB')),
1665 1675 (1, 1<<30, _('%.2f GB')),
1666 1676 (100, 1<<20, _('%.0f MB')),
1667 1677 (10, 1<<20, _('%.1f MB')),
1668 1678 (1, 1<<20, _('%.2f MB')),
1669 1679 (100, 1<<10, _('%.0f KB')),
1670 1680 (10, 1<<10, _('%.1f KB')),
1671 1681 (1, 1<<10, _('%.2f KB')),
1672 1682 (1, 1, _('%.0f bytes')),
1673 1683 )
1674 1684
1675 1685 for multiplier, divisor, format in units:
1676 1686 if nbytes >= divisor * multiplier:
1677 1687 return format % (nbytes / float(divisor))
1678 1688 return units[-1][2] % nbytes
1679 1689
1680 1690 def drop_scheme(scheme, path):
1681 1691 sc = scheme + ':'
1682 1692 if path.startswith(sc):
1683 1693 path = path[len(sc):]
1684 1694 if path.startswith('//'):
1685 1695 path = path[2:]
1686 1696 return path
1687 1697
1688 1698 def uirepr(s):
1689 1699 # Avoid double backslash in Windows path repr()
1690 1700 return repr(s).replace('\\\\', '\\')
@@ -1,79 +1,79
1 1 # Copyright (C) 2005, 2006 by Intevation GmbH
2 2 # Author(s):
3 3 # Thomas Arendsen Hein <thomas@intevation.de>
4 4 #
5 5 # This program is free software under the GNU GPL (>=v2)
6 6 # Read the file COPYING coming with the software for details.
7 7
8 8 """
9 9 Mercurial version
10 10 """
11 11
12 12 import os
13 13 import os.path
14 14 import re
15 15 import time
16 16 import util
17 17
18 18 unknown_version = 'unknown'
19 19 remembered_version = False
20 20
21 21 def get_version(doreload=False):
22 22 """Return version information if available."""
23 23 try:
24 24 import mercurial.__version__
25 25 if doreload:
26 26 reload(mercurial.__version__)
27 27 version = mercurial.__version__.version
28 28 except ImportError:
29 29 version = unknown_version
30 30 return version
31 31
32 32 def write_version(version):
33 33 """Overwrite version file."""
34 34 if version == get_version():
35 35 return
36 36 directory = os.path.dirname(__file__)
37 37 for suffix in ['py', 'pyc', 'pyo']:
38 38 try:
39 39 os.unlink(os.path.join(directory, '__version__.%s' % suffix))
40 40 except OSError:
41 41 pass
42 42 f = open(os.path.join(directory, '__version__.py'), 'w')
43 43 f.write("# This file is auto-generated.\n")
44 44 f.write("version = %r\n" % version)
45 45 f.close()
46 46 # reload the file we've just written
47 47 get_version(True)
48 48
49 49 def remember_version(version=None):
50 50 """Store version information."""
51 51 global remembered_version
52 52 if not version and os.path.isdir(".hg"):
53 f = os.popen("hg identify 2> %s" % util.nulldev) # use real hg installation
53 f = util.popen("hg identify") # use real hg installation
54 54 ident = f.read()[:-1]
55 55 if not f.close() and ident:
56 56 ids = ident.split(' ', 1)
57 57 version = ids.pop(0)
58 58 if version[-1] == '+':
59 59 version = version[:-1]
60 60 modified = True
61 61 else:
62 62 modified = False
63 63 if version.isalnum() and ids:
64 64 for tag in ids[0].split('/'):
65 65 # is a tag is suitable as a version number?
66 66 if re.match(r'^(\d+\.)+[\w.-]+$', tag):
67 67 version = tag
68 68 break
69 69 if modified:
70 70 version += time.strftime('+%Y%m%d')
71 71 if version:
72 72 remembered_version = True
73 73 write_version(version)
74 74
75 75 def forget_version():
76 76 """Remove version information."""
77 77 if remembered_version:
78 78 write_version(unknown_version)
79 79
General Comments 0
You need to be logged in to leave comments. Login now