##// END OF EJS Templates
make ui.interactive() return false in case stdin lacks isatty
Ronny Pfannschmidt -
r10077:89617aac default
parent child Browse files
Show More
@@ -1,389 +1,395 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, incorporated herein by reference.
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 v.lower() not in _booleans:
138 138 raise error.ConfigError(_("%s.%s not a boolean ('%s')")
139 139 % (section, name, v))
140 140 return _booleans[v.lower()]
141 141
142 142 def configlist(self, section, name, default=None, untrusted=False):
143 143 """Return a list of comma/space separated strings"""
144 144 result = self.config(section, name, untrusted=untrusted)
145 145 if result is None:
146 146 result = default or []
147 147 if isinstance(result, basestring):
148 148 result = result.replace(",", " ").split()
149 149 return result
150 150
151 151 def has_section(self, section, untrusted=False):
152 152 '''tell whether section exists in config.'''
153 153 return section in self._data(untrusted)
154 154
155 155 def configitems(self, section, untrusted=False):
156 156 items = self._data(untrusted).items(section)
157 157 if self.debugflag and not untrusted and self._reportuntrusted:
158 158 for k, v in self._ucfg.items(section):
159 159 if self._tcfg.get(section, k) != v:
160 160 self.debug(_("ignoring untrusted configuration option "
161 161 "%s.%s = %s\n") % (section, k, v))
162 162 return items
163 163
164 164 def walkconfig(self, untrusted=False):
165 165 cfg = self._data(untrusted)
166 166 for section in cfg.sections():
167 167 for name, value in self.configitems(section, untrusted):
168 168 yield section, name, str(value).replace('\n', '\\n')
169 169
170 170 def username(self):
171 171 """Return default username to be used in commits.
172 172
173 173 Searched in this order: $HGUSER, [ui] section of hgrcs, $EMAIL
174 174 and stop searching if one of these is set.
175 175 If not found and ui.askusername is True, ask the user, else use
176 176 ($LOGNAME or $USER or $LNAME or $USERNAME) + "@full.hostname".
177 177 """
178 178 user = os.environ.get("HGUSER")
179 179 if user is None:
180 180 user = self.config("ui", "username")
181 181 if user is None:
182 182 user = os.environ.get("EMAIL")
183 183 if user is None and self.configbool("ui", "askusername"):
184 184 user = self.prompt(_("enter a commit username:"), default=None)
185 185 if user is None and not self.interactive():
186 186 try:
187 187 user = '%s@%s' % (util.getuser(), socket.getfqdn())
188 188 self.warn(_("No username found, using '%s' instead\n") % user)
189 189 except KeyError:
190 190 pass
191 191 if not user:
192 192 raise util.Abort(_('no username supplied (see "hg help config")'))
193 193 if "\n" in user:
194 194 raise util.Abort(_("username %s contains a newline\n") % repr(user))
195 195 return user
196 196
197 197 def shortuser(self, user):
198 198 """Return a short representation of a user name or email address."""
199 199 if not self.verbose: user = util.shortuser(user)
200 200 return user
201 201
202 202 def _path(self, loc):
203 203 p = self.config('paths', loc)
204 204 if p:
205 205 if '%%' in p:
206 206 self.warn("(deprecated '%%' in path %s=%s from %s)\n" %
207 207 (loc, p, self.configsource('paths', loc)))
208 208 p = p.replace('%%', '%')
209 209 p = util.expandpath(p)
210 210 return p
211 211
212 212 def expandpath(self, loc, default=None):
213 213 """Return repository location relative to cwd or from [paths]"""
214 214 if "://" in loc or os.path.isdir(os.path.join(loc, '.hg')):
215 215 return loc
216 216
217 217 path = self._path(loc)
218 218 if not path and default is not None:
219 219 path = self._path(default)
220 220 return path or loc
221 221
222 222 def pushbuffer(self):
223 223 self._buffers.append([])
224 224
225 225 def popbuffer(self):
226 226 return "".join(self._buffers.pop())
227 227
228 228 def write(self, *args):
229 229 if self._buffers:
230 230 self._buffers[-1].extend([str(a) for a in args])
231 231 else:
232 232 for a in args:
233 233 sys.stdout.write(str(a))
234 234
235 235 def write_err(self, *args):
236 236 try:
237 237 if not sys.stdout.closed: sys.stdout.flush()
238 238 for a in args:
239 239 sys.stderr.write(str(a))
240 240 # stderr may be buffered under win32 when redirected to files,
241 241 # including stdout.
242 242 if not sys.stderr.closed: sys.stderr.flush()
243 243 except IOError, inst:
244 244 if inst.errno != errno.EPIPE:
245 245 raise
246 246
247 247 def flush(self):
248 248 try: sys.stdout.flush()
249 249 except: pass
250 250 try: sys.stderr.flush()
251 251 except: pass
252 252
253 253 def interactive(self):
254 254 i = self.configbool("ui", "interactive", None)
255 255 if i is None:
256 try:
256 257 return sys.stdin.isatty()
258 except AttributeError:
259 # some environments replace stdin without implementing isatty
260 # usually those are non-interactive
261 return False
262
257 263 return i
258 264
259 265 def _readline(self, prompt=''):
260 266 if sys.stdin.isatty():
261 267 try:
262 268 # magically add command line editing support, where
263 269 # available
264 270 import readline
265 271 # force demandimport to really load the module
266 272 readline.read_history_file
267 273 # windows sometimes raises something other than ImportError
268 274 except Exception:
269 275 pass
270 276 line = raw_input(prompt)
271 277 # When stdin is in binary mode on Windows, it can cause
272 278 # raw_input() to emit an extra trailing carriage return
273 279 if os.linesep == '\r\n' and line and line[-1] == '\r':
274 280 line = line[:-1]
275 281 return line
276 282
277 283 def prompt(self, msg, default="y"):
278 284 """Prompt user with msg, read response.
279 285 If ui is not interactive, the default is returned.
280 286 """
281 287 if not self.interactive():
282 288 self.write(msg, ' ', default, "\n")
283 289 return default
284 290 try:
285 291 r = self._readline(msg + ' ')
286 292 if not r:
287 293 return default
288 294 return r
289 295 except EOFError:
290 296 raise util.Abort(_('response expected'))
291 297
292 298 def promptchoice(self, msg, choices, default=0):
293 299 """Prompt user with msg, read response, and ensure it matches
294 300 one of the provided choices. The index of the choice is returned.
295 301 choices is a sequence of acceptable responses with the format:
296 302 ('&None', 'E&xec', 'Sym&link') Responses are case insensitive.
297 303 If ui is not interactive, the default is returned.
298 304 """
299 305 resps = [s[s.index('&')+1].lower() for s in choices]
300 306 while True:
301 307 r = self.prompt(msg, resps[default])
302 308 if r.lower() in resps:
303 309 return resps.index(r.lower())
304 310 self.write(_("unrecognized response\n"))
305 311
306 312
307 313 def getpass(self, prompt=None, default=None):
308 314 if not self.interactive(): return default
309 315 try:
310 316 return getpass.getpass(prompt or _('password: '))
311 317 except EOFError:
312 318 raise util.Abort(_('response expected'))
313 319 def status(self, *msg):
314 320 if not self.quiet: self.write(*msg)
315 321 def warn(self, *msg):
316 322 self.write_err(*msg)
317 323 def note(self, *msg):
318 324 if self.verbose: self.write(*msg)
319 325 def debug(self, *msg):
320 326 if self.debugflag: self.write(*msg)
321 327 def edit(self, text, user):
322 328 (fd, name) = tempfile.mkstemp(prefix="hg-editor-", suffix=".txt",
323 329 text=True)
324 330 try:
325 331 f = os.fdopen(fd, "w")
326 332 f.write(text)
327 333 f.close()
328 334
329 335 editor = self.geteditor()
330 336
331 337 util.system("%s \"%s\"" % (editor, name),
332 338 environ={'HGUSER': user},
333 339 onerr=util.Abort, errprefix=_("edit failed"))
334 340
335 341 f = open(name)
336 342 t = f.read()
337 343 f.close()
338 344 finally:
339 345 os.unlink(name)
340 346
341 347 return t
342 348
343 349 def traceback(self, exc=None):
344 350 '''print exception traceback if traceback printing enabled.
345 351 only to call in exception handler. returns true if traceback
346 352 printed.'''
347 353 if self.tracebackflag:
348 354 if exc:
349 355 traceback.print_exception(exc[0], exc[1], exc[2])
350 356 else:
351 357 traceback.print_exc()
352 358 return self.tracebackflag
353 359
354 360 def geteditor(self):
355 361 '''return editor to use'''
356 362 return (os.environ.get("HGEDITOR") or
357 363 self.config("ui", "editor") or
358 364 os.environ.get("VISUAL") or
359 365 os.environ.get("EDITOR", "vi"))
360 366
361 367 def progress(self, topic, pos, item="", unit="", total=None):
362 368 '''show a progress message
363 369
364 370 With stock hg, this is simply a debug message that is hidden
365 371 by default, but with extensions or GUI tools it may be
366 372 visible. 'topic' is the current operation, 'item' is a
367 373 non-numeric marker of the current position (ie the currently
368 374 in-process file), 'pos' is the current numeric position (ie
369 375 revision, bytes, etc.), unit is a corresponding unit label,
370 376 and total is the highest expected pos.
371 377
372 378 Multiple nested topics may be active at a time. All topics
373 379 should be marked closed by setting pos to None at termination.
374 380 '''
375 381
376 382 if pos == None or not self.debugflag:
377 383 return
378 384
379 385 if unit:
380 386 unit = ' ' + unit
381 387 if item:
382 388 item = ' ' + item
383 389
384 390 if total:
385 391 pct = 100.0 * pos / total
386 392 self.debug('%s:%s %s/%s%s (%4.2g%%)\n'
387 393 % (topic, item, pos, total, unit, pct))
388 394 else:
389 395 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