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