]> arthur.barton.de Git - bup.git/blob - lib/bup/helpers.py
Add --human-readable option to "bup web".
[bup.git] / lib / bup / helpers.py
1 """Helper functions and classes for bup."""
2
3 import sys, os, pwd, subprocess, errno, socket, select, mmap, stat, re, struct
4 import hashlib, heapq, operator, time, grp
5 from bup import _version, _helpers
6 import bup._helpers as _helpers
7 import math
8
9 # This function should really be in helpers, not in bup.options.  But we
10 # want options.py to be standalone so people can include it in other projects.
11 from bup.options import _tty_width
12 tty_width = _tty_width
13
14
15 def atoi(s):
16     """Convert the string 's' to an integer. Return 0 if s is not a number."""
17     try:
18         return int(s or '0')
19     except ValueError:
20         return 0
21
22
23 def atof(s):
24     """Convert the string 's' to a float. Return 0 if s is not a number."""
25     try:
26         return float(s or '0')
27     except ValueError:
28         return 0
29
30
31 buglvl = atoi(os.environ.get('BUP_DEBUG', 0))
32
33
34 # Write (blockingly) to sockets that may or may not be in blocking mode.
35 # We need this because our stderr is sometimes eaten by subprocesses
36 # (probably ssh) that sometimes make it nonblocking, if only temporarily,
37 # leading to race conditions.  Ick.  We'll do it the hard way.
38 def _hard_write(fd, buf):
39     while buf:
40         (r,w,x) = select.select([], [fd], [], None)
41         if not w:
42             raise IOError('select(fd) returned without being writable')
43         try:
44             sz = os.write(fd, buf)
45         except OSError, e:
46             if e.errno != errno.EAGAIN:
47                 raise
48         assert(sz >= 0)
49         buf = buf[sz:]
50
51
52 _last_prog = 0
53 def log(s):
54     """Print a log message to stderr."""
55     global _last_prog
56     sys.stdout.flush()
57     _hard_write(sys.stderr.fileno(), s)
58     _last_prog = 0
59
60
61 def debug1(s):
62     if buglvl >= 1:
63         log(s)
64
65
66 def debug2(s):
67     if buglvl >= 2:
68         log(s)
69
70
71 istty1 = os.isatty(1) or (atoi(os.environ.get('BUP_FORCE_TTY')) & 1)
72 istty2 = os.isatty(2) or (atoi(os.environ.get('BUP_FORCE_TTY')) & 2)
73 _last_progress = ''
74 def progress(s):
75     """Calls log() if stderr is a TTY.  Does nothing otherwise."""
76     global _last_progress
77     if istty2:
78         log(s)
79         _last_progress = s
80
81
82 def qprogress(s):
83     """Calls progress() only if we haven't printed progress in a while.
84     
85     This avoids overloading the stderr buffer with excess junk.
86     """
87     global _last_prog
88     now = time.time()
89     if now - _last_prog > 0.1:
90         progress(s)
91         _last_prog = now
92
93
94 def reprogress():
95     """Calls progress() to redisplay the most recent progress message.
96
97     Useful after you've printed some other message that wipes out the
98     progress line.
99     """
100     if _last_progress and _last_progress.endswith('\r'):
101         progress(_last_progress)
102
103
104 def mkdirp(d, mode=None):
105     """Recursively create directories on path 'd'.
106
107     Unlike os.makedirs(), it doesn't raise an exception if the last element of
108     the path already exists.
109     """
110     try:
111         if mode:
112             os.makedirs(d, mode)
113         else:
114             os.makedirs(d)
115     except OSError, e:
116         if e.errno == errno.EEXIST:
117             pass
118         else:
119             raise
120
121
122 def next(it):
123     """Get the next item from an iterator, None if we reached the end."""
124     try:
125         return it.next()
126     except StopIteration:
127         return None
128
129
130 def merge_iter(iters, pfreq, pfunc, pfinal, key=None):
131     if key:
132         samekey = lambda e, pe: getattr(e, key) == getattr(pe, key, None)
133     else:
134         samekey = operator.eq
135     count = 0
136     total = sum(len(it) for it in iters)
137     iters = (iter(it) for it in iters)
138     heap = ((next(it),it) for it in iters)
139     heap = [(e,it) for e,it in heap if e]
140
141     heapq.heapify(heap)
142     pe = None
143     while heap:
144         if not count % pfreq:
145             pfunc(count, total)
146         e, it = heap[0]
147         if not samekey(e, pe):
148             pe = e
149             yield e
150         count += 1
151         try:
152             e = it.next() # Don't use next() function, it's too expensive
153         except StopIteration:
154             heapq.heappop(heap) # remove current
155         else:
156             heapq.heapreplace(heap, (e, it)) # shift current to new location
157     pfinal(count, total)
158
159
160 def unlink(f):
161     """Delete a file at path 'f' if it currently exists.
162
163     Unlike os.unlink(), does not throw an exception if the file didn't already
164     exist.
165     """
166     try:
167         os.unlink(f)
168     except OSError, e:
169         if e.errno == errno.ENOENT:
170             pass  # it doesn't exist, that's what you asked for
171
172
173 def readpipe(argv):
174     """Run a subprocess and return its output."""
175     p = subprocess.Popen(argv, stdout=subprocess.PIPE)
176     r = p.stdout.read()
177     p.wait()
178     return r
179
180
181 def realpath(p):
182     """Get the absolute path of a file.
183
184     Behaves like os.path.realpath, but doesn't follow a symlink for the last
185     element. (ie. if 'p' itself is a symlink, this one won't follow it, but it
186     will follow symlinks in p's directory)
187     """
188     try:
189         st = os.lstat(p)
190     except OSError:
191         st = None
192     if st and stat.S_ISLNK(st.st_mode):
193         (dir, name) = os.path.split(p)
194         dir = os.path.realpath(dir)
195         out = os.path.join(dir, name)
196     else:
197         out = os.path.realpath(p)
198     #log('realpathing:%r,%r\n' % (p, out))
199     return out
200
201
202 def detect_fakeroot():
203     "Return True if we appear to be running under fakeroot."
204     return os.getenv("FAKEROOTKEY") != None
205
206
207 def is_superuser():
208     if sys.platform.startswith('cygwin'):
209         import ctypes
210         return ctypes.cdll.shell32.IsUserAnAdmin()
211     else:
212         return os.geteuid() == 0
213
214
215 def _cache_key_value(get_value, key, cache):
216     """Return (value, was_cached).  If there is a value in the cache
217     for key, use that, otherwise, call get_value(key) which should
218     throw a KeyError if there is no value -- in which case the cached
219     and returned value will be None.
220     """
221     try: # Do we already have it (or know there wasn't one)?
222         value = cache[key]
223         return value, True
224     except KeyError:
225         pass
226     value = None
227     try:
228         cache[key] = value = get_value(key)
229     except KeyError:
230         cache[key] = None
231     return value, False
232
233
234 _uid_to_pwd_cache = {}
235 _name_to_pwd_cache = {}
236
237 def pwd_from_uid(uid):
238     """Return password database entry for uid (may be a cached value).
239     Return None if no entry is found.
240     """
241     global _uid_to_pwd_cache, _name_to_pwd_cache
242     entry, cached = _cache_key_value(pwd.getpwuid, uid, _uid_to_pwd_cache)
243     if entry and not cached:
244         _name_to_pwd_cache[entry.pw_name] = entry
245     return entry
246
247
248 def pwd_from_name(name):
249     """Return password database entry for name (may be a cached value).
250     Return None if no entry is found.
251     """
252     global _uid_to_pwd_cache, _name_to_pwd_cache
253     entry, cached = _cache_key_value(pwd.getpwnam, name, _name_to_pwd_cache)
254     if entry and not cached:
255         _uid_to_pwd_cache[entry.pw_uid] = entry
256     return entry
257
258
259 _gid_to_grp_cache = {}
260 _name_to_grp_cache = {}
261
262 def grp_from_gid(gid):
263     """Return password database entry for gid (may be a cached value).
264     Return None if no entry is found.
265     """
266     global _gid_to_grp_cache, _name_to_grp_cache
267     entry, cached = _cache_key_value(grp.getgrgid, gid, _gid_to_grp_cache)
268     if entry and not cached:
269         _name_to_grp_cache[entry.gr_name] = entry
270     return entry
271
272
273 def grp_from_name(name):
274     """Return password database entry for name (may be a cached value).
275     Return None if no entry is found.
276     """
277     global _gid_to_grp_cache, _name_to_grp_cache
278     entry, cached = _cache_key_value(grp.getgrnam, name, _name_to_grp_cache)
279     if entry and not cached:
280         _gid_to_grp_cache[entry.gr_gid] = entry
281     return entry
282
283
284 _username = None
285 def username():
286     """Get the user's login name."""
287     global _username
288     if not _username:
289         uid = os.getuid()
290         _username = pwd_from_uid(uid)[0] or 'user%d' % uid
291     return _username
292
293
294 _userfullname = None
295 def userfullname():
296     """Get the user's full name."""
297     global _userfullname
298     if not _userfullname:
299         uid = os.getuid()
300         entry = pwd_from_uid(uid)
301         if entry:
302             _userfullname = entry[4].split(',')[0] or entry[0]
303         if not _userfullname:
304             _userfullname = 'user%d' % uid
305     return _userfullname
306
307
308 _hostname = None
309 def hostname():
310     """Get the FQDN of this machine."""
311     global _hostname
312     if not _hostname:
313         _hostname = socket.getfqdn()
314     return _hostname
315
316
317 _resource_path = None
318 def resource_path(subdir=''):
319     global _resource_path
320     if not _resource_path:
321         _resource_path = os.environ.get('BUP_RESOURCE_PATH') or '.'
322     return os.path.join(_resource_path, subdir)
323
324 def format_filesize(size):
325     unit = 1024.0
326     size = float(size)
327     if size < unit:
328         return "%d" % (size)
329     exponent = int(math.log(size) / math.log(unit))
330     size_prefix = "KMGTPE"[exponent - 1]
331     return "%.1f%s" % (size / math.pow(unit, exponent), size_prefix)
332
333
334 class NotOk(Exception):
335     pass
336
337
338 class BaseConn:
339     def __init__(self, outp):
340         self.outp = outp
341
342     def close(self):
343         while self._read(65536): pass
344
345     def read(self, size):
346         """Read 'size' bytes from input stream."""
347         self.outp.flush()
348         return self._read(size)
349
350     def readline(self):
351         """Read from input stream until a newline is found."""
352         self.outp.flush()
353         return self._readline()
354
355     def write(self, data):
356         """Write 'data' to output stream."""
357         #log('%d writing: %d bytes\n' % (os.getpid(), len(data)))
358         self.outp.write(data)
359
360     def has_input(self):
361         """Return true if input stream is readable."""
362         raise NotImplemented("Subclasses must implement has_input")
363
364     def ok(self):
365         """Indicate end of output from last sent command."""
366         self.write('\nok\n')
367
368     def error(self, s):
369         """Indicate server error to the client."""
370         s = re.sub(r'\s+', ' ', str(s))
371         self.write('\nerror %s\n' % s)
372
373     def _check_ok(self, onempty):
374         self.outp.flush()
375         rl = ''
376         for rl in linereader(self):
377             #log('%d got line: %r\n' % (os.getpid(), rl))
378             if not rl:  # empty line
379                 continue
380             elif rl == 'ok':
381                 return None
382             elif rl.startswith('error '):
383                 #log('client: error: %s\n' % rl[6:])
384                 return NotOk(rl[6:])
385             else:
386                 onempty(rl)
387         raise Exception('server exited unexpectedly; see errors above')
388
389     def drain_and_check_ok(self):
390         """Remove all data for the current command from input stream."""
391         def onempty(rl):
392             pass
393         return self._check_ok(onempty)
394
395     def check_ok(self):
396         """Verify that server action completed successfully."""
397         def onempty(rl):
398             raise Exception('expected "ok", got %r' % rl)
399         return self._check_ok(onempty)
400
401
402 class Conn(BaseConn):
403     def __init__(self, inp, outp):
404         BaseConn.__init__(self, outp)
405         self.inp = inp
406
407     def _read(self, size):
408         return self.inp.read(size)
409
410     def _readline(self):
411         return self.inp.readline()
412
413     def has_input(self):
414         [rl, wl, xl] = select.select([self.inp.fileno()], [], [], 0)
415         if rl:
416             assert(rl[0] == self.inp.fileno())
417             return True
418         else:
419             return None
420
421
422 def checked_reader(fd, n):
423     while n > 0:
424         rl, _, _ = select.select([fd], [], [])
425         assert(rl[0] == fd)
426         buf = os.read(fd, n)
427         if not buf: raise Exception("Unexpected EOF reading %d more bytes" % n)
428         yield buf
429         n -= len(buf)
430
431
432 MAX_PACKET = 128 * 1024
433 def mux(p, outfd, outr, errr):
434     try:
435         fds = [outr, errr]
436         while p.poll() is None:
437             rl, _, _ = select.select(fds, [], [])
438             for fd in rl:
439                 if fd == outr:
440                     buf = os.read(outr, MAX_PACKET)
441                     if not buf: break
442                     os.write(outfd, struct.pack('!IB', len(buf), 1) + buf)
443                 elif fd == errr:
444                     buf = os.read(errr, 1024)
445                     if not buf: break
446                     os.write(outfd, struct.pack('!IB', len(buf), 2) + buf)
447     finally:
448         os.write(outfd, struct.pack('!IB', 0, 3))
449
450
451 class DemuxConn(BaseConn):
452     """A helper class for bup's client-server protocol."""
453     def __init__(self, infd, outp):
454         BaseConn.__init__(self, outp)
455         # Anything that comes through before the sync string was not
456         # multiplexed and can be assumed to be debug/log before mux init.
457         tail = ''
458         while tail != 'BUPMUX':
459             b = os.read(infd, (len(tail) < 6) and (6-len(tail)) or 1)
460             if not b:
461                 raise IOError('demux: unexpected EOF during initialization')
462             tail += b
463             sys.stderr.write(tail[:-6])  # pre-mux log messages
464             tail = tail[-6:]
465         self.infd = infd
466         self.reader = None
467         self.buf = None
468         self.closed = False
469
470     def write(self, data):
471         self._load_buf(0)
472         BaseConn.write(self, data)
473
474     def _next_packet(self, timeout):
475         if self.closed: return False
476         rl, wl, xl = select.select([self.infd], [], [], timeout)
477         if not rl: return False
478         assert(rl[0] == self.infd)
479         ns = ''.join(checked_reader(self.infd, 5))
480         n, fdw = struct.unpack('!IB', ns)
481         assert(n <= MAX_PACKET)
482         if fdw == 1:
483             self.reader = checked_reader(self.infd, n)
484         elif fdw == 2:
485             for buf in checked_reader(self.infd, n):
486                 sys.stderr.write(buf)
487         elif fdw == 3:
488             self.closed = True
489             debug2("DemuxConn: marked closed\n")
490         return True
491
492     def _load_buf(self, timeout):
493         if self.buf is not None:
494             return True
495         while not self.closed:
496             while not self.reader:
497                 if not self._next_packet(timeout):
498                     return False
499             try:
500                 self.buf = self.reader.next()
501                 return True
502             except StopIteration:
503                 self.reader = None
504         return False
505
506     def _read_parts(self, ix_fn):
507         while self._load_buf(None):
508             assert(self.buf is not None)
509             i = ix_fn(self.buf)
510             if i is None or i == len(self.buf):
511                 yv = self.buf
512                 self.buf = None
513             else:
514                 yv = self.buf[:i]
515                 self.buf = self.buf[i:]
516             yield yv
517             if i is not None:
518                 break
519
520     def _readline(self):
521         def find_eol(buf):
522             try:
523                 return buf.index('\n')+1
524             except ValueError:
525                 return None
526         return ''.join(self._read_parts(find_eol))
527
528     def _read(self, size):
529         csize = [size]
530         def until_size(buf): # Closes on csize
531             if len(buf) < csize[0]:
532                 csize[0] -= len(buf)
533                 return None
534             else:
535                 return csize[0]
536         return ''.join(self._read_parts(until_size))
537
538     def has_input(self):
539         return self._load_buf(0)
540
541
542 def linereader(f):
543     """Generate a list of input lines from 'f' without terminating newlines."""
544     while 1:
545         line = f.readline()
546         if not line:
547             break
548         yield line[:-1]
549
550
551 def chunkyreader(f, count = None):
552     """Generate a list of chunks of data read from 'f'.
553
554     If count is None, read until EOF is reached.
555
556     If count is a positive integer, read 'count' bytes from 'f'. If EOF is
557     reached while reading, raise IOError.
558     """
559     if count != None:
560         while count > 0:
561             b = f.read(min(count, 65536))
562             if not b:
563                 raise IOError('EOF with %d bytes remaining' % count)
564             yield b
565             count -= len(b)
566     else:
567         while 1:
568             b = f.read(65536)
569             if not b: break
570             yield b
571
572
573 def slashappend(s):
574     """Append "/" to 's' if it doesn't aleady end in "/"."""
575     if s and not s.endswith('/'):
576         return s + '/'
577     else:
578         return s
579
580
581 def _mmap_do(f, sz, flags, prot, close):
582     if not sz:
583         st = os.fstat(f.fileno())
584         sz = st.st_size
585     if not sz:
586         # trying to open a zero-length map gives an error, but an empty
587         # string has all the same behaviour of a zero-length map, ie. it has
588         # no elements :)
589         return ''
590     map = mmap.mmap(f.fileno(), sz, flags, prot)
591     if close:
592         f.close()  # map will persist beyond file close
593     return map
594
595
596 def mmap_read(f, sz = 0, close=True):
597     """Create a read-only memory mapped region on file 'f'.
598     If sz is 0, the region will cover the entire file.
599     """
600     return _mmap_do(f, sz, mmap.MAP_PRIVATE, mmap.PROT_READ, close)
601
602
603 def mmap_readwrite(f, sz = 0, close=True):
604     """Create a read-write memory mapped region on file 'f'.
605     If sz is 0, the region will cover the entire file.
606     """
607     return _mmap_do(f, sz, mmap.MAP_SHARED, mmap.PROT_READ|mmap.PROT_WRITE,
608                     close)
609
610
611 def mmap_readwrite_private(f, sz = 0, close=True):
612     """Create a read-write memory mapped region on file 'f'.
613     If sz is 0, the region will cover the entire file.
614     The map is private, which means the changes are never flushed back to the
615     file.
616     """
617     return _mmap_do(f, sz, mmap.MAP_PRIVATE, mmap.PROT_READ|mmap.PROT_WRITE,
618                     close)
619
620
621 def parse_num(s):
622     """Parse data size information into a float number.
623
624     Here are some examples of conversions:
625         199.2k means 203981 bytes
626         1GB means 1073741824 bytes
627         2.1 tb means 2199023255552 bytes
628     """
629     g = re.match(r'([-+\d.e]+)\s*(\w*)', str(s))
630     if not g:
631         raise ValueError("can't parse %r as a number" % s)
632     (val, unit) = g.groups()
633     num = float(val)
634     unit = unit.lower()
635     if unit in ['t', 'tb']:
636         mult = 1024*1024*1024*1024
637     elif unit in ['g', 'gb']:
638         mult = 1024*1024*1024
639     elif unit in ['m', 'mb']:
640         mult = 1024*1024
641     elif unit in ['k', 'kb']:
642         mult = 1024
643     elif unit in ['', 'b']:
644         mult = 1
645     else:
646         raise ValueError("invalid unit %r in number %r" % (unit, s))
647     return int(num*mult)
648
649
650 def count(l):
651     """Count the number of elements in an iterator. (consumes the iterator)"""
652     return reduce(lambda x,y: x+1, l)
653
654
655 saved_errors = []
656 def add_error(e):
657     """Append an error message to the list of saved errors.
658
659     Once processing is able to stop and output the errors, the saved errors are
660     accessible in the module variable helpers.saved_errors.
661     """
662     saved_errors.append(e)
663     log('%-70s\n' % e)
664
665
666 def clear_errors():
667     global saved_errors
668     saved_errors = []
669
670
671 def handle_ctrl_c():
672     """Replace the default exception handler for KeyboardInterrupt (Ctrl-C).
673
674     The new exception handler will make sure that bup will exit without an ugly
675     stacktrace when Ctrl-C is hit.
676     """
677     oldhook = sys.excepthook
678     def newhook(exctype, value, traceback):
679         if exctype == KeyboardInterrupt:
680             log('\nInterrupted.\n')
681         else:
682             return oldhook(exctype, value, traceback)
683     sys.excepthook = newhook
684
685
686 def columnate(l, prefix):
687     """Format elements of 'l' in columns with 'prefix' leading each line.
688
689     The number of columns is determined automatically based on the string
690     lengths.
691     """
692     if not l:
693         return ""
694     l = l[:]
695     clen = max(len(s) for s in l)
696     ncols = (tty_width() - len(prefix)) / (clen + 2)
697     if ncols <= 1:
698         ncols = 1
699         clen = 0
700     cols = []
701     while len(l) % ncols:
702         l.append('')
703     rows = len(l)/ncols
704     for s in range(0, len(l), rows):
705         cols.append(l[s:s+rows])
706     out = ''
707     for row in zip(*cols):
708         out += prefix + ''.join(('%-*s' % (clen+2, s)) for s in row) + '\n'
709     return out
710
711
712 def parse_date_or_fatal(str, fatal):
713     """Parses the given date or calls Option.fatal().
714     For now we expect a string that contains a float."""
715     try:
716         date = atof(str)
717     except ValueError, e:
718         raise fatal('invalid date format (should be a float): %r' % e)
719     else:
720         return date
721
722
723 def parse_excludes(options, fatal):
724     """Traverse the options and extract all excludes, or call Option.fatal()."""
725     excluded_paths = []
726
727     for flag in options:
728         (option, parameter) = flag
729         if option == '--exclude':
730             excluded_paths.append(realpath(parameter))
731         elif option == '--exclude-from':
732             try:
733                 f = open(realpath(parameter))
734             except IOError, e:
735                 raise fatal("couldn't read %s" % parameter)
736             for exclude_path in f.readlines():
737                 excluded_paths.append(realpath(exclude_path.strip()))
738     return excluded_paths
739
740
741 def parse_rx_excludes(options, fatal):
742     """Traverse the options and extract all rx excludes, or call
743     Option.fatal()."""
744     rxs = [v for f, v in options if f == '--exclude-rx']
745     for i in range(len(rxs)):
746         try:
747             rxs[i] = re.compile(rxs[i])
748         except re.error, ex:
749             o.fatal('invalid --exclude-rx pattern (%s):' % (ex, rxs[i]))
750     return rxs
751
752
753 def should_rx_exclude_path(path, exclude_rxs):
754     """Return True if path matches a regular expression in exclude_rxs."""
755     for rx in exclude_rxs:
756         if rx.search(path):
757             debug1('Skipping %r: excluded by rx pattern %r.\n'
758                    % (path, rx.pattern))
759             return True
760     return False
761
762
763 # FIXME: Carefully consider the use of functions (os.path.*, etc.)
764 # that resolve against the current filesystem in the strip/graft
765 # functions for example, but elsewhere as well.  I suspect bup's not
766 # always being careful about that.  For some cases, the contents of
767 # the current filesystem should be irrelevant, and consulting it might
768 # produce the wrong result, perhaps via unintended symlink resolution,
769 # for example.
770
771 def path_components(path):
772     """Break path into a list of pairs of the form (name,
773     full_path_to_name).  Path must start with '/'.
774     Example:
775       '/home/foo' -> [('', '/'), ('home', '/home'), ('foo', '/home/foo')]"""
776     if not path.startswith('/'):
777         raise Exception, 'path must start with "/": %s' % path
778     # Since we assume path startswith('/'), we can skip the first element.
779     result = [('', '/')]
780     norm_path = os.path.abspath(path)
781     if norm_path == '/':
782         return result
783     full_path = ''
784     for p in norm_path.split('/')[1:]:
785         full_path += '/' + p
786         result.append((p, full_path))
787     return result
788
789
790 def stripped_path_components(path, strip_prefixes):
791     """Strip any prefix in strip_prefixes from path and return a list
792     of path components where each component is (name,
793     none_or_full_fs_path_to_name).  Assume path startswith('/').
794     See thelpers.py for examples."""
795     normalized_path = os.path.abspath(path)
796     sorted_strip_prefixes = sorted(strip_prefixes, key=len, reverse=True)
797     for bp in sorted_strip_prefixes:
798         normalized_bp = os.path.abspath(bp)
799         if normalized_path.startswith(normalized_bp):
800             prefix = normalized_path[:len(normalized_bp)]
801             result = []
802             for p in normalized_path[len(normalized_bp):].split('/'):
803                 if p: # not root
804                     prefix += '/'
805                 prefix += p
806                 result.append((p, prefix))
807             return result
808     # Nothing to strip.
809     return path_components(path)
810
811
812 def grafted_path_components(graft_points, path):
813     # Create a result that consists of some number of faked graft
814     # directories before the graft point, followed by all of the real
815     # directories from path that are after the graft point.  Arrange
816     # for the directory at the graft point in the result to correspond
817     # to the "orig" directory in --graft orig=new.  See t/thelpers.py
818     # for some examples.
819
820     # Note that given --graft orig=new, orig and new have *nothing* to
821     # do with each other, even if some of their component names
822     # match. i.e. --graft /foo/bar/baz=/foo/bar/bax is semantically
823     # equivalent to --graft /foo/bar/baz=/x/y/z, or even
824     # /foo/bar/baz=/x.
825
826     # FIXME: This can't be the best solution...
827     clean_path = os.path.abspath(path)
828     for graft_point in graft_points:
829         old_prefix, new_prefix = graft_point
830         # Expand prefixes iff not absolute paths.
831         old_prefix = os.path.normpath(old_prefix)
832         new_prefix = os.path.normpath(new_prefix)
833         if clean_path.startswith(old_prefix):
834             escaped_prefix = re.escape(old_prefix)
835             grafted_path = re.sub(r'^' + escaped_prefix, new_prefix, clean_path)
836             # Handle /foo=/ (at least) -- which produces //whatever.
837             grafted_path = '/' + grafted_path.lstrip('/')
838             clean_path_components = path_components(clean_path)
839             # Count the components that were stripped.
840             strip_count = 0 if old_prefix == '/' else old_prefix.count('/')
841             new_prefix_parts = new_prefix.split('/')
842             result_prefix = grafted_path.split('/')[:new_prefix.count('/')]
843             result = [(p, None) for p in result_prefix] \
844                 + clean_path_components[strip_count:]
845             # Now set the graft point name to match the end of new_prefix.
846             graft_point = len(result_prefix)
847             result[graft_point] = \
848                 (new_prefix_parts[-1], clean_path_components[strip_count][1])
849             if new_prefix == '/': # --graft ...=/ is a special case.
850                 return result[1:]
851             return result
852     return path_components(clean_path)
853
854 Sha1 = hashlib.sha1
855
856 def version_date():
857     """Format bup's version date string for output."""
858     return _version.DATE.split(' ')[0]
859
860
861 def version_commit():
862     """Get the commit hash of bup's current version."""
863     return _version.COMMIT
864
865
866 def version_tag():
867     """Format bup's version tag (the official version number).
868
869     When generated from a commit other than one pointed to with a tag, the
870     returned string will be "unknown-" followed by the first seven positions of
871     the commit hash.
872     """
873     names = _version.NAMES.strip()
874     assert(names[0] == '(')
875     assert(names[-1] == ')')
876     names = names[1:-1]
877     l = [n.strip() for n in names.split(',')]
878     for n in l:
879         if n.startswith('tag: bup-'):
880             return n[9:]
881     return 'unknown-%s' % _version.COMMIT[:7]