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