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