##// END OF EJS Templates
templater: move email function to util
Matt Mackall -
r5975:75d9fe70 default
parent child Browse files
Show More
@@ -1,204 +1,204
1 1 # churn.py - create a graph showing who changed the most lines
2 2 #
3 3 # Copyright 2006 Josef "Jeff" Sipek <jeffpc@josefsipek.net>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7 #
8 8 #
9 9 # Aliases map file format is simple one alias per line in the following
10 10 # format:
11 11 #
12 12 # <alias email> <actual email>
13 13
14 14 from mercurial.i18n import gettext as _
15 15 from mercurial import hg, mdiff, cmdutil, ui, util, templater, node
16 16 import os, sys
17 17
18 18 def get_tty_width():
19 19 if 'COLUMNS' in os.environ:
20 20 try:
21 21 return int(os.environ['COLUMNS'])
22 22 except ValueError:
23 23 pass
24 24 try:
25 25 import termios, array, fcntl
26 26 for dev in (sys.stdout, sys.stdin):
27 27 try:
28 28 fd = dev.fileno()
29 29 if not os.isatty(fd):
30 30 continue
31 31 arri = fcntl.ioctl(fd, termios.TIOCGWINSZ, '\0' * 8)
32 32 return array.array('h', arri)[1]
33 33 except ValueError:
34 34 pass
35 35 except ImportError:
36 36 pass
37 37 return 80
38 38
39 39 def __gather(ui, repo, node1, node2):
40 40 def dirtywork(f, mmap1, mmap2):
41 41 lines = 0
42 42
43 43 to = mmap1 and repo.file(f).read(mmap1[f]) or None
44 44 tn = mmap2 and repo.file(f).read(mmap2[f]) or None
45 45
46 46 diff = mdiff.unidiff(to, "", tn, "", f, f).split("\n")
47 47
48 48 for line in diff:
49 49 if not line:
50 50 continue # skip EOF
51 51 if line.startswith(" "):
52 52 continue # context line
53 53 if line.startswith("--- ") or line.startswith("+++ "):
54 54 continue # begining of diff
55 55 if line.startswith("@@ "):
56 56 continue # info line
57 57
58 58 # changed lines
59 59 lines += 1
60 60
61 61 return lines
62 62
63 63 ##
64 64
65 65 lines = 0
66 66
67 67 changes = repo.status(node1, node2, None, util.always)[:5]
68 68
69 69 modified, added, removed, deleted, unknown = changes
70 70
71 71 who = repo.changelog.read(node2)[1]
72 who = templater.email(who) # get the email of the person
72 who = util.email(who) # get the email of the person
73 73
74 74 mmap1 = repo.manifest.read(repo.changelog.read(node1)[0])
75 75 mmap2 = repo.manifest.read(repo.changelog.read(node2)[0])
76 76 for f in modified:
77 77 lines += dirtywork(f, mmap1, mmap2)
78 78
79 79 for f in added:
80 80 lines += dirtywork(f, None, mmap2)
81 81
82 82 for f in removed:
83 83 lines += dirtywork(f, mmap1, None)
84 84
85 85 for f in deleted:
86 86 lines += dirtywork(f, mmap1, mmap2)
87 87
88 88 for f in unknown:
89 89 lines += dirtywork(f, mmap1, mmap2)
90 90
91 91 return (who, lines)
92 92
93 93 def gather_stats(ui, repo, amap, revs=None, progress=False):
94 94 stats = {}
95 95
96 96 cl = repo.changelog
97 97
98 98 if not revs:
99 99 revs = range(0, cl.count())
100 100
101 101 nr_revs = len(revs)
102 102 cur_rev = 0
103 103
104 104 for rev in revs:
105 105 cur_rev += 1 # next revision
106 106
107 107 node2 = cl.node(rev)
108 108 node1 = cl.parents(node2)[0]
109 109
110 110 if cl.parents(node2)[1] != node.nullid:
111 111 ui.note(_('Revision %d is a merge, ignoring...\n') % (rev,))
112 112 continue
113 113
114 114 who, lines = __gather(ui, repo, node1, node2)
115 115
116 116 # remap the owner if possible
117 117 if who in amap:
118 118 ui.note("using '%s' alias for '%s'\n" % (amap[who], who))
119 119 who = amap[who]
120 120
121 121 if not who in stats:
122 122 stats[who] = 0
123 123 stats[who] += lines
124 124
125 125 ui.note("rev %d: %d lines by %s\n" % (rev, lines, who))
126 126
127 127 if progress:
128 128 nr_revs = max(nr_revs, 1)
129 129 if int(100.0*(cur_rev - 1)/nr_revs) < int(100.0*cur_rev/nr_revs):
130 130 ui.write("%d%%.." % (int(100.0*cur_rev/nr_revs),))
131 131 sys.stdout.flush()
132 132
133 133 if progress:
134 134 ui.write("done\n")
135 135 sys.stdout.flush()
136 136
137 137 return stats
138 138
139 139 def churn(ui, repo, **opts):
140 140 "Graphs the number of lines changed"
141 141
142 142 def pad(s, l):
143 143 if len(s) < l:
144 144 return s + " " * (l-len(s))
145 145 return s[0:l]
146 146
147 147 def graph(n, maximum, width, char):
148 148 maximum = max(1, maximum)
149 149 n = int(n * width / float(maximum))
150 150
151 151 return char * (n)
152 152
153 153 def get_aliases(f):
154 154 aliases = {}
155 155
156 156 for l in f.readlines():
157 157 l = l.strip()
158 158 alias, actual = l.split(" ")
159 159 aliases[alias] = actual
160 160
161 161 return aliases
162 162
163 163 amap = {}
164 164 aliases = opts.get('aliases')
165 165 if aliases:
166 166 try:
167 167 f = open(aliases,"r")
168 168 except OSError, e:
169 169 print "Error: " + e
170 170 return
171 171
172 172 amap = get_aliases(f)
173 173 f.close()
174 174
175 175 revs = [int(r) for r in cmdutil.revrange(repo, opts['rev'])]
176 176 revs.sort()
177 177 stats = gather_stats(ui, repo, amap, revs, opts.get('progress'))
178 178
179 179 # make a list of tuples (name, lines) and sort it in descending order
180 180 ordered = stats.items()
181 181 ordered.sort(lambda x, y: cmp(y[1], x[1]))
182 182
183 183 if not ordered:
184 184 return
185 185 maximum = ordered[0][1]
186 186
187 187 width = get_tty_width()
188 188 ui.note(_("assuming %i character terminal\n") % width)
189 189 width -= 1
190 190
191 191 for i in ordered:
192 192 person = i[0]
193 193 lines = i[1]
194 194 print "%s %6d %s" % (pad(person, 20), lines,
195 195 graph(lines, maximum, width - 20 - 1 - 6 - 2 - 2, '*'))
196 196
197 197 cmdtable = {
198 198 "churn":
199 199 (churn,
200 200 [('r', 'rev', [], _('limit statistics to the specified revisions')),
201 201 ('', 'aliases', '', _('file with email aliases')),
202 202 ('', 'progress', None, _('show progress'))],
203 203 'hg churn [-r revision range] [-a file] [--progress]'),
204 204 }
@@ -1,311 +1,311
1 1 # bugzilla.py - bugzilla integration for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7 #
8 8 # hook extension to update comments of bugzilla bugs when changesets
9 9 # that refer to bugs by id are seen. this hook does not change bug
10 10 # status, only comments.
11 11 #
12 12 # to configure, add items to '[bugzilla]' section of hgrc.
13 13 #
14 14 # to use, configure bugzilla extension and enable like this:
15 15 #
16 16 # [extensions]
17 17 # hgext.bugzilla =
18 18 #
19 19 # [hooks]
20 20 # # run bugzilla hook on every change pulled or pushed in here
21 21 # incoming.bugzilla = python:hgext.bugzilla.hook
22 22 #
23 23 # config items:
24 24 #
25 25 # section name is 'bugzilla'.
26 26 # [bugzilla]
27 27 #
28 28 # REQUIRED:
29 29 # host = bugzilla # mysql server where bugzilla database lives
30 30 # password = ** # user's password
31 31 # version = 2.16 # version of bugzilla installed
32 32 #
33 33 # OPTIONAL:
34 34 # bzuser = ... # fallback bugzilla user name to record comments with
35 35 # db = bugs # database to connect to
36 36 # notify = ... # command to run to get bugzilla to send mail
37 37 # regexp = ... # regexp to match bug ids (must contain one "()" group)
38 38 # strip = 0 # number of slashes to strip for url paths
39 39 # style = ... # style file to use when formatting comments
40 40 # template = ... # template to use when formatting comments
41 41 # timeout = 5 # database connection timeout (seconds)
42 42 # user = bugs # user to connect to database as
43 43 # [web]
44 44 # baseurl = http://hgserver/... # root of hg web site for browsing commits
45 45 #
46 46 # if hg committer names are not same as bugzilla user names, use
47 47 # "usermap" feature to map from committer email to bugzilla user name.
48 48 # usermap can be in hgrc or separate config file.
49 49 #
50 50 # [bugzilla]
51 51 # usermap = filename # cfg file with "committer"="bugzilla user" info
52 52 # [usermap]
53 53 # committer_email = bugzilla_user_name
54 54
55 55 from mercurial.i18n import _
56 56 from mercurial.node import *
57 57 from mercurial import cmdutil, templater, util
58 58 import os, re, time
59 59
60 60 MySQLdb = None
61 61
62 62 def buglist(ids):
63 63 return '(' + ','.join(map(str, ids)) + ')'
64 64
65 65 class bugzilla_2_16(object):
66 66 '''support for bugzilla version 2.16.'''
67 67
68 68 def __init__(self, ui):
69 69 self.ui = ui
70 70 host = self.ui.config('bugzilla', 'host', 'localhost')
71 71 user = self.ui.config('bugzilla', 'user', 'bugs')
72 72 passwd = self.ui.config('bugzilla', 'password')
73 73 db = self.ui.config('bugzilla', 'db', 'bugs')
74 74 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
75 75 usermap = self.ui.config('bugzilla', 'usermap')
76 76 if usermap:
77 77 self.ui.readsections(usermap, 'usermap')
78 78 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
79 79 (host, db, user, '*' * len(passwd)))
80 80 self.conn = MySQLdb.connect(host=host, user=user, passwd=passwd,
81 81 db=db, connect_timeout=timeout)
82 82 self.cursor = self.conn.cursor()
83 83 self.run('select fieldid from fielddefs where name = "longdesc"')
84 84 ids = self.cursor.fetchall()
85 85 if len(ids) != 1:
86 86 raise util.Abort(_('unknown database schema'))
87 87 self.longdesc_id = ids[0][0]
88 88 self.user_ids = {}
89 89
90 90 def run(self, *args, **kwargs):
91 91 '''run a query.'''
92 92 self.ui.note(_('query: %s %s\n') % (args, kwargs))
93 93 try:
94 94 self.cursor.execute(*args, **kwargs)
95 95 except MySQLdb.MySQLError, err:
96 96 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
97 97 raise
98 98
99 99 def filter_real_bug_ids(self, ids):
100 100 '''filter not-existing bug ids from list.'''
101 101 self.run('select bug_id from bugs where bug_id in %s' % buglist(ids))
102 102 ids = [c[0] for c in self.cursor.fetchall()]
103 103 ids.sort()
104 104 return ids
105 105
106 106 def filter_unknown_bug_ids(self, node, ids):
107 107 '''filter bug ids from list that already refer to this changeset.'''
108 108
109 109 self.run('''select bug_id from longdescs where
110 110 bug_id in %s and thetext like "%%%s%%"''' %
111 111 (buglist(ids), short(node)))
112 112 unknown = dict.fromkeys(ids)
113 113 for (id,) in self.cursor.fetchall():
114 114 self.ui.status(_('bug %d already knows about changeset %s\n') %
115 115 (id, short(node)))
116 116 unknown.pop(id, None)
117 117 ids = unknown.keys()
118 118 ids.sort()
119 119 return ids
120 120
121 121 def notify(self, ids):
122 122 '''tell bugzilla to send mail.'''
123 123
124 124 self.ui.status(_('telling bugzilla to send mail:\n'))
125 125 for id in ids:
126 126 self.ui.status(_(' bug %s\n') % id)
127 127 cmd = self.ui.config('bugzilla', 'notify',
128 128 'cd /var/www/html/bugzilla && '
129 129 './processmail %s nobody@nowhere.com') % id
130 130 fp = os.popen('(%s) 2>&1' % cmd)
131 131 out = fp.read()
132 132 ret = fp.close()
133 133 if ret:
134 134 self.ui.warn(out)
135 135 raise util.Abort(_('bugzilla notify command %s') %
136 136 util.explain_exit(ret)[0])
137 137 self.ui.status(_('done\n'))
138 138
139 139 def get_user_id(self, user):
140 140 '''look up numeric bugzilla user id.'''
141 141 try:
142 142 return self.user_ids[user]
143 143 except KeyError:
144 144 try:
145 145 userid = int(user)
146 146 except ValueError:
147 147 self.ui.note(_('looking up user %s\n') % user)
148 148 self.run('''select userid from profiles
149 149 where login_name like %s''', user)
150 150 all = self.cursor.fetchall()
151 151 if len(all) != 1:
152 152 raise KeyError(user)
153 153 userid = int(all[0][0])
154 154 self.user_ids[user] = userid
155 155 return userid
156 156
157 157 def map_committer(self, user):
158 158 '''map name of committer to bugzilla user name.'''
159 159 for committer, bzuser in self.ui.configitems('usermap'):
160 160 if committer.lower() == user.lower():
161 161 return bzuser
162 162 return user
163 163
164 164 def add_comment(self, bugid, text, committer):
165 165 '''add comment to bug. try adding comment as committer of
166 166 changeset, otherwise as default bugzilla user.'''
167 167 user = self.map_committer(committer)
168 168 try:
169 169 userid = self.get_user_id(user)
170 170 except KeyError:
171 171 try:
172 172 defaultuser = self.ui.config('bugzilla', 'bzuser')
173 173 if not defaultuser:
174 174 raise util.Abort(_('cannot find bugzilla user id for %s') %
175 175 user)
176 176 userid = self.get_user_id(defaultuser)
177 177 except KeyError:
178 178 raise util.Abort(_('cannot find bugzilla user id for %s or %s') %
179 179 (user, defaultuser))
180 180 now = time.strftime('%Y-%m-%d %H:%M:%S')
181 181 self.run('''insert into longdescs
182 182 (bug_id, who, bug_when, thetext)
183 183 values (%s, %s, %s, %s)''',
184 184 (bugid, userid, now, text))
185 185 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
186 186 values (%s, %s, %s, %s)''',
187 187 (bugid, userid, now, self.longdesc_id))
188 188
189 189 class bugzilla(object):
190 190 # supported versions of bugzilla. different versions have
191 191 # different schemas.
192 192 _versions = {
193 193 '2.16': bugzilla_2_16,
194 194 }
195 195
196 196 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
197 197 r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)')
198 198
199 199 _bz = None
200 200
201 201 def __init__(self, ui, repo):
202 202 self.ui = ui
203 203 self.repo = repo
204 204
205 205 def bz(self):
206 206 '''return object that knows how to talk to bugzilla version in
207 207 use.'''
208 208
209 209 if bugzilla._bz is None:
210 210 bzversion = self.ui.config('bugzilla', 'version')
211 211 try:
212 212 bzclass = bugzilla._versions[bzversion]
213 213 except KeyError:
214 214 raise util.Abort(_('bugzilla version %s not supported') %
215 215 bzversion)
216 216 bugzilla._bz = bzclass(self.ui)
217 217 return bugzilla._bz
218 218
219 219 def __getattr__(self, key):
220 220 return getattr(self.bz(), key)
221 221
222 222 _bug_re = None
223 223 _split_re = None
224 224
225 225 def find_bug_ids(self, ctx):
226 226 '''find valid bug ids that are referred to in changeset
227 227 comments and that do not already have references to this
228 228 changeset.'''
229 229
230 230 if bugzilla._bug_re is None:
231 231 bugzilla._bug_re = re.compile(
232 232 self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re),
233 233 re.IGNORECASE)
234 234 bugzilla._split_re = re.compile(r'\D+')
235 235 start = 0
236 236 ids = {}
237 237 while True:
238 238 m = bugzilla._bug_re.search(ctx.description(), start)
239 239 if not m:
240 240 break
241 241 start = m.end()
242 242 for id in bugzilla._split_re.split(m.group(1)):
243 243 if not id: continue
244 244 ids[int(id)] = 1
245 245 ids = ids.keys()
246 246 if ids:
247 247 ids = self.filter_real_bug_ids(ids)
248 248 if ids:
249 249 ids = self.filter_unknown_bug_ids(ctx.node(), ids)
250 250 return ids
251 251
252 252 def update(self, bugid, ctx):
253 253 '''update bugzilla bug with reference to changeset.'''
254 254
255 255 def webroot(root):
256 256 '''strip leading prefix of repo root and turn into
257 257 url-safe path.'''
258 258 count = int(self.ui.config('bugzilla', 'strip', 0))
259 259 root = util.pconvert(root)
260 260 while count > 0:
261 261 c = root.find('/')
262 262 if c == -1:
263 263 break
264 264 root = root[c+1:]
265 265 count -= 1
266 266 return root
267 267
268 268 mapfile = self.ui.config('bugzilla', 'style')
269 269 tmpl = self.ui.config('bugzilla', 'template')
270 270 t = cmdutil.changeset_templater(self.ui, self.repo,
271 271 False, mapfile, False)
272 272 if not mapfile and not tmpl:
273 273 tmpl = _('changeset {node|short} in repo {root} refers '
274 274 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
275 275 if tmpl:
276 276 tmpl = templater.parsestring(tmpl, quoted=False)
277 277 t.use_template(tmpl)
278 278 self.ui.pushbuffer()
279 279 t.show(changenode=ctx.node(), changes=ctx.changeset(),
280 280 bug=str(bugid),
281 281 hgweb=self.ui.config('web', 'baseurl'),
282 282 root=self.repo.root,
283 283 webroot=webroot(self.repo.root))
284 284 data = self.ui.popbuffer()
285 self.add_comment(bugid, data, templater.email(ctx.user()))
285 self.add_comment(bugid, data, util.email(ctx.user()))
286 286
287 287 def hook(ui, repo, hooktype, node=None, **kwargs):
288 288 '''add comment to bugzilla for each changeset that refers to a
289 289 bugzilla bug id. only add a comment once per bug, so same change
290 290 seen multiple times does not fill bug with duplicate data.'''
291 291 try:
292 292 import MySQLdb as mysql
293 293 global MySQLdb
294 294 MySQLdb = mysql
295 295 except ImportError, err:
296 296 raise util.Abort(_('python mysql support not available: %s') % err)
297 297
298 298 if node is None:
299 299 raise util.Abort(_('hook type %s does not pass a changeset id') %
300 300 hooktype)
301 301 try:
302 302 bz = bugzilla(ui, repo)
303 303 ctx = repo.changectx(node)
304 304 ids = bz.find_bug_ids(ctx)
305 305 if ids:
306 306 for id in ids:
307 307 bz.update(id, ctx)
308 308 bz.notify(ids)
309 309 except MySQLdb.MySQLError, err:
310 310 raise util.Abort(_('database error: %s') % err[1])
311 311
@@ -1,285 +1,285
1 1 # notify.py - email notifications for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7 #
8 8 # hook extension to email notifications to people when changesets are
9 9 # committed to a repo they subscribe to.
10 10 #
11 11 # default mode is to print messages to stdout, for testing and
12 12 # configuring.
13 13 #
14 14 # to use, configure notify extension and enable in hgrc like this:
15 15 #
16 16 # [extensions]
17 17 # hgext.notify =
18 18 #
19 19 # [hooks]
20 20 # # one email for each incoming changeset
21 21 # incoming.notify = python:hgext.notify.hook
22 22 # # batch emails when many changesets incoming at one time
23 23 # changegroup.notify = python:hgext.notify.hook
24 24 #
25 25 # [notify]
26 26 # # config items go in here
27 27 #
28 28 # config items:
29 29 #
30 30 # REQUIRED:
31 31 # config = /path/to/file # file containing subscriptions
32 32 #
33 33 # OPTIONAL:
34 34 # test = True # print messages to stdout for testing
35 35 # strip = 3 # number of slashes to strip for url paths
36 36 # domain = example.com # domain to use if committer missing domain
37 37 # style = ... # style file to use when formatting email
38 38 # template = ... # template to use when formatting email
39 39 # incoming = ... # template to use when run as incoming hook
40 40 # changegroup = ... # template when run as changegroup hook
41 41 # maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
42 42 # maxsubject = 67 # truncate subject line longer than this
43 43 # diffstat = True # add a diffstat before the diff content
44 44 # sources = serve # notify if source of incoming changes in this list
45 45 # # (serve == ssh or http, push, pull, bundle)
46 46 # [email]
47 47 # from = user@host.com # email address to send as if none given
48 48 # [web]
49 49 # baseurl = http://hgserver/... # root of hg web site for browsing commits
50 50 #
51 51 # notify config file has same format as regular hgrc. it has two
52 52 # sections so you can express subscriptions in whatever way is handier
53 53 # for you.
54 54 #
55 55 # [usersubs]
56 56 # # key is subscriber email, value is ","-separated list of glob patterns
57 57 # user@host = pattern
58 58 #
59 59 # [reposubs]
60 60 # # key is glob pattern, value is ","-separated list of subscriber emails
61 61 # pattern = user@host
62 62 #
63 63 # glob patterns are matched against path to repo root.
64 64 #
65 65 # if you like, you can put notify config file in repo that users can
66 66 # push changes to, they can manage their own subscriptions.
67 67
68 68 from mercurial.i18n import _
69 69 from mercurial.node import *
70 70 from mercurial import patch, cmdutil, templater, util, mail
71 71 import email.Parser, fnmatch, socket, time
72 72
73 73 # template for single changeset can include email headers.
74 74 single_template = '''
75 75 Subject: changeset in {webroot}: {desc|firstline|strip}
76 76 From: {author}
77 77
78 78 changeset {node|short} in {root}
79 79 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
80 80 description:
81 81 \t{desc|tabindent|strip}
82 82 '''.lstrip()
83 83
84 84 # template for multiple changesets should not contain email headers,
85 85 # because only first set of headers will be used and result will look
86 86 # strange.
87 87 multiple_template = '''
88 88 changeset {node|short} in {root}
89 89 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
90 90 summary: {desc|firstline}
91 91 '''
92 92
93 93 deftemplates = {
94 94 'changegroup': multiple_template,
95 95 }
96 96
97 97 class notifier(object):
98 98 '''email notification class.'''
99 99
100 100 def __init__(self, ui, repo, hooktype):
101 101 self.ui = ui
102 102 cfg = self.ui.config('notify', 'config')
103 103 if cfg:
104 104 self.ui.readsections(cfg, 'usersubs', 'reposubs')
105 105 self.repo = repo
106 106 self.stripcount = int(self.ui.config('notify', 'strip', 0))
107 107 self.root = self.strip(self.repo.root)
108 108 self.domain = self.ui.config('notify', 'domain')
109 109 self.subs = self.subscribers()
110 110
111 111 mapfile = self.ui.config('notify', 'style')
112 112 template = (self.ui.config('notify', hooktype) or
113 113 self.ui.config('notify', 'template'))
114 114 self.t = cmdutil.changeset_templater(self.ui, self.repo,
115 115 False, mapfile, False)
116 116 if not mapfile and not template:
117 117 template = deftemplates.get(hooktype) or single_template
118 118 if template:
119 119 template = templater.parsestring(template, quoted=False)
120 120 self.t.use_template(template)
121 121
122 122 def strip(self, path):
123 123 '''strip leading slashes from local path, turn into web-safe path.'''
124 124
125 125 path = util.pconvert(path)
126 126 count = self.stripcount
127 127 while count > 0:
128 128 c = path.find('/')
129 129 if c == -1:
130 130 break
131 131 path = path[c+1:]
132 132 count -= 1
133 133 return path
134 134
135 135 def fixmail(self, addr):
136 136 '''try to clean up email addresses.'''
137 137
138 addr = templater.email(addr.strip())
138 addr = util.email(addr.strip())
139 139 if self.domain:
140 140 a = addr.find('@localhost')
141 141 if a != -1:
142 142 addr = addr[:a]
143 143 if '@' not in addr:
144 144 return addr + '@' + self.domain
145 145 return addr
146 146
147 147 def subscribers(self):
148 148 '''return list of email addresses of subscribers to this repo.'''
149 149
150 150 subs = {}
151 151 for user, pats in self.ui.configitems('usersubs'):
152 152 for pat in pats.split(','):
153 153 if fnmatch.fnmatch(self.repo.root, pat.strip()):
154 154 subs[self.fixmail(user)] = 1
155 155 for pat, users in self.ui.configitems('reposubs'):
156 156 if fnmatch.fnmatch(self.repo.root, pat):
157 157 for user in users.split(','):
158 158 subs[self.fixmail(user)] = 1
159 159 subs = subs.keys()
160 160 subs.sort()
161 161 return subs
162 162
163 163 def url(self, path=None):
164 164 return self.ui.config('web', 'baseurl') + (path or self.root)
165 165
166 166 def node(self, node):
167 167 '''format one changeset.'''
168 168
169 169 self.t.show(changenode=node, changes=self.repo.changelog.read(node),
170 170 baseurl=self.ui.config('web', 'baseurl'),
171 171 root=self.repo.root,
172 172 webroot=self.root)
173 173
174 174 def skipsource(self, source):
175 175 '''true if incoming changes from this source should be skipped.'''
176 176 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
177 177 return source not in ok_sources
178 178
179 179 def send(self, node, count, data):
180 180 '''send message.'''
181 181
182 182 p = email.Parser.Parser()
183 183 msg = p.parsestr(data)
184 184
185 185 def fix_subject():
186 186 '''try to make subject line exist and be useful.'''
187 187
188 188 subject = msg['Subject']
189 189 if not subject:
190 190 if count > 1:
191 191 subject = _('%s: %d new changesets') % (self.root, count)
192 192 else:
193 193 changes = self.repo.changelog.read(node)
194 194 s = changes[4].lstrip().split('\n', 1)[0].rstrip()
195 195 subject = '%s: %s' % (self.root, s)
196 196 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
197 197 if maxsubject and len(subject) > maxsubject:
198 198 subject = subject[:maxsubject-3] + '...'
199 199 del msg['Subject']
200 200 msg['Subject'] = subject
201 201
202 202 def fix_sender():
203 203 '''try to make message have proper sender.'''
204 204
205 205 sender = msg['From']
206 206 if not sender:
207 207 sender = self.ui.config('email', 'from') or self.ui.username()
208 208 if '@' not in sender or '@localhost' in sender:
209 209 sender = self.fixmail(sender)
210 210 del msg['From']
211 211 msg['From'] = sender
212 212
213 213 msg['Date'] = util.datestr(date=util.makedate(),
214 214 format="%a, %d %b %Y %H:%M:%S",
215 215 timezone=True)
216 216 fix_subject()
217 217 fix_sender()
218 218
219 219 msg['X-Hg-Notification'] = 'changeset ' + short(node)
220 220 if not msg['Message-Id']:
221 221 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
222 222 (short(node), int(time.time()),
223 223 hash(self.repo.root), socket.getfqdn()))
224 224 msg['To'] = ', '.join(self.subs)
225 225
226 226 msgtext = msg.as_string(0)
227 227 if self.ui.configbool('notify', 'test', True):
228 228 self.ui.write(msgtext)
229 229 if not msgtext.endswith('\n'):
230 230 self.ui.write('\n')
231 231 else:
232 232 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
233 233 (len(self.subs), count))
234 mail.sendmail(self.ui, templater.email(msg['From']),
234 mail.sendmail(self.ui, util.email(msg['From']),
235 235 self.subs, msgtext)
236 236
237 237 def diff(self, node, ref):
238 238 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
239 239 if maxdiff == 0:
240 240 return
241 241 prev = self.repo.changelog.parents(node)[0]
242 242 self.ui.pushbuffer()
243 243 patch.diff(self.repo, prev, ref)
244 244 difflines = self.ui.popbuffer().splitlines(1)
245 245 if self.ui.configbool('notify', 'diffstat', True):
246 246 s = patch.diffstat(difflines)
247 247 # s may be nil, don't include the header if it is
248 248 if s:
249 249 self.ui.write('\ndiffstat:\n\n%s' % s)
250 250 if maxdiff > 0 and len(difflines) > maxdiff:
251 251 self.ui.write(_('\ndiffs (truncated from %d to %d lines):\n\n') %
252 252 (len(difflines), maxdiff))
253 253 difflines = difflines[:maxdiff]
254 254 elif difflines:
255 255 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
256 256 self.ui.write(*difflines)
257 257
258 258 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
259 259 '''send email notifications to interested subscribers.
260 260
261 261 if used as changegroup hook, send one email for all changesets in
262 262 changegroup. else send one email per changeset.'''
263 263 n = notifier(ui, repo, hooktype)
264 264 if not n.subs:
265 265 ui.debug(_('notify: no subscribers to repo %s\n') % n.root)
266 266 return
267 267 if n.skipsource(source):
268 268 ui.debug(_('notify: changes have source "%s" - skipping\n') %
269 269 source)
270 270 return
271 271 node = bin(node)
272 272 ui.pushbuffer()
273 273 if hooktype == 'changegroup':
274 274 start = repo.changelog.rev(node)
275 275 end = repo.changelog.count()
276 276 count = end - start
277 277 for rev in xrange(start, end):
278 278 n.node(repo.changelog.node(rev))
279 279 n.diff(node, repo.changelog.tip())
280 280 else:
281 281 count = 1
282 282 n.node(node)
283 283 n.diff(node, node)
284 284 data = ui.popbuffer()
285 285 n.send(node, count, data)
@@ -1,85 +1,85
1 1 # mail.py - mail sending bits for mercurial
2 2 #
3 3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from i18n import _
9 import os, smtplib, templater, util, socket
9 import os, smtplib, util, socket
10 10
11 11 def _smtp(ui):
12 12 '''build an smtp connection and return a function to send mail'''
13 13 local_hostname = ui.config('smtp', 'local_hostname')
14 14 s = smtplib.SMTP(local_hostname=local_hostname)
15 15 mailhost = ui.config('smtp', 'host')
16 16 if not mailhost:
17 17 raise util.Abort(_('no [smtp]host in hgrc - cannot send mail'))
18 18 mailport = int(ui.config('smtp', 'port', 25))
19 19 ui.note(_('sending mail: smtp host %s, port %s\n') %
20 20 (mailhost, mailport))
21 21 s.connect(host=mailhost, port=mailport)
22 22 if ui.configbool('smtp', 'tls'):
23 23 if not hasattr(socket, 'ssl'):
24 24 raise util.Abort(_("can't use TLS: Python SSL support "
25 25 "not installed"))
26 26 ui.note(_('(using tls)\n'))
27 27 s.ehlo()
28 28 s.starttls()
29 29 s.ehlo()
30 30 username = ui.config('smtp', 'username')
31 31 password = ui.config('smtp', 'password')
32 32 if username and not password:
33 33 password = ui.getpass()
34 34 if username and password:
35 35 ui.note(_('(authenticating to mail server as %s)\n') %
36 36 (username))
37 37 s.login(username, password)
38 38
39 39 def send(sender, recipients, msg):
40 40 try:
41 41 return s.sendmail(sender, recipients, msg)
42 42 except smtplib.SMTPRecipientsRefused, inst:
43 43 recipients = [r[1] for r in inst.recipients.values()]
44 44 raise util.Abort('\n' + '\n'.join(recipients))
45 45 except smtplib.SMTPException, inst:
46 46 raise util.Abort(inst)
47 47
48 48 return send
49 49
50 50 def _sendmail(ui, sender, recipients, msg):
51 51 '''send mail using sendmail.'''
52 52 program = ui.config('email', 'method')
53 cmdline = '%s -f %s %s' % (program, templater.email(sender),
54 ' '.join(map(templater.email, recipients)))
53 cmdline = '%s -f %s %s' % (program, util.email(sender),
54 ' '.join(map(util.email, recipients)))
55 55 ui.note(_('sending mail: %s\n') % cmdline)
56 56 fp = os.popen(cmdline, 'w')
57 57 fp.write(msg)
58 58 ret = fp.close()
59 59 if ret:
60 60 raise util.Abort('%s %s' % (
61 61 os.path.basename(program.split(None, 1)[0]),
62 62 util.explain_exit(ret)[0]))
63 63
64 64 def connect(ui):
65 65 '''make a mail connection. return a function to send mail.
66 66 call as sendmail(sender, list-of-recipients, msg).'''
67 67 if ui.config('email', 'method', 'smtp') == 'smtp':
68 68 return _smtp(ui)
69 69 return lambda s, r, m: _sendmail(ui, s, r, m)
70 70
71 71 def sendmail(ui, sender, recipients, msg):
72 72 send = connect(ui)
73 73 return send(sender, recipients, msg)
74 74
75 75 def validateconfig(ui):
76 76 '''determine if we have enough config data to try sending email.'''
77 77 method = ui.config('email', 'method', 'smtp')
78 78 if method == 'smtp':
79 79 if not ui.config('smtp', 'host'):
80 80 raise util.Abort(_('smtp specified as email transport, '
81 81 'but no smtp host configured'))
82 82 else:
83 83 if not util.find_exe(method):
84 84 raise util.Abort(_('%r specified as email transport, '
85 85 'but not in PATH') % method)
@@ -1,297 +1,291
1 1 # templater.py - template expansion for output
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from i18n import _
9 9 import cgi, re, sys, os, time, urllib, util, textwrap
10 10
11 11 def parsestring(s, quoted=True):
12 12 '''parse a string using simple c-like syntax.
13 13 string must be in quotes if quoted is True.'''
14 14 if quoted:
15 15 if len(s) < 2 or s[0] != s[-1]:
16 16 raise SyntaxError(_('unmatched quotes'))
17 17 return s[1:-1].decode('string_escape')
18 18
19 19 return s.decode('string_escape')
20 20
21 21 class templater(object):
22 22 '''template expansion engine.
23 23
24 24 template expansion works like this. a map file contains key=value
25 25 pairs. if value is quoted, it is treated as string. otherwise, it
26 26 is treated as name of template file.
27 27
28 28 templater is asked to expand a key in map. it looks up key, and
29 29 looks for strings like this: {foo}. it expands {foo} by looking up
30 30 foo in map, and substituting it. expansion is recursive: it stops
31 31 when there is no more {foo} to replace.
32 32
33 33 expansion also allows formatting and filtering.
34 34
35 35 format uses key to expand each item in list. syntax is
36 36 {key%format}.
37 37
38 38 filter uses function to transform value. syntax is
39 39 {key|filter1|filter2|...}.'''
40 40
41 41 template_re = re.compile(r"(?:(?:#(?=[\w\|%]+#))|(?:{(?=[\w\|%]+})))"
42 42 r"(\w+)(?:(?:%(\w+))|((?:\|\w+)*))[#}]")
43 43
44 44 def __init__(self, mapfile, filters={}, defaults={}, cache={}):
45 45 '''set up template engine.
46 46 mapfile is name of file to read map definitions from.
47 47 filters is dict of functions. each transforms a value into another.
48 48 defaults is dict of default map definitions.'''
49 49 self.mapfile = mapfile or 'template'
50 50 self.cache = cache.copy()
51 51 self.map = {}
52 52 self.base = (mapfile and os.path.dirname(mapfile)) or ''
53 53 self.filters = filters
54 54 self.defaults = defaults
55 55
56 56 if not mapfile:
57 57 return
58 58 i = 0
59 59 for l in file(mapfile):
60 60 l = l.strip()
61 61 i += 1
62 62 if not l or l[0] in '#;': continue
63 63 m = re.match(r'([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$', l)
64 64 if m:
65 65 key, val = m.groups()
66 66 if val[0] in "'\"":
67 67 try:
68 68 self.cache[key] = parsestring(val)
69 69 except SyntaxError, inst:
70 70 raise SyntaxError('%s:%s: %s' %
71 71 (mapfile, i, inst.args[0]))
72 72 else:
73 73 self.map[key] = os.path.join(self.base, val)
74 74 else:
75 75 raise SyntaxError(_("%s:%s: parse error") % (mapfile, i))
76 76
77 77 def __contains__(self, key):
78 78 return key in self.cache or key in self.map
79 79
80 80 def __call__(self, t, **map):
81 81 '''perform expansion.
82 82 t is name of map element to expand.
83 83 map is added elements to use during expansion.'''
84 84 if not t in self.cache:
85 85 try:
86 86 self.cache[t] = file(self.map[t]).read()
87 87 except IOError, inst:
88 88 raise IOError(inst.args[0], _('template file %s: %s') %
89 89 (self.map[t], inst.args[1]))
90 90 tmpl = self.cache[t]
91 91
92 92 while tmpl:
93 93 m = self.template_re.search(tmpl)
94 94 if not m:
95 95 yield tmpl
96 96 break
97 97
98 98 start, end = m.span(0)
99 99 key, format, fl = m.groups()
100 100
101 101 if start:
102 102 yield tmpl[:start]
103 103 tmpl = tmpl[end:]
104 104
105 105 if key in map:
106 106 v = map[key]
107 107 else:
108 108 v = self.defaults.get(key, "")
109 109 if callable(v):
110 110 v = v(**map)
111 111 if format:
112 112 if not hasattr(v, '__iter__'):
113 113 raise SyntaxError(_("Error expanding '%s%s'")
114 114 % (key, format))
115 115 lm = map.copy()
116 116 for i in v:
117 117 lm.update(i)
118 118 yield self(format, **lm)
119 119 else:
120 120 if fl:
121 121 for f in fl.split("|")[1:]:
122 122 v = self.filters[f](v)
123 123 yield v
124 124
125 125 agescales = [("second", 1),
126 126 ("minute", 60),
127 127 ("hour", 3600),
128 128 ("day", 3600 * 24),
129 129 ("week", 3600 * 24 * 7),
130 130 ("month", 3600 * 24 * 30),
131 131 ("year", 3600 * 24 * 365)]
132 132
133 133 agescales.reverse()
134 134
135 135 def age(date):
136 136 '''turn a (timestamp, tzoff) tuple into an age string.'''
137 137
138 138 def plural(t, c):
139 139 if c == 1:
140 140 return t
141 141 return t + "s"
142 142 def fmt(t, c):
143 143 return "%d %s" % (c, plural(t, c))
144 144
145 145 now = time.time()
146 146 then = date[0]
147 147 delta = max(1, int(now - then))
148 148
149 149 for t, s in agescales:
150 150 n = delta / s
151 151 if n >= 2 or s == 1:
152 152 return fmt(t, n)
153 153
154 154 def stringify(thing):
155 155 '''turn nested template iterator into string.'''
156 156 if hasattr(thing, '__iter__'):
157 157 return "".join([stringify(t) for t in thing if t is not None])
158 158 return str(thing)
159 159
160 160 para_re = None
161 161 space_re = None
162 162
163 163 def fill(text, width):
164 164 '''fill many paragraphs.'''
165 165 global para_re, space_re
166 166 if para_re is None:
167 167 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
168 168 space_re = re.compile(r' +')
169 169
170 170 def findparas():
171 171 start = 0
172 172 while True:
173 173 m = para_re.search(text, start)
174 174 if not m:
175 175 w = len(text)
176 176 while w > start and text[w-1].isspace(): w -= 1
177 177 yield text[start:w], text[w:]
178 178 break
179 179 yield text[start:m.start(0)], m.group(1)
180 180 start = m.end(1)
181 181
182 182 return "".join([space_re.sub(' ', textwrap.fill(para, width)) + rest
183 183 for para, rest in findparas()])
184 184
185 185 def firstline(text):
186 186 '''return the first line of text'''
187 187 try:
188 188 return text.splitlines(1)[0].rstrip('\r\n')
189 189 except IndexError:
190 190 return ''
191 191
192 192 def isodate(date):
193 193 '''turn a (timestamp, tzoff) tuple into an iso 8631 date and time.'''
194 194 return util.datestr(date, format='%Y-%m-%d %H:%M')
195 195
196 196 def hgdate(date):
197 197 '''turn a (timestamp, tzoff) tuple into an hg cset timestamp.'''
198 198 return "%d %d" % date
199 199
200 200 def nl2br(text):
201 201 '''replace raw newlines with xhtml line breaks.'''
202 202 return text.replace('\n', '<br/>\n')
203 203
204 204 def obfuscate(text):
205 205 text = unicode(text, util._encoding, 'replace')
206 206 return ''.join(['&#%d;' % ord(c) for c in text])
207 207
208 208 def domain(author):
209 209 '''get domain of author, or empty string if none.'''
210 210 f = author.find('@')
211 211 if f == -1: return ''
212 212 author = author[f+1:]
213 213 f = author.find('>')
214 214 if f >= 0: author = author[:f]
215 215 return author
216 216
217 def email(author):
218 '''get email of author.'''
219 r = author.find('>')
220 if r == -1: r = None
221 return author[author.find('<')+1:r]
222
223 217 def person(author):
224 218 '''get name of author, or else username.'''
225 219 f = author.find('<')
226 220 if f == -1: return util.shortuser(author)
227 221 return author[:f].rstrip()
228 222
229 223 def shortdate(date):
230 224 '''turn (timestamp, tzoff) tuple into iso 8631 date.'''
231 225 return util.datestr(date, format='%Y-%m-%d', timezone=False)
232 226
233 227 def indent(text, prefix):
234 228 '''indent each non-empty line of text after first with prefix.'''
235 229 lines = text.splitlines()
236 230 num_lines = len(lines)
237 231 def indenter():
238 232 for i in xrange(num_lines):
239 233 l = lines[i]
240 234 if i and l.strip():
241 235 yield prefix
242 236 yield l
243 237 if i < num_lines - 1 or text.endswith('\n'):
244 238 yield '\n'
245 239 return "".join(indenter())
246 240
247 241 def permissions(flags):
248 242 if "l" in flags:
249 243 return "lrwxrwxrwx"
250 244 if "x" in flags:
251 245 return "-rwxr-xr-x"
252 246 return "-rw-r--r--"
253 247
254 248 common_filters = {
255 249 "addbreaks": nl2br,
256 250 "basename": os.path.basename,
257 251 "age": age,
258 252 "date": lambda x: util.datestr(x),
259 253 "domain": domain,
260 "email": email,
254 "email": util.email,
261 255 "escape": lambda x: cgi.escape(x, True),
262 256 "fill68": lambda x: fill(x, width=68),
263 257 "fill76": lambda x: fill(x, width=76),
264 258 "firstline": firstline,
265 259 "tabindent": lambda x: indent(x, '\t'),
266 260 "hgdate": hgdate,
267 261 "isodate": isodate,
268 262 "obfuscate": obfuscate,
269 263 "permissions": permissions,
270 264 "person": person,
271 265 "rfc822date": lambda x: util.datestr(x, "%a, %d %b %Y %H:%M:%S"),
272 266 "rfc3339date": lambda x: util.datestr(x, "%Y-%m-%dT%H:%M:%S", True, "%+03d:%02d"),
273 267 "short": lambda x: x[:12],
274 268 "shortdate": shortdate,
275 269 "stringify": stringify,
276 270 "strip": lambda x: x.strip(),
277 271 "urlescape": lambda x: urllib.quote(x),
278 272 "user": lambda x: util.shortuser(x),
279 273 "stringescape": lambda x: x.encode('string_escape'),
280 274 }
281 275
282 276 def templatepath(name=None):
283 277 '''return location of template file or directory (if no name).
284 278 returns None if not found.'''
285 279
286 280 # executable version (py2exe) doesn't support __file__
287 281 if hasattr(sys, 'frozen'):
288 282 module = sys.executable
289 283 else:
290 284 module = __file__
291 285 for f in 'templates', '../templates':
292 286 fl = f.split('/')
293 287 if name: fl.append(name)
294 288 p = os.path.join(os.path.dirname(module), *fl)
295 289 if (name and os.path.exists(p)) or os.path.isdir(p):
296 290 return os.path.normpath(p)
297 291
@@ -1,1733 +1,1739
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 import urlparse
19 19
20 20 try:
21 21 set = set
22 22 frozenset = frozenset
23 23 except NameError:
24 24 from sets import Set as set, ImmutableSet as frozenset
25 25
26 26 try:
27 27 _encoding = os.environ.get("HGENCODING")
28 28 if sys.platform == 'darwin' and not _encoding:
29 29 # On darwin, getpreferredencoding ignores the locale environment and
30 30 # always returns mac-roman. We override this if the environment is
31 31 # not C (has been customized by the user).
32 32 locale.setlocale(locale.LC_CTYPE, '')
33 33 _encoding = locale.getlocale()[1]
34 34 if not _encoding:
35 35 _encoding = locale.getpreferredencoding() or 'ascii'
36 36 except locale.Error:
37 37 _encoding = 'ascii'
38 38 _encodingmode = os.environ.get("HGENCODINGMODE", "strict")
39 39 _fallbackencoding = 'ISO-8859-1'
40 40
41 41 def tolocal(s):
42 42 """
43 43 Convert a string from internal UTF-8 to local encoding
44 44
45 45 All internal strings should be UTF-8 but some repos before the
46 46 implementation of locale support may contain latin1 or possibly
47 47 other character sets. We attempt to decode everything strictly
48 48 using UTF-8, then Latin-1, and failing that, we use UTF-8 and
49 49 replace unknown characters.
50 50 """
51 51 for e in ('UTF-8', _fallbackencoding):
52 52 try:
53 53 u = s.decode(e) # attempt strict decoding
54 54 return u.encode(_encoding, "replace")
55 55 except LookupError, k:
56 56 raise Abort(_("%s, please check your locale settings") % k)
57 57 except UnicodeDecodeError:
58 58 pass
59 59 u = s.decode("utf-8", "replace") # last ditch
60 60 return u.encode(_encoding, "replace")
61 61
62 62 def fromlocal(s):
63 63 """
64 64 Convert a string from the local character encoding to UTF-8
65 65
66 66 We attempt to decode strings using the encoding mode set by
67 67 HGENCODINGMODE, which defaults to 'strict'. In this mode, unknown
68 68 characters will cause an error message. Other modes include
69 69 'replace', which replaces unknown characters with a special
70 70 Unicode character, and 'ignore', which drops the character.
71 71 """
72 72 try:
73 73 return s.decode(_encoding, _encodingmode).encode("utf-8")
74 74 except UnicodeDecodeError, inst:
75 75 sub = s[max(0, inst.start-10):inst.start+10]
76 76 raise Abort("decoding near '%s': %s!" % (sub, inst))
77 77 except LookupError, k:
78 78 raise Abort(_("%s, please check your locale settings") % k)
79 79
80 80 def locallen(s):
81 81 """Find the length in characters of a local string"""
82 82 return len(s.decode(_encoding, "replace"))
83 83
84 84 # used by parsedate
85 85 defaultdateformats = (
86 86 '%Y-%m-%d %H:%M:%S',
87 87 '%Y-%m-%d %I:%M:%S%p',
88 88 '%Y-%m-%d %H:%M',
89 89 '%Y-%m-%d %I:%M%p',
90 90 '%Y-%m-%d',
91 91 '%m-%d',
92 92 '%m/%d',
93 93 '%m/%d/%y',
94 94 '%m/%d/%Y',
95 95 '%a %b %d %H:%M:%S %Y',
96 96 '%a %b %d %I:%M:%S%p %Y',
97 97 '%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822"
98 98 '%b %d %H:%M:%S %Y',
99 99 '%b %d %I:%M:%S%p %Y',
100 100 '%b %d %H:%M:%S',
101 101 '%b %d %I:%M:%S%p',
102 102 '%b %d %H:%M',
103 103 '%b %d %I:%M%p',
104 104 '%b %d %Y',
105 105 '%b %d',
106 106 '%H:%M:%S',
107 107 '%I:%M:%SP',
108 108 '%H:%M',
109 109 '%I:%M%p',
110 110 )
111 111
112 112 extendeddateformats = defaultdateformats + (
113 113 "%Y",
114 114 "%Y-%m",
115 115 "%b",
116 116 "%b %Y",
117 117 )
118 118
119 119 class SignalInterrupt(Exception):
120 120 """Exception raised on SIGTERM and SIGHUP."""
121 121
122 122 # differences from SafeConfigParser:
123 123 # - case-sensitive keys
124 124 # - allows values that are not strings (this means that you may not
125 125 # be able to save the configuration to a file)
126 126 class configparser(ConfigParser.SafeConfigParser):
127 127 def optionxform(self, optionstr):
128 128 return optionstr
129 129
130 130 def set(self, section, option, value):
131 131 return ConfigParser.ConfigParser.set(self, section, option, value)
132 132
133 133 def _interpolate(self, section, option, rawval, vars):
134 134 if not isinstance(rawval, basestring):
135 135 return rawval
136 136 return ConfigParser.SafeConfigParser._interpolate(self, section,
137 137 option, rawval, vars)
138 138
139 139 def cachefunc(func):
140 140 '''cache the result of function calls'''
141 141 # XXX doesn't handle keywords args
142 142 cache = {}
143 143 if func.func_code.co_argcount == 1:
144 144 # we gain a small amount of time because
145 145 # we don't need to pack/unpack the list
146 146 def f(arg):
147 147 if arg not in cache:
148 148 cache[arg] = func(arg)
149 149 return cache[arg]
150 150 else:
151 151 def f(*args):
152 152 if args not in cache:
153 153 cache[args] = func(*args)
154 154 return cache[args]
155 155
156 156 return f
157 157
158 158 def pipefilter(s, cmd):
159 159 '''filter string S through command CMD, returning its output'''
160 160 (pin, pout) = os.popen2(cmd, 'b')
161 161 def writer():
162 162 try:
163 163 pin.write(s)
164 164 pin.close()
165 165 except IOError, inst:
166 166 if inst.errno != errno.EPIPE:
167 167 raise
168 168
169 169 # we should use select instead on UNIX, but this will work on most
170 170 # systems, including Windows
171 171 w = threading.Thread(target=writer)
172 172 w.start()
173 173 f = pout.read()
174 174 pout.close()
175 175 w.join()
176 176 return f
177 177
178 178 def tempfilter(s, cmd):
179 179 '''filter string S through a pair of temporary files with CMD.
180 180 CMD is used as a template to create the real command to be run,
181 181 with the strings INFILE and OUTFILE replaced by the real names of
182 182 the temporary files generated.'''
183 183 inname, outname = None, None
184 184 try:
185 185 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
186 186 fp = os.fdopen(infd, 'wb')
187 187 fp.write(s)
188 188 fp.close()
189 189 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
190 190 os.close(outfd)
191 191 cmd = cmd.replace('INFILE', inname)
192 192 cmd = cmd.replace('OUTFILE', outname)
193 193 code = os.system(cmd)
194 194 if sys.platform == 'OpenVMS' and code & 1:
195 195 code = 0
196 196 if code: raise Abort(_("command '%s' failed: %s") %
197 197 (cmd, explain_exit(code)))
198 198 return open(outname, 'rb').read()
199 199 finally:
200 200 try:
201 201 if inname: os.unlink(inname)
202 202 except: pass
203 203 try:
204 204 if outname: os.unlink(outname)
205 205 except: pass
206 206
207 207 filtertable = {
208 208 'tempfile:': tempfilter,
209 209 'pipe:': pipefilter,
210 210 }
211 211
212 212 def filter(s, cmd):
213 213 "filter a string through a command that transforms its input to its output"
214 214 for name, fn in filtertable.iteritems():
215 215 if cmd.startswith(name):
216 216 return fn(s, cmd[len(name):].lstrip())
217 217 return pipefilter(s, cmd)
218 218
219 219 def binary(s):
220 220 """return true if a string is binary data using diff's heuristic"""
221 221 if s and '\0' in s[:4096]:
222 222 return True
223 223 return False
224 224
225 225 def unique(g):
226 226 """return the uniq elements of iterable g"""
227 227 return dict.fromkeys(g).keys()
228 228
229 229 class Abort(Exception):
230 230 """Raised if a command needs to print an error and exit."""
231 231
232 232 class UnexpectedOutput(Abort):
233 233 """Raised to print an error with part of output and exit."""
234 234
235 235 def always(fn): return True
236 236 def never(fn): return False
237 237
238 238 def expand_glob(pats):
239 239 '''On Windows, expand the implicit globs in a list of patterns'''
240 240 if os.name != 'nt':
241 241 return list(pats)
242 242 ret = []
243 243 for p in pats:
244 244 kind, name = patkind(p, None)
245 245 if kind is None:
246 246 globbed = glob.glob(name)
247 247 if globbed:
248 248 ret.extend(globbed)
249 249 continue
250 250 # if we couldn't expand the glob, just keep it around
251 251 ret.append(p)
252 252 return ret
253 253
254 254 def patkind(name, dflt_pat='glob'):
255 255 """Split a string into an optional pattern kind prefix and the
256 256 actual pattern."""
257 257 for prefix in 're', 'glob', 'path', 'relglob', 'relpath', 'relre':
258 258 if name.startswith(prefix + ':'): return name.split(':', 1)
259 259 return dflt_pat, name
260 260
261 261 def globre(pat, head='^', tail='$'):
262 262 "convert a glob pattern into a regexp"
263 263 i, n = 0, len(pat)
264 264 res = ''
265 265 group = 0
266 266 def peek(): return i < n and pat[i]
267 267 while i < n:
268 268 c = pat[i]
269 269 i = i+1
270 270 if c == '*':
271 271 if peek() == '*':
272 272 i += 1
273 273 res += '.*'
274 274 else:
275 275 res += '[^/]*'
276 276 elif c == '?':
277 277 res += '.'
278 278 elif c == '[':
279 279 j = i
280 280 if j < n and pat[j] in '!]':
281 281 j += 1
282 282 while j < n and pat[j] != ']':
283 283 j += 1
284 284 if j >= n:
285 285 res += '\\['
286 286 else:
287 287 stuff = pat[i:j].replace('\\','\\\\')
288 288 i = j + 1
289 289 if stuff[0] == '!':
290 290 stuff = '^' + stuff[1:]
291 291 elif stuff[0] == '^':
292 292 stuff = '\\' + stuff
293 293 res = '%s[%s]' % (res, stuff)
294 294 elif c == '{':
295 295 group += 1
296 296 res += '(?:'
297 297 elif c == '}' and group:
298 298 res += ')'
299 299 group -= 1
300 300 elif c == ',' and group:
301 301 res += '|'
302 302 elif c == '\\':
303 303 p = peek()
304 304 if p:
305 305 i += 1
306 306 res += re.escape(p)
307 307 else:
308 308 res += re.escape(c)
309 309 else:
310 310 res += re.escape(c)
311 311 return head + res + tail
312 312
313 313 _globchars = {'[': 1, '{': 1, '*': 1, '?': 1}
314 314
315 315 def pathto(root, n1, n2):
316 316 '''return the relative path from one place to another.
317 317 root should use os.sep to separate directories
318 318 n1 should use os.sep to separate directories
319 319 n2 should use "/" to separate directories
320 320 returns an os.sep-separated path.
321 321
322 322 If n1 is a relative path, it's assumed it's
323 323 relative to root.
324 324 n2 should always be relative to root.
325 325 '''
326 326 if not n1: return localpath(n2)
327 327 if os.path.isabs(n1):
328 328 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
329 329 return os.path.join(root, localpath(n2))
330 330 n2 = '/'.join((pconvert(root), n2))
331 331 a, b = splitpath(n1), n2.split('/')
332 332 a.reverse()
333 333 b.reverse()
334 334 while a and b and a[-1] == b[-1]:
335 335 a.pop()
336 336 b.pop()
337 337 b.reverse()
338 338 return os.sep.join((['..'] * len(a)) + b)
339 339
340 340 def canonpath(root, cwd, myname):
341 341 """return the canonical path of myname, given cwd and root"""
342 342 if root == os.sep:
343 343 rootsep = os.sep
344 344 elif endswithsep(root):
345 345 rootsep = root
346 346 else:
347 347 rootsep = root + os.sep
348 348 name = myname
349 349 if not os.path.isabs(name):
350 350 name = os.path.join(root, cwd, name)
351 351 name = os.path.normpath(name)
352 352 audit_path = path_auditor(root)
353 353 if name != rootsep and name.startswith(rootsep):
354 354 name = name[len(rootsep):]
355 355 audit_path(name)
356 356 return pconvert(name)
357 357 elif name == root:
358 358 return ''
359 359 else:
360 360 # Determine whether `name' is in the hierarchy at or beneath `root',
361 361 # by iterating name=dirname(name) until that causes no change (can't
362 362 # check name == '/', because that doesn't work on windows). For each
363 363 # `name', compare dev/inode numbers. If they match, the list `rel'
364 364 # holds the reversed list of components making up the relative file
365 365 # name we want.
366 366 root_st = os.stat(root)
367 367 rel = []
368 368 while True:
369 369 try:
370 370 name_st = os.stat(name)
371 371 except OSError:
372 372 break
373 373 if samestat(name_st, root_st):
374 374 if not rel:
375 375 # name was actually the same as root (maybe a symlink)
376 376 return ''
377 377 rel.reverse()
378 378 name = os.path.join(*rel)
379 379 audit_path(name)
380 380 return pconvert(name)
381 381 dirname, basename = os.path.split(name)
382 382 rel.append(basename)
383 383 if dirname == name:
384 384 break
385 385 name = dirname
386 386
387 387 raise Abort('%s not under root' % myname)
388 388
389 389 def matcher(canonroot, cwd='', names=[], inc=[], exc=[], src=None):
390 390 return _matcher(canonroot, cwd, names, inc, exc, 'glob', src)
391 391
392 392 def cmdmatcher(canonroot, cwd='', names=[], inc=[], exc=[], src=None,
393 393 globbed=False, default=None):
394 394 default = default or 'relpath'
395 395 if default == 'relpath' and not globbed:
396 396 names = expand_glob(names)
397 397 return _matcher(canonroot, cwd, names, inc, exc, default, src)
398 398
399 399 def _matcher(canonroot, cwd, names, inc, exc, dflt_pat, src):
400 400 """build a function to match a set of file patterns
401 401
402 402 arguments:
403 403 canonroot - the canonical root of the tree you're matching against
404 404 cwd - the current working directory, if relevant
405 405 names - patterns to find
406 406 inc - patterns to include
407 407 exc - patterns to exclude
408 408 dflt_pat - if a pattern in names has no explicit type, assume this one
409 409 src - where these patterns came from (e.g. .hgignore)
410 410
411 411 a pattern is one of:
412 412 'glob:<glob>' - a glob relative to cwd
413 413 're:<regexp>' - a regular expression
414 414 'path:<path>' - a path relative to canonroot
415 415 'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
416 416 'relpath:<path>' - a path relative to cwd
417 417 'relre:<regexp>' - a regexp that doesn't have to match the start of a name
418 418 '<something>' - one of the cases above, selected by the dflt_pat argument
419 419
420 420 returns:
421 421 a 3-tuple containing
422 422 - list of roots (places where one should start a recursive walk of the fs);
423 423 this often matches the explicit non-pattern names passed in, but also
424 424 includes the initial part of glob: patterns that has no glob characters
425 425 - a bool match(filename) function
426 426 - a bool indicating if any patterns were passed in
427 427 """
428 428
429 429 # a common case: no patterns at all
430 430 if not names and not inc and not exc:
431 431 return [], always, False
432 432
433 433 def contains_glob(name):
434 434 for c in name:
435 435 if c in _globchars: return True
436 436 return False
437 437
438 438 def regex(kind, name, tail):
439 439 '''convert a pattern into a regular expression'''
440 440 if not name:
441 441 return ''
442 442 if kind == 're':
443 443 return name
444 444 elif kind == 'path':
445 445 return '^' + re.escape(name) + '(?:/|$)'
446 446 elif kind == 'relglob':
447 447 return globre(name, '(?:|.*/)', tail)
448 448 elif kind == 'relpath':
449 449 return re.escape(name) + '(?:/|$)'
450 450 elif kind == 'relre':
451 451 if name.startswith('^'):
452 452 return name
453 453 return '.*' + name
454 454 return globre(name, '', tail)
455 455
456 456 def matchfn(pats, tail):
457 457 """build a matching function from a set of patterns"""
458 458 if not pats:
459 459 return
460 460 try:
461 461 pat = '(?:%s)' % '|'.join([regex(k, p, tail) for (k, p) in pats])
462 462 return re.compile(pat).match
463 463 except OverflowError:
464 464 # We're using a Python with a tiny regex engine and we
465 465 # made it explode, so we'll divide the pattern list in two
466 466 # until it works
467 467 l = len(pats)
468 468 if l < 2:
469 469 raise
470 470 a, b = matchfn(pats[:l//2], tail), matchfn(pats[l//2:], tail)
471 471 return lambda s: a(s) or b(s)
472 472 except re.error:
473 473 for k, p in pats:
474 474 try:
475 475 re.compile('(?:%s)' % regex(k, p, tail))
476 476 except re.error:
477 477 if src:
478 478 raise Abort("%s: invalid pattern (%s): %s" %
479 479 (src, k, p))
480 480 else:
481 481 raise Abort("invalid pattern (%s): %s" % (k, p))
482 482 raise Abort("invalid pattern")
483 483
484 484 def globprefix(pat):
485 485 '''return the non-glob prefix of a path, e.g. foo/* -> foo'''
486 486 root = []
487 487 for p in pat.split('/'):
488 488 if contains_glob(p): break
489 489 root.append(p)
490 490 return '/'.join(root) or '.'
491 491
492 492 def normalizepats(names, default):
493 493 pats = []
494 494 roots = []
495 495 anypats = False
496 496 for kind, name in [patkind(p, default) for p in names]:
497 497 if kind in ('glob', 'relpath'):
498 498 name = canonpath(canonroot, cwd, name)
499 499 elif kind in ('relglob', 'path'):
500 500 name = normpath(name)
501 501
502 502 pats.append((kind, name))
503 503
504 504 if kind in ('glob', 're', 'relglob', 'relre'):
505 505 anypats = True
506 506
507 507 if kind == 'glob':
508 508 root = globprefix(name)
509 509 roots.append(root)
510 510 elif kind in ('relpath', 'path'):
511 511 roots.append(name or '.')
512 512 elif kind == 'relglob':
513 513 roots.append('.')
514 514 return roots, pats, anypats
515 515
516 516 roots, pats, anypats = normalizepats(names, dflt_pat)
517 517
518 518 patmatch = matchfn(pats, '$') or always
519 519 incmatch = always
520 520 if inc:
521 521 dummy, inckinds, dummy = normalizepats(inc, 'glob')
522 522 incmatch = matchfn(inckinds, '(?:/|$)')
523 523 excmatch = lambda fn: False
524 524 if exc:
525 525 dummy, exckinds, dummy = normalizepats(exc, 'glob')
526 526 excmatch = matchfn(exckinds, '(?:/|$)')
527 527
528 528 if not names and inc and not exc:
529 529 # common case: hgignore patterns
530 530 match = incmatch
531 531 else:
532 532 match = lambda fn: incmatch(fn) and not excmatch(fn) and patmatch(fn)
533 533
534 534 return (roots, match, (inc or exc or anypats) and True)
535 535
536 536 _hgexecutable = None
537 537
538 538 def hgexecutable():
539 539 """return location of the 'hg' executable.
540 540
541 541 Defaults to $HG or 'hg' in the search path.
542 542 """
543 543 if _hgexecutable is None:
544 544 set_hgexecutable(os.environ.get('HG') or find_exe('hg', 'hg'))
545 545 return _hgexecutable
546 546
547 547 def set_hgexecutable(path):
548 548 """set location of the 'hg' executable"""
549 549 global _hgexecutable
550 550 _hgexecutable = path
551 551
552 552 def system(cmd, environ={}, cwd=None, onerr=None, errprefix=None):
553 553 '''enhanced shell command execution.
554 554 run with environment maybe modified, maybe in different dir.
555 555
556 556 if command fails and onerr is None, return status. if ui object,
557 557 print error message and return status, else raise onerr object as
558 558 exception.'''
559 559 def py2shell(val):
560 560 'convert python object into string that is useful to shell'
561 561 if val in (None, False):
562 562 return '0'
563 563 if val == True:
564 564 return '1'
565 565 return str(val)
566 566 oldenv = {}
567 567 for k in environ:
568 568 oldenv[k] = os.environ.get(k)
569 569 if cwd is not None:
570 570 oldcwd = os.getcwd()
571 571 origcmd = cmd
572 572 if os.name == 'nt':
573 573 cmd = '"%s"' % cmd
574 574 try:
575 575 for k, v in environ.iteritems():
576 576 os.environ[k] = py2shell(v)
577 577 os.environ['HG'] = hgexecutable()
578 578 if cwd is not None and oldcwd != cwd:
579 579 os.chdir(cwd)
580 580 rc = os.system(cmd)
581 581 if sys.platform == 'OpenVMS' and rc & 1:
582 582 rc = 0
583 583 if rc and onerr:
584 584 errmsg = '%s %s' % (os.path.basename(origcmd.split(None, 1)[0]),
585 585 explain_exit(rc)[0])
586 586 if errprefix:
587 587 errmsg = '%s: %s' % (errprefix, errmsg)
588 588 try:
589 589 onerr.warn(errmsg + '\n')
590 590 except AttributeError:
591 591 raise onerr(errmsg)
592 592 return rc
593 593 finally:
594 594 for k, v in oldenv.iteritems():
595 595 if v is None:
596 596 del os.environ[k]
597 597 else:
598 598 os.environ[k] = v
599 599 if cwd is not None and oldcwd != cwd:
600 600 os.chdir(oldcwd)
601 601
602 602 # os.path.lexists is not available on python2.3
603 603 def lexists(filename):
604 604 "test whether a file with this name exists. does not follow symlinks"
605 605 try:
606 606 os.lstat(filename)
607 607 except:
608 608 return False
609 609 return True
610 610
611 611 def rename(src, dst):
612 612 """forcibly rename a file"""
613 613 try:
614 614 os.rename(src, dst)
615 615 except OSError, err: # FIXME: check err (EEXIST ?)
616 616 # on windows, rename to existing file is not allowed, so we
617 617 # must delete destination first. but if file is open, unlink
618 618 # schedules it for delete but does not delete it. rename
619 619 # happens immediately even for open files, so we create
620 620 # temporary file, delete it, rename destination to that name,
621 621 # then delete that. then rename is safe to do.
622 622 fd, temp = tempfile.mkstemp(dir=os.path.dirname(dst) or '.')
623 623 os.close(fd)
624 624 os.unlink(temp)
625 625 os.rename(dst, temp)
626 626 os.unlink(temp)
627 627 os.rename(src, dst)
628 628
629 629 def unlink(f):
630 630 """unlink and remove the directory if it is empty"""
631 631 os.unlink(f)
632 632 # try removing directories that might now be empty
633 633 try:
634 634 os.removedirs(os.path.dirname(f))
635 635 except OSError:
636 636 pass
637 637
638 638 def copyfile(src, dest):
639 639 "copy a file, preserving mode"
640 640 if os.path.islink(src):
641 641 try:
642 642 os.unlink(dest)
643 643 except:
644 644 pass
645 645 os.symlink(os.readlink(src), dest)
646 646 else:
647 647 try:
648 648 shutil.copyfile(src, dest)
649 649 shutil.copymode(src, dest)
650 650 except shutil.Error, inst:
651 651 raise Abort(str(inst))
652 652
653 653 def copyfiles(src, dst, hardlink=None):
654 654 """Copy a directory tree using hardlinks if possible"""
655 655
656 656 if hardlink is None:
657 657 hardlink = (os.stat(src).st_dev ==
658 658 os.stat(os.path.dirname(dst)).st_dev)
659 659
660 660 if os.path.isdir(src):
661 661 os.mkdir(dst)
662 662 for name, kind in osutil.listdir(src):
663 663 srcname = os.path.join(src, name)
664 664 dstname = os.path.join(dst, name)
665 665 copyfiles(srcname, dstname, hardlink)
666 666 else:
667 667 if hardlink:
668 668 try:
669 669 os_link(src, dst)
670 670 except (IOError, OSError):
671 671 hardlink = False
672 672 shutil.copy(src, dst)
673 673 else:
674 674 shutil.copy(src, dst)
675 675
676 676 class path_auditor(object):
677 677 '''ensure that a filesystem path contains no banned components.
678 678 the following properties of a path are checked:
679 679
680 680 - under top-level .hg
681 681 - starts at the root of a windows drive
682 682 - contains ".."
683 683 - traverses a symlink (e.g. a/symlink_here/b)
684 684 - inside a nested repository'''
685 685
686 686 def __init__(self, root):
687 687 self.audited = set()
688 688 self.auditeddir = set()
689 689 self.root = root
690 690
691 691 def __call__(self, path):
692 692 if path in self.audited:
693 693 return
694 694 normpath = os.path.normcase(path)
695 695 parts = splitpath(normpath)
696 696 if (os.path.splitdrive(path)[0] or parts[0] in ('.hg', '')
697 697 or os.pardir in parts):
698 698 raise Abort(_("path contains illegal component: %s") % path)
699 699 def check(prefix):
700 700 curpath = os.path.join(self.root, prefix)
701 701 try:
702 702 st = os.lstat(curpath)
703 703 except OSError, err:
704 704 # EINVAL can be raised as invalid path syntax under win32.
705 705 # They must be ignored for patterns can be checked too.
706 706 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
707 707 raise
708 708 else:
709 709 if stat.S_ISLNK(st.st_mode):
710 710 raise Abort(_('path %r traverses symbolic link %r') %
711 711 (path, prefix))
712 712 elif (stat.S_ISDIR(st.st_mode) and
713 713 os.path.isdir(os.path.join(curpath, '.hg'))):
714 714 raise Abort(_('path %r is inside repo %r') %
715 715 (path, prefix))
716 716 parts.pop()
717 717 prefixes = []
718 718 for n in range(len(parts)):
719 719 prefix = os.sep.join(parts)
720 720 if prefix in self.auditeddir:
721 721 break
722 722 check(prefix)
723 723 prefixes.append(prefix)
724 724 parts.pop()
725 725
726 726 self.audited.add(path)
727 727 # only add prefixes to the cache after checking everything: we don't
728 728 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
729 729 self.auditeddir.update(prefixes)
730 730
731 731 def _makelock_file(info, pathname):
732 732 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
733 733 os.write(ld, info)
734 734 os.close(ld)
735 735
736 736 def _readlock_file(pathname):
737 737 return posixfile(pathname).read()
738 738
739 739 def nlinks(pathname):
740 740 """Return number of hardlinks for the given file."""
741 741 return os.lstat(pathname).st_nlink
742 742
743 743 if hasattr(os, 'link'):
744 744 os_link = os.link
745 745 else:
746 746 def os_link(src, dst):
747 747 raise OSError(0, _("Hardlinks not supported"))
748 748
749 749 def fstat(fp):
750 750 '''stat file object that may not have fileno method.'''
751 751 try:
752 752 return os.fstat(fp.fileno())
753 753 except AttributeError:
754 754 return os.stat(fp.name)
755 755
756 756 posixfile = file
757 757
758 758 def openhardlinks():
759 759 '''return true if it is safe to hold open file handles to hardlinks'''
760 760 return True
761 761
762 762 getuser_fallback = None
763 763
764 764 def getuser():
765 765 '''return name of current user'''
766 766 try:
767 767 return getpass.getuser()
768 768 except ImportError:
769 769 # import of pwd will fail on windows - try fallback
770 770 if getuser_fallback:
771 771 return getuser_fallback()
772 772 # raised if win32api not available
773 773 raise Abort(_('user name not available - set USERNAME '
774 774 'environment variable'))
775 775
776 776 def username(uid=None):
777 777 """Return the name of the user with the given uid.
778 778
779 779 If uid is None, return the name of the current user."""
780 780 try:
781 781 import pwd
782 782 if uid is None:
783 783 uid = os.getuid()
784 784 try:
785 785 return pwd.getpwuid(uid)[0]
786 786 except KeyError:
787 787 return str(uid)
788 788 except ImportError:
789 789 return None
790 790
791 791 def groupname(gid=None):
792 792 """Return the name of the group with the given gid.
793 793
794 794 If gid is None, return the name of the current group."""
795 795 try:
796 796 import grp
797 797 if gid is None:
798 798 gid = os.getgid()
799 799 try:
800 800 return grp.getgrgid(gid)[0]
801 801 except KeyError:
802 802 return str(gid)
803 803 except ImportError:
804 804 return None
805 805
806 806 # File system features
807 807
808 808 def checkfolding(path):
809 809 """
810 810 Check whether the given path is on a case-sensitive filesystem
811 811
812 812 Requires a path (like /foo/.hg) ending with a foldable final
813 813 directory component.
814 814 """
815 815 s1 = os.stat(path)
816 816 d, b = os.path.split(path)
817 817 p2 = os.path.join(d, b.upper())
818 818 if path == p2:
819 819 p2 = os.path.join(d, b.lower())
820 820 try:
821 821 s2 = os.stat(p2)
822 822 if s2 == s1:
823 823 return False
824 824 return True
825 825 except:
826 826 return True
827 827
828 828 def checkexec(path):
829 829 """
830 830 Check whether the given path is on a filesystem with UNIX-like exec flags
831 831
832 832 Requires a directory (like /foo/.hg)
833 833 """
834 834
835 835 # VFAT on some Linux versions can flip mode but it doesn't persist
836 836 # a FS remount. Frequently we can detect it if files are created
837 837 # with exec bit on.
838 838
839 839 try:
840 840 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
841 841 fh, fn = tempfile.mkstemp("", "", path)
842 842 try:
843 843 os.close(fh)
844 844 m = os.stat(fn).st_mode & 0777
845 845 new_file_has_exec = m & EXECFLAGS
846 846 os.chmod(fn, m ^ EXECFLAGS)
847 847 exec_flags_cannot_flip = ((os.stat(fn).st_mode & 0777) == m)
848 848 finally:
849 849 os.unlink(fn)
850 850 except (IOError, OSError):
851 851 # we don't care, the user probably won't be able to commit anyway
852 852 return False
853 853 return not (new_file_has_exec or exec_flags_cannot_flip)
854 854
855 855 def execfunc(path, fallback):
856 856 '''return an is_exec() function with default to fallback'''
857 857 if checkexec(path):
858 858 return lambda x: is_exec(os.path.join(path, x))
859 859 return fallback
860 860
861 861 def checklink(path):
862 862 """check whether the given path is on a symlink-capable filesystem"""
863 863 # mktemp is not racy because symlink creation will fail if the
864 864 # file already exists
865 865 name = tempfile.mktemp(dir=path)
866 866 try:
867 867 os.symlink(".", name)
868 868 os.unlink(name)
869 869 return True
870 870 except (OSError, AttributeError):
871 871 return False
872 872
873 873 def linkfunc(path, fallback):
874 874 '''return an is_link() function with default to fallback'''
875 875 if checklink(path):
876 876 return lambda x: os.path.islink(os.path.join(path, x))
877 877 return fallback
878 878
879 879 _umask = os.umask(0)
880 880 os.umask(_umask)
881 881
882 882 def needbinarypatch():
883 883 """return True if patches should be applied in binary mode by default."""
884 884 return os.name == 'nt'
885 885
886 886 def endswithsep(path):
887 887 '''Check path ends with os.sep or os.altsep.'''
888 888 return path.endswith(os.sep) or os.altsep and path.endswith(os.altsep)
889 889
890 890 def splitpath(path):
891 891 '''Split path by os.sep.
892 892 Note that this function does not use os.altsep because this is
893 893 an alternative of simple "xxx.split(os.sep)".
894 894 It is recommended to use os.path.normpath() before using this
895 895 function if need.'''
896 896 return path.split(os.sep)
897 897
898 898 # Platform specific variants
899 899 if os.name == 'nt':
900 900 import msvcrt
901 901 nulldev = 'NUL:'
902 902
903 903 class winstdout:
904 904 '''stdout on windows misbehaves if sent through a pipe'''
905 905
906 906 def __init__(self, fp):
907 907 self.fp = fp
908 908
909 909 def __getattr__(self, key):
910 910 return getattr(self.fp, key)
911 911
912 912 def close(self):
913 913 try:
914 914 self.fp.close()
915 915 except: pass
916 916
917 917 def write(self, s):
918 918 try:
919 919 # This is workaround for "Not enough space" error on
920 920 # writing large size of data to console.
921 921 limit = 16000
922 922 l = len(s)
923 923 start = 0
924 924 while start < l:
925 925 end = start + limit
926 926 self.fp.write(s[start:end])
927 927 start = end
928 928 except IOError, inst:
929 929 if inst.errno != 0: raise
930 930 self.close()
931 931 raise IOError(errno.EPIPE, 'Broken pipe')
932 932
933 933 def flush(self):
934 934 try:
935 935 return self.fp.flush()
936 936 except IOError, inst:
937 937 if inst.errno != errno.EINVAL: raise
938 938 self.close()
939 939 raise IOError(errno.EPIPE, 'Broken pipe')
940 940
941 941 sys.stdout = winstdout(sys.stdout)
942 942
943 943 def _is_win_9x():
944 944 '''return true if run on windows 95, 98 or me.'''
945 945 try:
946 946 return sys.getwindowsversion()[3] == 1
947 947 except AttributeError:
948 948 return 'command' in os.environ.get('comspec', '')
949 949
950 950 def openhardlinks():
951 951 return not _is_win_9x and "win32api" in locals()
952 952
953 953 def system_rcpath():
954 954 try:
955 955 return system_rcpath_win32()
956 956 except:
957 957 return [r'c:\mercurial\mercurial.ini']
958 958
959 959 def user_rcpath():
960 960 '''return os-specific hgrc search path to the user dir'''
961 961 try:
962 962 userrc = user_rcpath_win32()
963 963 except:
964 964 userrc = os.path.join(os.path.expanduser('~'), 'mercurial.ini')
965 965 path = [userrc]
966 966 userprofile = os.environ.get('USERPROFILE')
967 967 if userprofile:
968 968 path.append(os.path.join(userprofile, 'mercurial.ini'))
969 969 return path
970 970
971 971 def parse_patch_output(output_line):
972 972 """parses the output produced by patch and returns the file name"""
973 973 pf = output_line[14:]
974 974 if pf[0] == '`':
975 975 pf = pf[1:-1] # Remove the quotes
976 976 return pf
977 977
978 978 def sshargs(sshcmd, host, user, port):
979 979 '''Build argument list for ssh or Plink'''
980 980 pflag = 'plink' in sshcmd.lower() and '-P' or '-p'
981 981 args = user and ("%s@%s" % (user, host)) or host
982 982 return port and ("%s %s %s" % (args, pflag, port)) or args
983 983
984 984 def testpid(pid):
985 985 '''return False if pid dead, True if running or not known'''
986 986 return True
987 987
988 988 def set_flags(f, flags):
989 989 pass
990 990
991 991 def set_binary(fd):
992 992 msvcrt.setmode(fd.fileno(), os.O_BINARY)
993 993
994 994 def pconvert(path):
995 995 return '/'.join(splitpath(path))
996 996
997 997 def localpath(path):
998 998 return path.replace('/', '\\')
999 999
1000 1000 def normpath(path):
1001 1001 return pconvert(os.path.normpath(path))
1002 1002
1003 1003 makelock = _makelock_file
1004 1004 readlock = _readlock_file
1005 1005
1006 1006 def samestat(s1, s2):
1007 1007 return False
1008 1008
1009 1009 # A sequence of backslashes is special iff it precedes a double quote:
1010 1010 # - if there's an even number of backslashes, the double quote is not
1011 1011 # quoted (i.e. it ends the quoted region)
1012 1012 # - if there's an odd number of backslashes, the double quote is quoted
1013 1013 # - in both cases, every pair of backslashes is unquoted into a single
1014 1014 # backslash
1015 1015 # (See http://msdn2.microsoft.com/en-us/library/a1y7w461.aspx )
1016 1016 # So, to quote a string, we must surround it in double quotes, double
1017 1017 # the number of backslashes that preceed double quotes and add another
1018 1018 # backslash before every double quote (being careful with the double
1019 1019 # quote we've appended to the end)
1020 1020 _quotere = None
1021 1021 def shellquote(s):
1022 1022 global _quotere
1023 1023 if _quotere is None:
1024 1024 _quotere = re.compile(r'(\\*)("|\\$)')
1025 1025 return '"%s"' % _quotere.sub(r'\1\1\\\2', s)
1026 1026
1027 1027 def quotecommand(cmd):
1028 1028 """Build a command string suitable for os.popen* calls."""
1029 1029 # The extra quotes are needed because popen* runs the command
1030 1030 # through the current COMSPEC. cmd.exe suppress enclosing quotes.
1031 1031 return '"' + cmd + '"'
1032 1032
1033 1033 def popen(command):
1034 1034 # Work around "popen spawned process may not write to stdout
1035 1035 # under windows"
1036 1036 # http://bugs.python.org/issue1366
1037 1037 command += " 2> %s" % nulldev
1038 1038 return os.popen(quotecommand(command))
1039 1039
1040 1040 def explain_exit(code):
1041 1041 return _("exited with status %d") % code, code
1042 1042
1043 1043 # if you change this stub into a real check, please try to implement the
1044 1044 # username and groupname functions above, too.
1045 1045 def isowner(fp, st=None):
1046 1046 return True
1047 1047
1048 1048 def find_in_path(name, path, default=None):
1049 1049 '''find name in search path. path can be string (will be split
1050 1050 with os.pathsep), or iterable thing that returns strings. if name
1051 1051 found, return path to name. else return default. name is looked up
1052 1052 using cmd.exe rules, using PATHEXT.'''
1053 1053 if isinstance(path, str):
1054 1054 path = path.split(os.pathsep)
1055 1055
1056 1056 pathext = os.environ.get('PATHEXT', '.COM;.EXE;.BAT;.CMD')
1057 1057 pathext = pathext.lower().split(os.pathsep)
1058 1058 isexec = os.path.splitext(name)[1].lower() in pathext
1059 1059
1060 1060 for p in path:
1061 1061 p_name = os.path.join(p, name)
1062 1062
1063 1063 if isexec and os.path.exists(p_name):
1064 1064 return p_name
1065 1065
1066 1066 for ext in pathext:
1067 1067 p_name_ext = p_name + ext
1068 1068 if os.path.exists(p_name_ext):
1069 1069 return p_name_ext
1070 1070 return default
1071 1071
1072 1072 def set_signal_handler():
1073 1073 try:
1074 1074 set_signal_handler_win32()
1075 1075 except NameError:
1076 1076 pass
1077 1077
1078 1078 try:
1079 1079 # override functions with win32 versions if possible
1080 1080 from util_win32 import *
1081 1081 if not _is_win_9x():
1082 1082 posixfile = posixfile_nt
1083 1083 except ImportError:
1084 1084 pass
1085 1085
1086 1086 else:
1087 1087 nulldev = '/dev/null'
1088 1088
1089 1089 def rcfiles(path):
1090 1090 rcs = [os.path.join(path, 'hgrc')]
1091 1091 rcdir = os.path.join(path, 'hgrc.d')
1092 1092 try:
1093 1093 rcs.extend([os.path.join(rcdir, f)
1094 1094 for f, kind in osutil.listdir(rcdir)
1095 1095 if f.endswith(".rc")])
1096 1096 except OSError:
1097 1097 pass
1098 1098 return rcs
1099 1099
1100 1100 def system_rcpath():
1101 1101 path = []
1102 1102 # old mod_python does not set sys.argv
1103 1103 if len(getattr(sys, 'argv', [])) > 0:
1104 1104 path.extend(rcfiles(os.path.dirname(sys.argv[0]) +
1105 1105 '/../etc/mercurial'))
1106 1106 path.extend(rcfiles('/etc/mercurial'))
1107 1107 return path
1108 1108
1109 1109 def user_rcpath():
1110 1110 return [os.path.expanduser('~/.hgrc')]
1111 1111
1112 1112 def parse_patch_output(output_line):
1113 1113 """parses the output produced by patch and returns the file name"""
1114 1114 pf = output_line[14:]
1115 1115 if os.sys.platform == 'OpenVMS':
1116 1116 if pf[0] == '`':
1117 1117 pf = pf[1:-1] # Remove the quotes
1118 1118 else:
1119 1119 if pf.startswith("'") and pf.endswith("'") and " " in pf:
1120 1120 pf = pf[1:-1] # Remove the quotes
1121 1121 return pf
1122 1122
1123 1123 def sshargs(sshcmd, host, user, port):
1124 1124 '''Build argument list for ssh'''
1125 1125 args = user and ("%s@%s" % (user, host)) or host
1126 1126 return port and ("%s -p %s" % (args, port)) or args
1127 1127
1128 1128 def is_exec(f):
1129 1129 """check whether a file is executable"""
1130 1130 return (os.lstat(f).st_mode & 0100 != 0)
1131 1131
1132 1132 def set_flags(f, flags):
1133 1133 s = os.lstat(f).st_mode
1134 1134 x = "x" in flags
1135 1135 l = "l" in flags
1136 1136 if l:
1137 1137 if not stat.S_ISLNK(s):
1138 1138 # switch file to link
1139 1139 data = file(f).read()
1140 1140 os.unlink(f)
1141 1141 os.symlink(data, f)
1142 1142 # no chmod needed at this point
1143 1143 return
1144 1144 if stat.S_ISLNK(s):
1145 1145 # switch link to file
1146 1146 data = os.readlink(f)
1147 1147 os.unlink(f)
1148 1148 file(f, "w").write(data)
1149 1149 s = 0666 & ~_umask # avoid restatting for chmod
1150 1150
1151 1151 sx = s & 0100
1152 1152 if x and not sx:
1153 1153 # Turn on +x for every +r bit when making a file executable
1154 1154 # and obey umask.
1155 1155 os.chmod(f, s | (s & 0444) >> 2 & ~_umask)
1156 1156 elif not x and sx:
1157 1157 # Turn off all +x bits
1158 1158 os.chmod(f, s & 0666)
1159 1159
1160 1160 def set_binary(fd):
1161 1161 pass
1162 1162
1163 1163 def pconvert(path):
1164 1164 return path
1165 1165
1166 1166 def localpath(path):
1167 1167 return path
1168 1168
1169 1169 normpath = os.path.normpath
1170 1170 samestat = os.path.samestat
1171 1171
1172 1172 def makelock(info, pathname):
1173 1173 try:
1174 1174 os.symlink(info, pathname)
1175 1175 except OSError, why:
1176 1176 if why.errno == errno.EEXIST:
1177 1177 raise
1178 1178 else:
1179 1179 _makelock_file(info, pathname)
1180 1180
1181 1181 def readlock(pathname):
1182 1182 try:
1183 1183 return os.readlink(pathname)
1184 1184 except OSError, why:
1185 1185 if why.errno in (errno.EINVAL, errno.ENOSYS):
1186 1186 return _readlock_file(pathname)
1187 1187 else:
1188 1188 raise
1189 1189
1190 1190 def shellquote(s):
1191 1191 if os.sys.platform == 'OpenVMS':
1192 1192 return '"%s"' % s
1193 1193 else:
1194 1194 return "'%s'" % s.replace("'", "'\\''")
1195 1195
1196 1196 def quotecommand(cmd):
1197 1197 return cmd
1198 1198
1199 1199 def popen(command):
1200 1200 return os.popen(command)
1201 1201
1202 1202 def testpid(pid):
1203 1203 '''return False if pid dead, True if running or not sure'''
1204 1204 if os.sys.platform == 'OpenVMS':
1205 1205 return True
1206 1206 try:
1207 1207 os.kill(pid, 0)
1208 1208 return True
1209 1209 except OSError, inst:
1210 1210 return inst.errno != errno.ESRCH
1211 1211
1212 1212 def explain_exit(code):
1213 1213 """return a 2-tuple (desc, code) describing a process's status"""
1214 1214 if os.WIFEXITED(code):
1215 1215 val = os.WEXITSTATUS(code)
1216 1216 return _("exited with status %d") % val, val
1217 1217 elif os.WIFSIGNALED(code):
1218 1218 val = os.WTERMSIG(code)
1219 1219 return _("killed by signal %d") % val, val
1220 1220 elif os.WIFSTOPPED(code):
1221 1221 val = os.WSTOPSIG(code)
1222 1222 return _("stopped by signal %d") % val, val
1223 1223 raise ValueError(_("invalid exit code"))
1224 1224
1225 1225 def isowner(fp, st=None):
1226 1226 """Return True if the file object f belongs to the current user.
1227 1227
1228 1228 The return value of a util.fstat(f) may be passed as the st argument.
1229 1229 """
1230 1230 if st is None:
1231 1231 st = fstat(fp)
1232 1232 return st.st_uid == os.getuid()
1233 1233
1234 1234 def find_in_path(name, path, default=None):
1235 1235 '''find name in search path. path can be string (will be split
1236 1236 with os.pathsep), or iterable thing that returns strings. if name
1237 1237 found, return path to name. else return default.'''
1238 1238 if isinstance(path, str):
1239 1239 path = path.split(os.pathsep)
1240 1240 for p in path:
1241 1241 p_name = os.path.join(p, name)
1242 1242 if os.path.exists(p_name):
1243 1243 return p_name
1244 1244 return default
1245 1245
1246 1246 def set_signal_handler():
1247 1247 pass
1248 1248
1249 1249 def find_exe(name, default=None):
1250 1250 '''find path of an executable.
1251 1251 if name contains a path component, return it as is. otherwise,
1252 1252 use normal executable search path.'''
1253 1253
1254 1254 if os.sep in name or sys.platform == 'OpenVMS':
1255 1255 # don't check the executable bit. if the file isn't
1256 1256 # executable, whoever tries to actually run it will give a
1257 1257 # much more useful error message.
1258 1258 return name
1259 1259 return find_in_path(name, os.environ.get('PATH', ''), default=default)
1260 1260
1261 1261 def _buildencodefun():
1262 1262 e = '_'
1263 1263 win_reserved = [ord(x) for x in '\\:*?"<>|']
1264 1264 cmap = dict([ (chr(x), chr(x)) for x in xrange(127) ])
1265 1265 for x in (range(32) + range(126, 256) + win_reserved):
1266 1266 cmap[chr(x)] = "~%02x" % x
1267 1267 for x in range(ord("A"), ord("Z")+1) + [ord(e)]:
1268 1268 cmap[chr(x)] = e + chr(x).lower()
1269 1269 dmap = {}
1270 1270 for k, v in cmap.iteritems():
1271 1271 dmap[v] = k
1272 1272 def decode(s):
1273 1273 i = 0
1274 1274 while i < len(s):
1275 1275 for l in xrange(1, 4):
1276 1276 try:
1277 1277 yield dmap[s[i:i+l]]
1278 1278 i += l
1279 1279 break
1280 1280 except KeyError:
1281 1281 pass
1282 1282 else:
1283 1283 raise KeyError
1284 1284 return (lambda s: "".join([cmap[c] for c in s]),
1285 1285 lambda s: "".join(list(decode(s))))
1286 1286
1287 1287 encodefilename, decodefilename = _buildencodefun()
1288 1288
1289 1289 def encodedopener(openerfn, fn):
1290 1290 def o(path, *args, **kw):
1291 1291 return openerfn(fn(path), *args, **kw)
1292 1292 return o
1293 1293
1294 1294 def mktempcopy(name, emptyok=False):
1295 1295 """Create a temporary file with the same contents from name
1296 1296
1297 1297 The permission bits are copied from the original file.
1298 1298
1299 1299 If the temporary file is going to be truncated immediately, you
1300 1300 can use emptyok=True as an optimization.
1301 1301
1302 1302 Returns the name of the temporary file.
1303 1303 """
1304 1304 d, fn = os.path.split(name)
1305 1305 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
1306 1306 os.close(fd)
1307 1307 # Temporary files are created with mode 0600, which is usually not
1308 1308 # what we want. If the original file already exists, just copy
1309 1309 # its mode. Otherwise, manually obey umask.
1310 1310 try:
1311 1311 st_mode = os.lstat(name).st_mode & 0777
1312 1312 except OSError, inst:
1313 1313 if inst.errno != errno.ENOENT:
1314 1314 raise
1315 1315 st_mode = 0666 & ~_umask
1316 1316 os.chmod(temp, st_mode)
1317 1317 if emptyok:
1318 1318 return temp
1319 1319 try:
1320 1320 try:
1321 1321 ifp = posixfile(name, "rb")
1322 1322 except IOError, inst:
1323 1323 if inst.errno == errno.ENOENT:
1324 1324 return temp
1325 1325 if not getattr(inst, 'filename', None):
1326 1326 inst.filename = name
1327 1327 raise
1328 1328 ofp = posixfile(temp, "wb")
1329 1329 for chunk in filechunkiter(ifp):
1330 1330 ofp.write(chunk)
1331 1331 ifp.close()
1332 1332 ofp.close()
1333 1333 except:
1334 1334 try: os.unlink(temp)
1335 1335 except: pass
1336 1336 raise
1337 1337 return temp
1338 1338
1339 1339 class atomictempfile(posixfile):
1340 1340 """file-like object that atomically updates a file
1341 1341
1342 1342 All writes will be redirected to a temporary copy of the original
1343 1343 file. When rename is called, the copy is renamed to the original
1344 1344 name, making the changes visible.
1345 1345 """
1346 1346 def __init__(self, name, mode):
1347 1347 self.__name = name
1348 1348 self.temp = mktempcopy(name, emptyok=('w' in mode))
1349 1349 posixfile.__init__(self, self.temp, mode)
1350 1350
1351 1351 def rename(self):
1352 1352 if not self.closed:
1353 1353 posixfile.close(self)
1354 1354 rename(self.temp, localpath(self.__name))
1355 1355
1356 1356 def __del__(self):
1357 1357 if not self.closed:
1358 1358 try:
1359 1359 os.unlink(self.temp)
1360 1360 except: pass
1361 1361 posixfile.close(self)
1362 1362
1363 1363 class opener(object):
1364 1364 """Open files relative to a base directory
1365 1365
1366 1366 This class is used to hide the details of COW semantics and
1367 1367 remote file access from higher level code.
1368 1368 """
1369 1369 def __init__(self, base, audit=True):
1370 1370 self.base = base
1371 1371 if audit:
1372 1372 self.audit_path = path_auditor(base)
1373 1373 else:
1374 1374 self.audit_path = always
1375 1375
1376 1376 def __getattr__(self, name):
1377 1377 if name == '_can_symlink':
1378 1378 self._can_symlink = checklink(self.base)
1379 1379 return self._can_symlink
1380 1380 raise AttributeError(name)
1381 1381
1382 1382 def __call__(self, path, mode="r", text=False, atomictemp=False):
1383 1383 self.audit_path(path)
1384 1384 f = os.path.join(self.base, path)
1385 1385
1386 1386 if not text and "b" not in mode:
1387 1387 mode += "b" # for that other OS
1388 1388
1389 1389 if mode[0] != "r":
1390 1390 try:
1391 1391 nlink = nlinks(f)
1392 1392 except OSError:
1393 1393 nlink = 0
1394 1394 d = os.path.dirname(f)
1395 1395 if not os.path.isdir(d):
1396 1396 os.makedirs(d)
1397 1397 if atomictemp:
1398 1398 return atomictempfile(f, mode)
1399 1399 if nlink > 1:
1400 1400 rename(mktempcopy(f), f)
1401 1401 return posixfile(f, mode)
1402 1402
1403 1403 def symlink(self, src, dst):
1404 1404 self.audit_path(dst)
1405 1405 linkname = os.path.join(self.base, dst)
1406 1406 try:
1407 1407 os.unlink(linkname)
1408 1408 except OSError:
1409 1409 pass
1410 1410
1411 1411 dirname = os.path.dirname(linkname)
1412 1412 if not os.path.exists(dirname):
1413 1413 os.makedirs(dirname)
1414 1414
1415 1415 if self._can_symlink:
1416 1416 try:
1417 1417 os.symlink(src, linkname)
1418 1418 except OSError, err:
1419 1419 raise OSError(err.errno, _('could not symlink to %r: %s') %
1420 1420 (src, err.strerror), linkname)
1421 1421 else:
1422 1422 f = self(dst, "w")
1423 1423 f.write(src)
1424 1424 f.close()
1425 1425
1426 1426 class chunkbuffer(object):
1427 1427 """Allow arbitrary sized chunks of data to be efficiently read from an
1428 1428 iterator over chunks of arbitrary size."""
1429 1429
1430 1430 def __init__(self, in_iter):
1431 1431 """in_iter is the iterator that's iterating over the input chunks.
1432 1432 targetsize is how big a buffer to try to maintain."""
1433 1433 self.iter = iter(in_iter)
1434 1434 self.buf = ''
1435 1435 self.targetsize = 2**16
1436 1436
1437 1437 def read(self, l):
1438 1438 """Read L bytes of data from the iterator of chunks of data.
1439 1439 Returns less than L bytes if the iterator runs dry."""
1440 1440 if l > len(self.buf) and self.iter:
1441 1441 # Clamp to a multiple of self.targetsize
1442 1442 targetsize = max(l, self.targetsize)
1443 1443 collector = cStringIO.StringIO()
1444 1444 collector.write(self.buf)
1445 1445 collected = len(self.buf)
1446 1446 for chunk in self.iter:
1447 1447 collector.write(chunk)
1448 1448 collected += len(chunk)
1449 1449 if collected >= targetsize:
1450 1450 break
1451 1451 if collected < targetsize:
1452 1452 self.iter = False
1453 1453 self.buf = collector.getvalue()
1454 1454 if len(self.buf) == l:
1455 1455 s, self.buf = str(self.buf), ''
1456 1456 else:
1457 1457 s, self.buf = self.buf[:l], buffer(self.buf, l)
1458 1458 return s
1459 1459
1460 1460 def filechunkiter(f, size=65536, limit=None):
1461 1461 """Create a generator that produces the data in the file size
1462 1462 (default 65536) bytes at a time, up to optional limit (default is
1463 1463 to read all data). Chunks may be less than size bytes if the
1464 1464 chunk is the last chunk in the file, or the file is a socket or
1465 1465 some other type of file that sometimes reads less data than is
1466 1466 requested."""
1467 1467 assert size >= 0
1468 1468 assert limit is None or limit >= 0
1469 1469 while True:
1470 1470 if limit is None: nbytes = size
1471 1471 else: nbytes = min(limit, size)
1472 1472 s = nbytes and f.read(nbytes)
1473 1473 if not s: break
1474 1474 if limit: limit -= len(s)
1475 1475 yield s
1476 1476
1477 1477 def makedate():
1478 1478 lt = time.localtime()
1479 1479 if lt[8] == 1 and time.daylight:
1480 1480 tz = time.altzone
1481 1481 else:
1482 1482 tz = time.timezone
1483 1483 return time.mktime(lt), tz
1484 1484
1485 1485 def datestr(date=None, format='%a %b %d %H:%M:%S %Y', timezone=True, timezone_format=" %+03d%02d"):
1486 1486 """represent a (unixtime, offset) tuple as a localized time.
1487 1487 unixtime is seconds since the epoch, and offset is the time zone's
1488 1488 number of seconds away from UTC. if timezone is false, do not
1489 1489 append time zone to string."""
1490 1490 t, tz = date or makedate()
1491 1491 s = time.strftime(format, time.gmtime(float(t) - tz))
1492 1492 if timezone:
1493 1493 s += timezone_format % (-tz / 3600, ((-tz % 3600) / 60))
1494 1494 return s
1495 1495
1496 1496 def strdate(string, format, defaults=[]):
1497 1497 """parse a localized time string and return a (unixtime, offset) tuple.
1498 1498 if the string cannot be parsed, ValueError is raised."""
1499 1499 def timezone(string):
1500 1500 tz = string.split()[-1]
1501 1501 if tz[0] in "+-" and len(tz) == 5 and tz[1:].isdigit():
1502 1502 tz = int(tz)
1503 1503 offset = - 3600 * (tz / 100) - 60 * (tz % 100)
1504 1504 return offset
1505 1505 if tz == "GMT" or tz == "UTC":
1506 1506 return 0
1507 1507 return None
1508 1508
1509 1509 # NOTE: unixtime = localunixtime + offset
1510 1510 offset, date = timezone(string), string
1511 1511 if offset != None:
1512 1512 date = " ".join(string.split()[:-1])
1513 1513
1514 1514 # add missing elements from defaults
1515 1515 for part in defaults:
1516 1516 found = [True for p in part if ("%"+p) in format]
1517 1517 if not found:
1518 1518 date += "@" + defaults[part]
1519 1519 format += "@%" + part[0]
1520 1520
1521 1521 timetuple = time.strptime(date, format)
1522 1522 localunixtime = int(calendar.timegm(timetuple))
1523 1523 if offset is None:
1524 1524 # local timezone
1525 1525 unixtime = int(time.mktime(timetuple))
1526 1526 offset = unixtime - localunixtime
1527 1527 else:
1528 1528 unixtime = localunixtime + offset
1529 1529 return unixtime, offset
1530 1530
1531 1531 def parsedate(string, formats=None, defaults=None):
1532 1532 """parse a localized time string and return a (unixtime, offset) tuple.
1533 1533 The date may be a "unixtime offset" string or in one of the specified
1534 1534 formats."""
1535 1535 if not string:
1536 1536 return 0, 0
1537 1537 if not formats:
1538 1538 formats = defaultdateformats
1539 1539 string = string.strip()
1540 1540 try:
1541 1541 when, offset = map(int, string.split(' '))
1542 1542 except ValueError:
1543 1543 # fill out defaults
1544 1544 if not defaults:
1545 1545 defaults = {}
1546 1546 now = makedate()
1547 1547 for part in "d mb yY HI M S".split():
1548 1548 if part not in defaults:
1549 1549 if part[0] in "HMS":
1550 1550 defaults[part] = "00"
1551 1551 elif part[0] in "dm":
1552 1552 defaults[part] = "1"
1553 1553 else:
1554 1554 defaults[part] = datestr(now, "%" + part[0], False)
1555 1555
1556 1556 for format in formats:
1557 1557 try:
1558 1558 when, offset = strdate(string, format, defaults)
1559 1559 except ValueError:
1560 1560 pass
1561 1561 else:
1562 1562 break
1563 1563 else:
1564 1564 raise Abort(_('invalid date: %r ') % string)
1565 1565 # validate explicit (probably user-specified) date and
1566 1566 # time zone offset. values must fit in signed 32 bits for
1567 1567 # current 32-bit linux runtimes. timezones go from UTC-12
1568 1568 # to UTC+14
1569 1569 if abs(when) > 0x7fffffff:
1570 1570 raise Abort(_('date exceeds 32 bits: %d') % when)
1571 1571 if offset < -50400 or offset > 43200:
1572 1572 raise Abort(_('impossible time zone offset: %d') % offset)
1573 1573 return when, offset
1574 1574
1575 1575 def matchdate(date):
1576 1576 """Return a function that matches a given date match specifier
1577 1577
1578 1578 Formats include:
1579 1579
1580 1580 '{date}' match a given date to the accuracy provided
1581 1581
1582 1582 '<{date}' on or before a given date
1583 1583
1584 1584 '>{date}' on or after a given date
1585 1585
1586 1586 """
1587 1587
1588 1588 def lower(date):
1589 1589 return parsedate(date, extendeddateformats)[0]
1590 1590
1591 1591 def upper(date):
1592 1592 d = dict(mb="12", HI="23", M="59", S="59")
1593 1593 for days in "31 30 29".split():
1594 1594 try:
1595 1595 d["d"] = days
1596 1596 return parsedate(date, extendeddateformats, d)[0]
1597 1597 except:
1598 1598 pass
1599 1599 d["d"] = "28"
1600 1600 return parsedate(date, extendeddateformats, d)[0]
1601 1601
1602 1602 if date[0] == "<":
1603 1603 when = upper(date[1:])
1604 1604 return lambda x: x <= when
1605 1605 elif date[0] == ">":
1606 1606 when = lower(date[1:])
1607 1607 return lambda x: x >= when
1608 1608 elif date[0] == "-":
1609 1609 try:
1610 1610 days = int(date[1:])
1611 1611 except ValueError:
1612 1612 raise Abort(_("invalid day spec: %s") % date[1:])
1613 1613 when = makedate()[0] - days * 3600 * 24
1614 1614 return lambda x: x >= when
1615 1615 elif " to " in date:
1616 1616 a, b = date.split(" to ")
1617 1617 start, stop = lower(a), upper(b)
1618 1618 return lambda x: x >= start and x <= stop
1619 1619 else:
1620 1620 start, stop = lower(date), upper(date)
1621 1621 return lambda x: x >= start and x <= stop
1622 1622
1623 1623 def shortuser(user):
1624 1624 """Return a short representation of a user name or email address."""
1625 1625 f = user.find('@')
1626 1626 if f >= 0:
1627 1627 user = user[:f]
1628 1628 f = user.find('<')
1629 1629 if f >= 0:
1630 1630 user = user[f+1:]
1631 1631 f = user.find(' ')
1632 1632 if f >= 0:
1633 1633 user = user[:f]
1634 1634 f = user.find('.')
1635 1635 if f >= 0:
1636 1636 user = user[:f]
1637 1637 return user
1638 1638
1639 def email(author):
1640 '''get email of author.'''
1641 r = author.find('>')
1642 if r == -1: r = None
1643 return author[author.find('<')+1:r]
1644
1639 1645 def ellipsis(text, maxlength=400):
1640 1646 """Trim string to at most maxlength (default: 400) characters."""
1641 1647 if len(text) <= maxlength:
1642 1648 return text
1643 1649 else:
1644 1650 return "%s..." % (text[:maxlength-3])
1645 1651
1646 1652 def walkrepos(path):
1647 1653 '''yield every hg repository under path, recursively.'''
1648 1654 def errhandler(err):
1649 1655 if err.filename == path:
1650 1656 raise err
1651 1657
1652 1658 for root, dirs, files in os.walk(path, onerror=errhandler):
1653 1659 for d in dirs:
1654 1660 if d == '.hg':
1655 1661 yield root
1656 1662 dirs[:] = []
1657 1663 break
1658 1664
1659 1665 _rcpath = None
1660 1666
1661 1667 def os_rcpath():
1662 1668 '''return default os-specific hgrc search path'''
1663 1669 path = system_rcpath()
1664 1670 path.extend(user_rcpath())
1665 1671 path = [os.path.normpath(f) for f in path]
1666 1672 return path
1667 1673
1668 1674 def rcpath():
1669 1675 '''return hgrc search path. if env var HGRCPATH is set, use it.
1670 1676 for each item in path, if directory, use files ending in .rc,
1671 1677 else use item.
1672 1678 make HGRCPATH empty to only look in .hg/hgrc of current repo.
1673 1679 if no HGRCPATH, use default os-specific path.'''
1674 1680 global _rcpath
1675 1681 if _rcpath is None:
1676 1682 if 'HGRCPATH' in os.environ:
1677 1683 _rcpath = []
1678 1684 for p in os.environ['HGRCPATH'].split(os.pathsep):
1679 1685 if not p: continue
1680 1686 if os.path.isdir(p):
1681 1687 for f, kind in osutil.listdir(p):
1682 1688 if f.endswith('.rc'):
1683 1689 _rcpath.append(os.path.join(p, f))
1684 1690 else:
1685 1691 _rcpath.append(p)
1686 1692 else:
1687 1693 _rcpath = os_rcpath()
1688 1694 return _rcpath
1689 1695
1690 1696 def bytecount(nbytes):
1691 1697 '''return byte count formatted as readable string, with units'''
1692 1698
1693 1699 units = (
1694 1700 (100, 1<<30, _('%.0f GB')),
1695 1701 (10, 1<<30, _('%.1f GB')),
1696 1702 (1, 1<<30, _('%.2f GB')),
1697 1703 (100, 1<<20, _('%.0f MB')),
1698 1704 (10, 1<<20, _('%.1f MB')),
1699 1705 (1, 1<<20, _('%.2f MB')),
1700 1706 (100, 1<<10, _('%.0f KB')),
1701 1707 (10, 1<<10, _('%.1f KB')),
1702 1708 (1, 1<<10, _('%.2f KB')),
1703 1709 (1, 1, _('%.0f bytes')),
1704 1710 )
1705 1711
1706 1712 for multiplier, divisor, format in units:
1707 1713 if nbytes >= divisor * multiplier:
1708 1714 return format % (nbytes / float(divisor))
1709 1715 return units[-1][2] % nbytes
1710 1716
1711 1717 def drop_scheme(scheme, path):
1712 1718 sc = scheme + ':'
1713 1719 if path.startswith(sc):
1714 1720 path = path[len(sc):]
1715 1721 if path.startswith('//'):
1716 1722 path = path[2:]
1717 1723 return path
1718 1724
1719 1725 def uirepr(s):
1720 1726 # Avoid double backslash in Windows path repr()
1721 1727 return repr(s).replace('\\\\', '\\')
1722 1728
1723 1729 def hidepassword(url):
1724 1730 '''hide user credential in a url string'''
1725 1731 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
1726 1732 netloc = re.sub('([^:]*):([^@]*)@(.*)', r'\1:***@\3', netloc)
1727 1733 return urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
1728 1734
1729 1735 def removeauth(url):
1730 1736 '''remove all authentication information from a url string'''
1731 1737 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
1732 1738 netloc = netloc[netloc.find('@')+1:]
1733 1739 return urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
General Comments 0
You need to be logged in to leave comments. Login now