##// END OF EJS Templates
ui.progress: clarify termination requirement
Augie Fackler -
r10425:f8a9de66 default
parent child Browse files
Show More
@@ -1,403 +1,405 b''
1 1 # ui.py - user interface bits for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from i18n import _
9 9 import errno, getpass, os, socket, sys, tempfile, traceback
10 10 import config, util, error
11 11
12 12 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True,
13 13 '0': False, 'no': False, 'false': False, 'off': False}
14 14
15 15 class ui(object):
16 16 def __init__(self, src=None):
17 17 self._buffers = []
18 18 self.quiet = self.verbose = self.debugflag = self.tracebackflag = False
19 19 self._reportuntrusted = True
20 20 self._ocfg = config.config() # overlay
21 21 self._tcfg = config.config() # trusted
22 22 self._ucfg = config.config() # untrusted
23 23 self._trustusers = set()
24 24 self._trustgroups = set()
25 25
26 26 if src:
27 27 self._tcfg = src._tcfg.copy()
28 28 self._ucfg = src._ucfg.copy()
29 29 self._ocfg = src._ocfg.copy()
30 30 self._trustusers = src._trustusers.copy()
31 31 self._trustgroups = src._trustgroups.copy()
32 32 self.environ = src.environ
33 33 self.fixconfig()
34 34 else:
35 35 # shared read-only environment
36 36 self.environ = os.environ
37 37 # we always trust global config files
38 38 for f in util.rcpath():
39 39 self.readconfig(f, trust=True)
40 40
41 41 def copy(self):
42 42 return self.__class__(self)
43 43
44 44 def _is_trusted(self, fp, f):
45 45 st = util.fstat(fp)
46 46 if util.isowner(st):
47 47 return True
48 48
49 49 tusers, tgroups = self._trustusers, self._trustgroups
50 50 if '*' in tusers or '*' in tgroups:
51 51 return True
52 52
53 53 user = util.username(st.st_uid)
54 54 group = util.groupname(st.st_gid)
55 55 if user in tusers or group in tgroups or user == util.username():
56 56 return True
57 57
58 58 if self._reportuntrusted:
59 59 self.warn(_('Not trusting file %s from untrusted '
60 60 'user %s, group %s\n') % (f, user, group))
61 61 return False
62 62
63 63 def readconfig(self, filename, root=None, trust=False,
64 64 sections=None, remap=None):
65 65 try:
66 66 fp = open(filename)
67 67 except IOError:
68 68 if not sections: # ignore unless we were looking for something
69 69 return
70 70 raise
71 71
72 72 cfg = config.config()
73 73 trusted = sections or trust or self._is_trusted(fp, filename)
74 74
75 75 try:
76 76 cfg.read(filename, fp, sections=sections, remap=remap)
77 77 except error.ConfigError, inst:
78 78 if trusted:
79 79 raise
80 80 self.warn(_("Ignored: %s\n") % str(inst))
81 81
82 82 if trusted:
83 83 self._tcfg.update(cfg)
84 84 self._tcfg.update(self._ocfg)
85 85 self._ucfg.update(cfg)
86 86 self._ucfg.update(self._ocfg)
87 87
88 88 if root is None:
89 89 root = os.path.expanduser('~')
90 90 self.fixconfig(root=root)
91 91
92 92 def fixconfig(self, root=None):
93 93 # translate paths relative to root (or home) into absolute paths
94 94 root = root or os.getcwd()
95 95 for c in self._tcfg, self._ucfg, self._ocfg:
96 96 for n, p in c.items('paths'):
97 97 if p and "://" not in p and not os.path.isabs(p):
98 98 c.set("paths", n, os.path.normpath(os.path.join(root, p)))
99 99
100 100 # update ui options
101 101 self.debugflag = self.configbool('ui', 'debug')
102 102 self.verbose = self.debugflag or self.configbool('ui', 'verbose')
103 103 self.quiet = not self.debugflag and self.configbool('ui', 'quiet')
104 104 if self.verbose and self.quiet:
105 105 self.quiet = self.verbose = False
106 106 self._reportuntrusted = self.configbool("ui", "report_untrusted", True)
107 107 self.tracebackflag = self.configbool('ui', 'traceback', False)
108 108
109 109 # update trust information
110 110 self._trustusers.update(self.configlist('trusted', 'users'))
111 111 self._trustgroups.update(self.configlist('trusted', 'groups'))
112 112
113 113 def setconfig(self, section, name, value):
114 114 for cfg in (self._ocfg, self._tcfg, self._ucfg):
115 115 cfg.set(section, name, value)
116 116 self.fixconfig()
117 117
118 118 def _data(self, untrusted):
119 119 return untrusted and self._ucfg or self._tcfg
120 120
121 121 def configsource(self, section, name, untrusted=False):
122 122 return self._data(untrusted).source(section, name) or 'none'
123 123
124 124 def config(self, section, name, default=None, untrusted=False):
125 125 value = self._data(untrusted).get(section, name, default)
126 126 if self.debugflag and not untrusted and self._reportuntrusted:
127 127 uvalue = self._ucfg.get(section, name)
128 128 if uvalue is not None and uvalue != value:
129 129 self.debug(_("ignoring untrusted configuration option "
130 130 "%s.%s = %s\n") % (section, name, uvalue))
131 131 return value
132 132
133 133 def configbool(self, section, name, default=False, untrusted=False):
134 134 v = self.config(section, name, None, untrusted)
135 135 if v is None:
136 136 return default
137 137 if isinstance(v, bool):
138 138 return v
139 139 if v.lower() not in _booleans:
140 140 raise error.ConfigError(_("%s.%s not a boolean ('%s')")
141 141 % (section, name, v))
142 142 return _booleans[v.lower()]
143 143
144 144 def configlist(self, section, name, default=None, untrusted=False):
145 145 """Return a list of comma/space separated strings"""
146 146 result = self.config(section, name, untrusted=untrusted)
147 147 if result is None:
148 148 result = default or []
149 149 if isinstance(result, basestring):
150 150 result = result.replace(",", " ").split()
151 151 return result
152 152
153 153 def has_section(self, section, untrusted=False):
154 154 '''tell whether section exists in config.'''
155 155 return section in self._data(untrusted)
156 156
157 157 def configitems(self, section, untrusted=False):
158 158 items = self._data(untrusted).items(section)
159 159 if self.debugflag and not untrusted and self._reportuntrusted:
160 160 for k, v in self._ucfg.items(section):
161 161 if self._tcfg.get(section, k) != v:
162 162 self.debug(_("ignoring untrusted configuration option "
163 163 "%s.%s = %s\n") % (section, k, v))
164 164 return items
165 165
166 166 def walkconfig(self, untrusted=False):
167 167 cfg = self._data(untrusted)
168 168 for section in cfg.sections():
169 169 for name, value in self.configitems(section, untrusted):
170 170 yield section, name, str(value).replace('\n', '\\n')
171 171
172 172 def username(self):
173 173 """Return default username to be used in commits.
174 174
175 175 Searched in this order: $HGUSER, [ui] section of hgrcs, $EMAIL
176 176 and stop searching if one of these is set.
177 177 If not found and ui.askusername is True, ask the user, else use
178 178 ($LOGNAME or $USER or $LNAME or $USERNAME) + "@full.hostname".
179 179 """
180 180 user = os.environ.get("HGUSER")
181 181 if user is None:
182 182 user = self.config("ui", "username")
183 183 if user is None:
184 184 user = os.environ.get("EMAIL")
185 185 if user is None and self.configbool("ui", "askusername"):
186 186 user = self.prompt(_("enter a commit username:"), default=None)
187 187 if user is None and not self.interactive():
188 188 try:
189 189 user = '%s@%s' % (util.getuser(), socket.getfqdn())
190 190 self.warn(_("No username found, using '%s' instead\n") % user)
191 191 except KeyError:
192 192 pass
193 193 if not user:
194 194 raise util.Abort(_('no username supplied (see "hg help config")'))
195 195 if "\n" in user:
196 196 raise util.Abort(_("username %s contains a newline\n") % repr(user))
197 197 return user
198 198
199 199 def shortuser(self, user):
200 200 """Return a short representation of a user name or email address."""
201 201 if not self.verbose:
202 202 user = util.shortuser(user)
203 203 return user
204 204
205 205 def _path(self, loc):
206 206 p = self.config('paths', loc)
207 207 if p:
208 208 if '%%' in p:
209 209 self.warn("(deprecated '%%' in path %s=%s from %s)\n" %
210 210 (loc, p, self.configsource('paths', loc)))
211 211 p = p.replace('%%', '%')
212 212 p = util.expandpath(p)
213 213 return p
214 214
215 215 def expandpath(self, loc, default=None):
216 216 """Return repository location relative to cwd or from [paths]"""
217 217 if "://" in loc or os.path.isdir(os.path.join(loc, '.hg')):
218 218 return loc
219 219
220 220 path = self._path(loc)
221 221 if not path and default is not None:
222 222 path = self._path(default)
223 223 return path or loc
224 224
225 225 def pushbuffer(self):
226 226 self._buffers.append([])
227 227
228 228 def popbuffer(self):
229 229 return "".join(self._buffers.pop())
230 230
231 231 def write(self, *args):
232 232 if self._buffers:
233 233 self._buffers[-1].extend([str(a) for a in args])
234 234 else:
235 235 for a in args:
236 236 sys.stdout.write(str(a))
237 237
238 238 def write_err(self, *args):
239 239 try:
240 240 if not hasattr(sys.stdout, 'closed') or not sys.stdout.closed:
241 241 sys.stdout.flush()
242 242 for a in args:
243 243 sys.stderr.write(str(a))
244 244 # stderr may be buffered under win32 when redirected to files,
245 245 # including stdout.
246 246 if not sys.stderr.closed:
247 247 sys.stderr.flush()
248 248 except IOError, inst:
249 249 if inst.errno != errno.EPIPE:
250 250 raise
251 251
252 252 def flush(self):
253 253 try: sys.stdout.flush()
254 254 except: pass
255 255 try: sys.stderr.flush()
256 256 except: pass
257 257
258 258 def interactive(self):
259 259 i = self.configbool("ui", "interactive", None)
260 260 if i is None:
261 261 try:
262 262 return sys.stdin.isatty()
263 263 except AttributeError:
264 264 # some environments replace stdin without implementing isatty
265 265 # usually those are non-interactive
266 266 return False
267 267
268 268 return i
269 269
270 270 def _readline(self, prompt=''):
271 271 if sys.stdin.isatty():
272 272 try:
273 273 # magically add command line editing support, where
274 274 # available
275 275 import readline
276 276 # force demandimport to really load the module
277 277 readline.read_history_file
278 278 # windows sometimes raises something other than ImportError
279 279 except Exception:
280 280 pass
281 281 line = raw_input(prompt)
282 282 # When stdin is in binary mode on Windows, it can cause
283 283 # raw_input() to emit an extra trailing carriage return
284 284 if os.linesep == '\r\n' and line and line[-1] == '\r':
285 285 line = line[:-1]
286 286 return line
287 287
288 288 def prompt(self, msg, default="y"):
289 289 """Prompt user with msg, read response.
290 290 If ui is not interactive, the default is returned.
291 291 """
292 292 if not self.interactive():
293 293 self.write(msg, ' ', default, "\n")
294 294 return default
295 295 try:
296 296 r = self._readline(msg + ' ')
297 297 if not r:
298 298 return default
299 299 return r
300 300 except EOFError:
301 301 raise util.Abort(_('response expected'))
302 302
303 303 def promptchoice(self, msg, choices, default=0):
304 304 """Prompt user with msg, read response, and ensure it matches
305 305 one of the provided choices. The index of the choice is returned.
306 306 choices is a sequence of acceptable responses with the format:
307 307 ('&None', 'E&xec', 'Sym&link') Responses are case insensitive.
308 308 If ui is not interactive, the default is returned.
309 309 """
310 310 resps = [s[s.index('&')+1].lower() for s in choices]
311 311 while True:
312 312 r = self.prompt(msg, resps[default])
313 313 if r.lower() in resps:
314 314 return resps.index(r.lower())
315 315 self.write(_("unrecognized response\n"))
316 316
317 317 def getpass(self, prompt=None, default=None):
318 318 if not self.interactive():
319 319 return default
320 320 try:
321 321 return getpass.getpass(prompt or _('password: '))
322 322 except EOFError:
323 323 raise util.Abort(_('response expected'))
324 324 def status(self, *msg):
325 325 if not self.quiet:
326 326 self.write(*msg)
327 327 def warn(self, *msg):
328 328 self.write_err(*msg)
329 329 def note(self, *msg):
330 330 if self.verbose:
331 331 self.write(*msg)
332 332 def debug(self, *msg):
333 333 if self.debugflag:
334 334 self.write(*msg)
335 335 def edit(self, text, user):
336 336 (fd, name) = tempfile.mkstemp(prefix="hg-editor-", suffix=".txt",
337 337 text=True)
338 338 try:
339 339 f = os.fdopen(fd, "w")
340 340 f.write(text)
341 341 f.close()
342 342
343 343 editor = self.geteditor()
344 344
345 345 util.system("%s \"%s\"" % (editor, name),
346 346 environ={'HGUSER': user},
347 347 onerr=util.Abort, errprefix=_("edit failed"))
348 348
349 349 f = open(name)
350 350 t = f.read()
351 351 f.close()
352 352 finally:
353 353 os.unlink(name)
354 354
355 355 return t
356 356
357 357 def traceback(self, exc=None):
358 358 '''print exception traceback if traceback printing enabled.
359 359 only to call in exception handler. returns true if traceback
360 360 printed.'''
361 361 if self.tracebackflag:
362 362 if exc:
363 363 traceback.print_exception(exc[0], exc[1], exc[2])
364 364 else:
365 365 traceback.print_exc()
366 366 return self.tracebackflag
367 367
368 368 def geteditor(self):
369 369 '''return editor to use'''
370 370 return (os.environ.get("HGEDITOR") or
371 371 self.config("ui", "editor") or
372 372 os.environ.get("VISUAL") or
373 373 os.environ.get("EDITOR", "vi"))
374 374
375 375 def progress(self, topic, pos, item="", unit="", total=None):
376 376 '''show a progress message
377 377
378 378 With stock hg, this is simply a debug message that is hidden
379 379 by default, but with extensions or GUI tools it may be
380 380 visible. 'topic' is the current operation, 'item' is a
381 381 non-numeric marker of the current position (ie the currently
382 382 in-process file), 'pos' is the current numeric position (ie
383 383 revision, bytes, etc.), unit is a corresponding unit label,
384 384 and total is the highest expected pos.
385 385
386 Multiple nested topics may be active at a time. All topics
387 should be marked closed by setting pos to None at termination.
386 Multiple nested topics may be active at a time.
387
388 All topics should be marked closed by setting pos to None at
389 termination.
388 390 '''
389 391
390 392 if pos == None or not self.debugflag:
391 393 return
392 394
393 395 if unit:
394 396 unit = ' ' + unit
395 397 if item:
396 398 item = ' ' + item
397 399
398 400 if total:
399 401 pct = 100.0 * pos / total
400 402 self.debug('%s:%s %s/%s%s (%4.2f%%)\n'
401 403 % (topic, item, pos, total, unit, pct))
402 404 else:
403 405 self.debug('%s:%s %s%s\n' % (topic, item, pos, unit))
General Comments 0
You need to be logged in to leave comments. Login now