"""
Directory diff utilities; for walking and comparing directories
"""
import sys,os
from stat import *

def error(msg,err):
    " ouputs error message and terminates "
    sys.stdout = sys.stderr
    print "Error:", msg
    sys.exit(err)

def cmpfile(file1,file2,fast=False):
    " tests if files have the same contents "
    s1=os.lstat(file1)
    s2=os.lstat(file2)
    # handle hardlinks
    if s1 == s2:
        return True
    # handle symlinks
    elif S_ISLNK(s1[ST_MODE]):
        return (S_ISLNK(s2[ST_MODE]) and
            os.readlink(file1) == os.readlink(file2))
    elif S_ISLNK(s2[ST_MODE]):
        return False
    # handle directories
    elif S_ISDIR(s1[ST_MODE]):
        return (S_ISDIR(s2[ST_MODE]) and
            set(os.listdir(file1)) == set(os.listdir(file2)))
    elif S_ISDIR(s2[ST_MODE]):
        return False
    # fast compare files mtime and size only
    elif fast:
        return (s1[ST_MTIME] == s2[ST_MTIME] and
            s1[ST_SIZE] == s2[ST_SIZE])
    # slow compare file contents
    f1=open(file1,'rb')
    f2=open(file2,'rb')
    b1=b2=1
    while b1 or b2:
        b1=f1.read(1024)
        b2=f2.read(1024)
        if b1 != b2:
            return False
    return True

def cmplink(file1,file2):
    " tests if files are hardlinked "
    return os.lstat(file1) == os.lstat(file2)

def cmpperm(file1,file2):
    " tests if files have the same permissions "
    s1=os.lstat(file1)
    s2=os.lstat(file2)
    return (s1[ST_MODE] == s2[ST_MODE] and
        s1[ST_UID] == s2[ST_UID] and
        s1[ST_GID] == s2[ST_GID])

def pul(list):
    " generator next() method that returns None when finished "
    try:
        return list.next()
    except StopIteration:
        return None

def isincluded(path,links=True,dirs=True):
    """ Test if path is included as a file, link or dir """
    mode = os.lstat(path)[ST_MODE]
    return (links or not S_ISLNK(mode)) and (dirs or not S_ISDIR(mode))

def isdir(path):
    return (path is not None) and S_ISDIR(os.lstat(path)[ST_MODE])

def islink(path):
    return (path is not None) and S_ISLNK(os.lstat(path)[ST_MODE])

def iterdir(src,links=True,dirs=True):
    """
    A directory list generator that optionaly filters links and dirs.
    """
    srcnames=os.listdir(src)
    srcnames.sort()
    for srcname in srcnames:
        srcpath = os.path.join(src,srcname)
        if isincluded(srcpath,links,dirs):
            yield srcname

def walkdir(top,links=True,dirs=True):
    """ path walking filename generator """
    for name in iterdir(top,links,dirs=True):
        path=os.path.join(top,name)
        if isdir(path):
            if dirs:
                yield name
            for f in walkdir(path,links,dirs):
                yield os.path.join(name,f)
        else:
            yield name

def iterddiff(src,dst,added=False,removed=False,links=True,dirs=True):
    """
    A directory difference generator that returns (srcfile,dstfile)
    tuples for corresponding files. Will optionally produce tuples
    for added or removed files with None for the missing filename.
    """
    srcnames=iterdir(src,links,dirs)
    dstnames=iterdir(dst,links,dirs)
    srcname=pul(srcnames)
    dstname=pul(dstnames)
    while srcname and dstname:
        if srcname < dstname:
            # srcname removed
            if removed:
                srcpath=os.path.join(src,srcname)
                yield srcpath,None
            srcname=pul(srcnames)
        elif srcname > dstname:
            #dstname added
            if added:
                dstpath=os.path.join(dst,dstname)
                yield None,dstpath
            dstname=pul(dstnames)
        else:
            # both exist
            srcpath=os.path.join(src,srcname)
            dstpath=os.path.join(dst,dstname)
            yield srcpath,dstpath
            srcname,dstname=pul(srcnames),pul(dstnames)
    while srcname and removed:
        # remaining srcnames removed
        srcpath=os.path.join(src,srcname)
        yield srcpath,None
        srcname=pul(srcnames)
    while dstname and added:
        # remaining dstnames added
        dstpath=os.path.join(dst,dstname)
        yield None,dstpath
        dstname=pul(dstnames)

def walkddiff(src,dst,added=False,removed=False,links=True,dirs=True):
    """
    A directory comparision generator that walks src and dir,
    producing (srcfile, dstfile) tupples for corresponding files. Will
    optionally produce tupples for added or removed files with None
    for this missing filename.
    """
    for srcpath,dstpath in iterddiff(src,dst,added,removed,links,dirs=True):
        if isdir(srcpath) and isdir(dstpath):
            if dirs:
                yield srcpath,dstpath
            for difwalk in walkddiff(srcpath,dstpath,added,removed,links,dirs):
                yield difwalk
        elif isdir(srcpath):
            if dirs:
                yield srcpath,dstpath
            elif added and dstpath:
                yield None,dstpath
            if removed:
                for srcwalk in walkdir(srcpath,links,dirs):
                    yield os.path.join(srcpath,srcwalk),None
        elif isdir(dstpath):
            if dirs:
                yield srcpath,dstpath
            elif removed and srcpath:
                yield srcpath,None
            if added:
                for dstwalk in walkdir(dstpath,links,dirs):
                    yield None,os.path.join(dstpath,dstwalk)
        else:
            yield srcpath,dstpath
