#!/usr/bin/python
#
# ketchup-1.1-beta2
#    http://github.com/psomas/ketchup 
#    git://github.com/psomas/ketchup.git
#
# Orignial code by:
#    Copyright 2004 Matt Mackall <mpm@selenic.com>
#
# Added code for new RT location and maintained until 2011 by:
#    Steven Rostedt <srostedt@redhat.com>
#
# Now maintained by:
#    Stratos Psomadakis <psomas@cslab.ece.ntua.gr>
# 
# Contributors:
#    Baruch Even <baruch@debian.org>
#    Pavel Machek <pavel@ucw.cz>
#    Johann Felix Soden <johfel@gmx.de>
#    Carsten Emde <C.Emde@osadl.org>
#
# This software may be used and distributed according to the terms
# of the GNU General Public License, incorporated herein by reference.
#
# Usage:
#
# in an existing kernel directory, run:
#
#  ketchup <version>
#
# where version is a complete kernel version, or a branch name to grab
# the latest version (see the available trees)
#
# You can override some variables by creating a ~/.ketchuprc file.
# The ~/.ketchuprc is just a Python script, eg. it might look like this:
#
# kernel_url = 'http://kernel.localdomain/pub/linux/kernel'
# archive = os.environ["HOME"] + '/tmp/ketchup-archive'
# gpg = '/weird/path/to/gpg'
#

import re, sys, urllib, os, getopt, glob, shutil, subprocess

def error(*args):
    sys.stderr.write("ketchup: ")
    for a in args:
        sys.stderr.write(str(a))
        sys.stderr.write("\n")

def qprint(*args):
    if not options["quiet"]:
        sys.stdout.write(" ".join(map(str, args)))
        sys.stdout.write("\n")

def lprint(*args):
    sys.stdout.write(" ".join(map(str, args)))
    sys.stdout.write("\n")

def fancyopts(args, options, state, syntax=''):
    long = []
    short = ''
    map = {}
    dt = {}

    def help(state, opt, arg, options = options, syntax = syntax):
        lprint("Usage: ", syntax)

        for s, l, d, c in options:
            opt = ' '
            if s: opt = opt + '-' + s + ' '
            if l: opt = opt + '--' + l + ' '
            if d: opt = opt + '(' + str(d) + ')'
            lprint(opt)
            if c: lprint('   %s' % c)
        sys.exit(0)

    options = [('h', 'help', help, 'Show usage info')] + options

    for s, l, d, c in options:
        map['-'+s] = map['--'+l]=l
        state[l] = d
        dt[l] = type(d)
        if not d is None and not type(d) is type(help): s, l = s + ':', l + '='
        if s: short = short + s
        if l: long.append(l)

    if os.environ.has_key("KETCHUP_OPTS"):
        args = os.environ["KETCHUP_OPTS"].split() + args

    try:
        opts, args = getopt.getopt(args, short, long)
    except getopt.GetoptError:
        help(state, None, args)
        sys.exit(-1)

    for opt, arg in opts:
        if dt[map[opt]] is type(help): state[map[opt]](state,map[opt],arg)
        elif dt[map[opt]] is type(1): state[map[opt]] = int(arg)
        elif dt[map[opt]] is type(''): state[map[opt]] = arg
        elif dt[map[opt]] is type([]): state[map[opt]].append(arg)
        elif dt[map[opt]] is type(None): state[map[opt]] = 1

    return args

# Default values
kernel_url = 'http://www.kernel.org/pub/linux/kernel'
archive = os.environ["HOME"] + "/.ketchup"
rename_prefix = 'linux-'
rename_with_localversion = False
wget = "/usr/bin/wget"
gpg = "/usr/bin/gpg"
precommand = postcommand = None
default_tree = None
local_trees = {}
local_quilt_patchsets = ()
quilt_patchsets = []

minors= {}
lmajor = None

compress_types = {".bz2" : "bzcat", ".gz" : "gzcat", ".xz" : "xzcat", "" : "cat"}

# Functions to parse version strings

def tree(ver):
    # every version must start with x.y, exit otherwise
    t = re.match(r'((\d+)\.\d+)', ver)
    if t == None:
        error("Invalid tree version!")
        sys.exit(-1)

    # linux versions >=3.0 use only the first digit to define the 'tree'
    ret = int(t.group(2)) 
    if ret >= 3:
        return ret

    # 2.x linux trees 
    return float(t.group(1))

def rev(ver):
    p = pre(ver)

    # 3.x trees
    if tree(ver) >= 3:
        r = re.match(r'\d+\.(\d+)', ver)
    # 2.x trees
    else:
        r = re.match(r'\d+\.\d+\.(\d+)', ver)

    # if there's no match, the version is invalid
    try: r = int(r.group(1))
    except: 
        error("Invalid tree version!")
        sys.exit(-1)

    #for -rc versions return the previous stable version
    if p: r = r - 1
    if r == -1:
        t = int(tree(ver)) - 1
        if t == 2:
            t = 2.6
        r = last_minor(t)
        
    return r

def pre(ver):
    # 3.x trees
    if tree(ver) >= 3:
        p = re.match(r'\d+\.\d+(\.\d+)?-(rc\d+)', ver)
    # 2.x trees
    else:
        p = re.match(r'\d+\.\d+\.\d+(\.\d+)?-(rc\d+)', ver)

    try: return p.group(2)
    except: return None

def post(ver):
    # 3.x trees
    if tree(ver) >= 3:
        p = re.match(r'\d+\.\d+\.(\d+)', ver)
    # 2.x trees
    else: 
        p = re.match(r'\d+\.\d+\.\d+\.(\d+)', ver)
    
    try: return p.group(1)
    except: return None

def prenum(ver):
    # 3.x trees
    if tree(ver) >= 3:
        p = re.match(r'\d+\.\d+(\.\d+)?-rc(\d+)', ver)
    # 2.x trees
    else:
        p = re.match(r'\d+\.\d+\.\d+(\.\d+)?-rc(\d+)', ver)
    
    try: return p.group(2)
    except: return None

def prebase(ver):
    # 3.x trees
    if tree(ver) >= 3:
        p = re.match(r'(\d+\.\d+(\.\d+)?(-rc\d+)?)', ver)
    else:
        p = re.match(r'(\d+\.\d+\.\d+(\.\d+)?(-rc\d+)?)', ver)

    try: return p.group(1)
    except: return None

def revbase(ver):
    t = tree(ver)
    r = rev(ver)
    # make sure 3.0-rcs are patched correctly against 2.6.39
    t1 = t - 1
    if t1 == 2:
        t1 = 2.6
    if t1 != 1.6 and r == last_minor(t1):
        t = t1

    return "%s.%s" % (t, r)

def base(ver):
    v = revbase(ver)
    if post(ver): v += "." + post(ver)
    return v

def forkname(ver):
    # 3.x trees
    if tree(ver) >= 3:
        f = re.match(r'\d+\.\d+(\.\d+)?(-(rc|pre)\d+)?(-([a-zA-Z]+)(-)?(\d+)?)?',
            ver)
    # 2.x trees
    else:
        f = re.match(r'\d+\.\d+\.\d+(\.\d+)?(-(rc|pre)\d+)?(-([a-zA-Z]+)(-)?(\d+)?)?',
            ver)
    
    try: return f.group(5)
    except: return None

def forknum(ver):
    # 3.x trees
    if tree(ver) >=3:
        f = re.match(r'\d+\.\d+(\.\d+)?(-(rc|pre)\d+)?(-([a-zA-Z]+)(-)?(\d+)?)?',
            ver)
    #2.x trees
    else:
        f = re.match(r'\d+\.\d+\.\d+(\.\d+)?(-(rc|pre)\d+)?(-([a-zA-Z]+)(-)?(\d+)?)?',
            ver)

    try: return int(f.group(7))
    except: return None

def fork(ver):
    # 3.x trees
    if tree(ver) >= 3:
        f = re.match(r'\d+\.\d+(\.\d+)?(-(rc|pre)\d+)?((\.\d+)?-(\w+(-)?(\d+)?))?',
            ver)
    # 2.x trees
    else:
        f = re.match(r'\d+\.\d+\.\d+(\.\d+)?(-(rc|pre)\d+)?((\.\d+)?-(\w+(-)?(\d+)?))?',
            ver)
    try: return f.group(4)
    except: return None

def get_ver(makefile):
    """ Read the version information from the specified makefile """
    part = {}
    extra = {}
    parts = "VERSION PATCHLEVEL SUBLEVEL".split(' ')
    m = open(makefile)
    for l in m.readlines():
        for p in parts:
            try: part[p] = re.match(r'%s\s*=\s*(\S+)' % p, l).group(1)
            except:            
                # get any other extra versions defined in the makefile (except
                # kernel version)
                v = re.match(r'(\S+VERSION)\s*=\s*(\S+)', l)
                try:
                    if v.group(1) != 'KERNELVERSION':
                        extra[v.group(1)] = v.group(2)
                except:
                    pass
            
    if part['VERSION'] >= '3'and part['SUBLEVEL'] == '0':
        version = "%s.%s" % tuple([part[p] for p in parts[:2]])
    else:
        version = "%s.%s.%s" % tuple([part[p] for p in parts[:3]])

    for v in extra.keys():
        version += extra.get(v, "")
    
    version += get_localversion()

    return version

def get_localversion():
    v = ''

    for name in glob.glob('localversion*'):
        try: v += open(name).readline().strip()
        except: pass

    try:
        c = open('.config').read()
        v += re.search(r'^CONFIG_LOCALVERSION="(.+)"', c, re.M).group(1)
    except: pass

    return v

def compare_ver(a, b):
    """
    Compare kernel versions a and b
    """
    if a == b: return 0

    c = cmp(float(tree(a)), float(tree(b)))
    if c: return c
    ra, rb = tree(revbase(a)), tree(revbase(b))
    c = cmp(ra, rb)
    if c: return c
    ra, rb = rev(a), rev(b)
    c = cmp(ra, rb)
    if c: return c
    c = cmp(int(post(a) or 0), int(post(b) or 0))
    if c: return c
    c = cmp(prenum(a), prenum(b))
    if c: return c
    c = cmp(forkname(a), forkname(b))
    if c: return c
    return cmp(forknum(a), forknum(b))

def last(url, pat="(.*/)"):
    for l in urllib.urlopen(url).readlines():
        m = re.search('(?i)<a href="%s">' % pat, l)
        if m: n = m.group(1)
    return n

def latest_dir_lt(url, pat):
    """Find the latest link to the stable release series that is used local"""
    cwd=os.getcwd()
    lv = None
    if os.path.isdir(options["directory"]):
        os.chdir(options["directory"])
        try:
            lv = get_ver('Makefile')
        except:
            lv = None
    if not lv and os.listdir("."):
        error("Can't find kernel version for non-empty directory")
        sys.exit(-1)
    if not lv:
        qprint("No local version found. Use newest kernel release instead.")
        return latest_dir(url, pat)
    local_revbase = full_tree(lv)
    p = []

    s = {
        'tree': float(tree(local_revbase)),
        'kernel_url': kernel_url,
        'revbase' : revbase(lv)
    }
    
    url = url % s
    for l in urllib.urlopen(url).readlines():
        m = re.search('"%s"' % pat, l)
        if m and revbase(m.group(1))==local_revbase:
            p.append(m.group(1))

    if not p: return None

    p.sort(compare_ver)
    return p[-1]

def find_www(url, pat, grp):
    """Parse the page the url is pointing to and fetch the matched items"""
    p = []
    
    for l in urllib.urlopen(url).readlines():
        m = re.search('"%s"' % pat, l)
        if m: p.append(m.group(grp))

    return p

def latest_dir(url, pat):
    """Find the latest link matching pat at url after sorting"""
    s = {
        'tree': latest_major(),
        'kernel_url': kernel_url,
        'full_tree': latest_major()
    }
    
    p = find_www(url % s, pat, 1)
    if not p: return None

    p.sort(compare_ver)
    return p[-1]

def find_majors(): 
    return find_www(kernel_url, '.*v(\d+\.\d+).*', 1)
   
def latest_major():
    global lmajor
    if lmajor == None:
        p = find_majors()

        p.sort(compare_ver)
        lmajor = p[-1]

    return lmajor

def last_minor(t):
    maj = t
    if t >= 3:
        maj += .0

    if maj not in minors:
        url = kernel_url + ("/v%s" % maj)

        p = map(int, find_www(url, ".*linux-%s\.(\d+)\.tar\.bz2" % t, 1))
        p.sort()

        if p:
            minors[maj] = int(p[-1])
        else:
            minors[maj] = None

    return minors[maj]

def find_info(ver):
    b = "linux%s"

    if ver == None:
        error("Cannot find latest version!")
        sys.exit(-1)

    t = tree(ver)
    if t == 2.4:
       s = b % '-2.4'
    else:
        f = forkname(ver)
        p = pre(ver)

        if f:
            fd = '-' + f
            s = b % fd
        elif p:
            s = b % '-rc'
        else:
            s = b % ''

        if s not in version_info:
            error("Unknown tree/patchset!")
            sys.exit(-1)

        v = version_info[s]

        if f in quilt_patchsets:
            if ver != v[0](os.path.dirname(v[1])):
                error("%s can only be applied to specific kernel versions" % s)
                sys.exit(-1)
            
    return version_info[s]

def full_tree(ver):
    if tree(ver) >= 3:
        t = re.match(r'(\d+\.\d+)', ver)
    else:
        t = re.match(r'(\d+\.\d+\.\d+)', ver)

    if t == None:
        error("Invalid tree version!")
    return t.group(0)

    
def version_urls(ver):
    """ Return the URL for the patch associated with the specified version """
    i = find_info(ver)[1]
    if type(i) != type([]):
        i = [i]

    v = {
        'full': ver,
        'tree': tree(ver) + .0,
        'base': base(ver),
        'prebase': prebase(ver),
        'fork': fork(ver),
        'revbase': revbase(ver),
        'kernel_url': kernel_url,
        'full_tree': full_tree(ver),
        }

    l = []
    for e in i:
        l.append(e % v)
    
    return l

def patch_path(ver):
    patch_name = os.path.basename(version_urls(ver)[0])
    if re.search(r'\d+\.\d+', patch_name) == None:
        suf = re.search(r'\.tar\.(gz|(bz(\d+)?))', patch_name).group(0)
        patch_name = re.sub(r'(\.tar\.(gz|(bz(\d+)?)))', "-%s%s" % (ver, suf), patch_name)
    
    return os.path.join(archive, patch_name)

def download(url, f):
    qprint("Downloading %s" % os.path.basename(url))
    if options["dry-run"]:
        return 1

    if not options["wget"]:
        p = urllib.urlopen(url).read()
        if p.find("<title>404") != -1:
            return None
        open(f, 'w').write(p)
    else:
        e = os.system("%s -c -O %s %s" %
                      (options["wget"], f + ".partial", url))
        if e & 255:
            error("wget terminated by signal.")
            sys.exit(1)
        if e:
            try:
                # remove empty files
                if os.path.getsize(f+".partial")==0:
                    os.unlink(f+".partial")
            except OSError:
                pass
            return None
        os.rename(f + ".partial", f)
    return 1

def check_if_gpg_key_available(sf):
    qprint("Check if GPG key is available...")

    process= subprocess.Popen([options["gpg-path"], "--no-tty", "--batch", "--verify",sf,sf],
                              stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    output, unused_err = process.communicate()
    r = process.poll()
    if r == 2: # key is not available
        qprint(output)
        error("The GPG key seems not to be in the keyring. Please fix this and try again.")
        qprint("In the case potential malicious kernel code is not a problem,\n"
               "you can skip the verifying by using --no-gpg.")
        return 0
    if r < 0: # killed by signal
        return 0
    return 1

def verify(url, f, sign):
    if options["no-gpg"] or options["dry-run"] or not options["gpg-path"]:
        return 1

    m = ""

    sf = f + sign
    if not os.path.isfile(sf) and not download(url + sign, sf):
        try: m = re.search("\.(bz2|xz|gz)$", f).group(0)
        except: m = ""

        f2 = f[:-len(m)]
        sf = f2 + sign
        url = url[:-len(m)]
        if not m or not os.path.isfile(sf) and not download(url + sign, sf):
            error("signature download failed")
            error("removing files...")
            os.unlink(f)
            return 0

    if not check_if_gpg_key_available(sf):
        return -1

    qprint("Verifying signature...")
#    if not m:
#        r = os.system("%s --verify %s %s" % (options["gpg-path"], sf, f))
#    else:
    r = os.system("%s %s | %s --verify %s -" % (compress_types[m], f, options["gpg-path"], sf))
    if r:
        error("gpg returned %d" % r)
        error("removing files...")
        os.unlink(f)
        os.unlink(sf)
        return 0

    return 1

def trydownload(urls, f, sign):
    for url in urls:
        if download(url, f):
            if not sign :
                return f
            result = verify(url, f, sign)
            if result == -1:
                return None
            elif result == 1:
                return f

        try: m = re.search("\.(bz2|xz|gz)$", f).group(0)
        except: m = ""

        for c in compress_types.keys():
            if c == m: continue

            f2 = f[:-len(m)] + c
            url2 = url[:-len(m)] + c
            if download(url2, f2):
                if not sign:
                    return f2
                result = verify(url2, f2, sign)
                if result == -1:
                    return None
                elif result == 1:
                    return f2

    return None

def get_patch(ver):
    """Return the path to patch for given ver, downloading if necessary"""
    f = patch_path(ver)
    if os.path.exists(f):
        return f

    try: m = re.search("\.(bz2|xz|gz)$", f).group(0)
    except: m = ""

    for c in compress_types.keys():
        if c == m: continue

        f2 = f[:-len(m)] + c
        if os.path.exists(f2):
            return f2

    urls = version_urls(ver)
    sign = find_info(ver)[3]
    if sign == 1: sign = ".sign"
    f = trydownload(urls, f, sign)
    if not f:
        error("patch download failed")
        sys.exit(-1)

    return f

def apply_quilt(ver, reverse = 0):
    if reverse:
        err = os.system("quilt pop -a &> .patchdiag")
        if err:
            sys.stderr.write(open(".patchdiag").read())
        os.unlink(".patchdiag")
        os.system("rm -rf patches .pc Next")
    else:
        old = os.getcwd()
        os.mkdir("patches")
        os.chdir("patches")
        err = os.system("zcat %s | tar -xf -" % patch_path(ver))
        if err:
            error("Unpacking failed: ", err)
            os.chdir(old)
            os.system("rm -rf patches")
            sys.exit(-1)
 
        os.system("mv broken-out/*.patch ./")

        os.chdir(old)

        err = os.system("quilt push -a --leave-rejects &> .patchdiag")
        if err:
            sys.stderr.write(open(".patchdiag").read())
        os.unlink(".patchdiag")

    return err

def apply_patch(ver, reverse = 0):
    """Find the patch to upgrade from the predecessor of ver to ver and
    apply or reverse it."""
    p = get_patch(ver)
    r = ""
    if reverse:
        r = " -R"

    qprint("Applying %s%s" % (os.path.basename(p), r))
    if options["dry-run"] or options["only-dl"]:
        return ver

    if forkname(ver) in quilt_patchsets:
        return apply_quilt(ver, reverse)

    def cmd(patch, reverse, dry):
        base = "patch -l -p1%s" % reverse
        if dry:
            base += " --dry-run"

        try: m = re.search("\.(bz2|xz|gz)$", patch).group(0)
        except: m = ""

        pipe = "%s %s | %s" % (compress_types[m], patch, base)

        err = os.system(pipe + " > .patchdiag")
        if err:
            sys.stderr.write(open(".patchdiag").read())
        os.unlink(".patchdiag")
        return err

    err = cmd(p, r, 1)
    if err:
        error("patch %s failed: %d" % (p, err))
        sys.exit(-1)

    err = cmd(p, r, 0)
    if err:
        error("patch %s failed while it was supposed to apply: %d" % (p, err))
        sys.exit(-1)

def untar(tarfile):
    old = os.getcwd()
    if os.path.exists("ketchup-tmp"):
        error("Please remove the ketchup-tmp directory by hand.")
        sys.exit(-1)
    os.mkdir("ketchup-tmp")
    try:
        os.chdir("ketchup-tmp")

        try: m = re.search("\.(bz2|xz|gz)$", tarfile).group(0)
        except: m = ""

        err = os.system("%s %s | tar -xf -" % (compress_types[m], tarfile))
        if err:
            error("Unpacking failed: ", err)
            sys.exit(-1)

        ldir = glob.glob("linux*")[0]
        for f in os.listdir(ldir):
            os.rename(ldir + "/" + f, "../" + f)
    finally:
        os.chdir(old)
        shutil.rmtree("ketchup-tmp")

def install_nearest(ver):
    t = tree(ver)
    if t == 2.4:
        tarballs = glob.glob(archive + "/linux-%s.*.tar.bz2" % t)
    else: 
        tarballs = glob.glob(archive + "/linux-2.6.*.tar.bz2")
        tarballs += glob.glob(archive + "/linux-[3-9].*.tar.bz2")
    
    list = []
    for f in tarballs:
        m = re.match(r'.*/linux-(.*).tar.bz2$', f)
        v = m.group(1)
        dt = tree(v) - tree(ver)
        # if we're 2 'major' trees apart, just get the full tarball
        if(abs(dt) > 1):
            break
        # carefully calc how many 'steps' we're away from the target version
        else:
            if dt == 0:
                d = abs(rev(v) - rev(ver))
            elif dt == 1:
                d = abs(last_minor(tree(ver)) - rev(ver) + rev(v) + 1)
            else:
                d = abs(last_minor(tree(v)) - rev(v) + rev(ver) + 1)

        list.append((d, f, v))

    list.sort()

    if t >= 3:
        t += .0

    if not list or (options["full-tarball"] and list[0][0]):
        f = "linux-%s.tar.bz2" % ver
        url = "%s/v%s/%s" % (kernel_url, t, f)
        url_longterm = "%s/v%s/longterm/v%s/%s" % (kernel_url, t, revbase(ver), f)
        f = archive + "/" + f

        sign = find_info(ver)[3]
        if sign == 1: sign = ".sign"

        f = trydownload([url, url_longterm], f, sign)
        if not f:
            error("Tarball download failed")
            sys.exit(-1)

    else:
        f = list[0][1]
        ver = list[0][2]

    qprint("Unpacking %s" % os.path.basename(f))
    if options["dry-run"] or options["only-dl"]: return ver
    untar(f)

    return ver

def find_ver(ver):
    if ver in version_info.keys():
        v = version_info[ver]

        d = v[1] 
        if type(d) is not type([]):
            d = [ d ]

        for u in d:
            version = v[0](os.path.dirname(u), v[2])
            if version != None:
                return version
        return version
    else:
        return ver

def transform(a, b):
    if a == b:
        qprint("Nothing to do!")
        return
    if not a:
        a = install_nearest(base(b))
    t = tree(a)
    # We can't patch trees <= 2.4 to a newer tree
    if (t <= 2.4 and tree(b) != t) or (tree(b) <= 2.4 and t != tree(b)):
        error("Can't patch %s to %s" % (tree(a), tree(b)))
        sys.exit(-1)
    if fork(a):
        apply_patch(a, 1)
        a = prebase(a)
    if prebase(a) != prebase(b):
        if pre(a):
            apply_patch(a, 1)
            a = base(a)

        if post(a) and (post(a) != post(b) or rev(a) != rev(b)):
            apply_patch(prebase(a), 1)
        
        ra, rb = rev(a), rev(b)
        ta, tb = tree(a), tree(b)
        if tree(revbase(b)) != tb:
            tb = tree(revbase(b))
    
        if ta == tb and ra > rb:
            for r in range(ra, rb, -1):
                apply_patch("%s.%s" % (ta, r), -1)
        if ta == tb and ra < rb:
            for r in range(ra + 1, rb + 1):
                apply_patch("%s.%s" % (tb, r))
        if ta > tb:
            for r in range(ra, -1, -1):
                apply_patch("%s.%s" % (ta, r), -1)

            t = tb

            if t >= 3:
                t += .0
            
            for r in range(last_minor(t), rb, -1):
                apply_patch("%s.%s" % (tb, r), -1)
        if tb > ta:
            t = ta
            if t >= 3:
                t += .0
            for r in range(ra + 1, last_minor(t) + 1):
                apply_patch("%s.%s" % (ta, r))
            if rb != last_minor(t):
                for r in range(0, rb + 1, 1):
                    apply_patch("%s.%s" % (tb, r))

        a = revbase(b)

        if post(b) and post(a) != post(b):
            apply_patch(prebase(b), 0)
            a = base(b)

        if pre(b):
            apply_patch(prebase(b))
            a = prebase(b)

    if fork(b):
        a = apply_patch(b)

def rename_dir(v):
    """Rename the current directory to linux-v, where v is the function arg"""
    if rename_with_localversion:
        v += get_localversion()
    cwd = os.getcwd()
    basedir = os.path.dirname(cwd)
    newdir = os.path.join(basedir, rename_prefix + v)
    if newdir == cwd:
        return
    if os.access(newdir, os.F_OK):
        error("Cannot rename directory, destination exists: %s", newdir);
        return
    os.rename(cwd, newdir)
    qprint('Current directory renamed to %s' % newdir)

# latest lookup function, canonical urls, pattern for lookup function,
# signature flag, description
version_info = {
    'linux-2.4': (latest_dir,
                 "%(kernel_url)s" + "/v2.4" + "/patch-%(base)s.bz2",
                 r'patch-(.*?).bz2',
                 1, "2.4 kernel series"),
    'linux-next': (latest_dir,
                 "%(kernel_url)s" + "/v2.6/next" + "/patch-v%(prebase)s%(fork)s.bz2",
                 r'patch-v(.*?).bz2',
                 1, "linux-next tree"),
    'linux-rc': (latest_dir,
                 [ "%(kernel_url)s" + "/v%(tree)s" + "/testing/patch-%(prebase)s.bz2",
                 "%(kernel_url)s" + "/v%(tree)s" + "/testing/v%(full_tree)s/patch-%(prebase)s.bz2"],
                 r'patch-(.*?).bz2',
                 1, "current stable kernel series prereleases"),
    'linux-lt': (latest_dir_lt,
                 ["%(kernel_url)s" + "/v%(tree)s/longterm/v%(revbase)s/patch-%(prebase)s.bz2",
                 "%(kernel_url)s" + "/v%(tree)s" + "/patch-%(prebase)s.bz2"],
                 r'patch-(.*?).bz2',
                 1, "longterm kernel series - update (only) to newer longterm stable releases"),
    'linux': (latest_dir,
                 ["%(kernel_url)s" + "/v%(tree)s" + "/patch-%(prebase)s.bz2",
                  "%(kernel_url)s" + "/v%(tree)s/longterm/v%(revbase)s/patch-%(prebase)s.bz2"],
                 r'patch-(.*?).bz2',
                 1, "current stable kernel series"),
    }

# Override defaults with /etc/ketchuprc and ~/.ketchuprc which are just Python scripts
rcpath = '/etc/ketchuprc'
if os.path.isfile(rcpath):
    try:
        execfile(rcpath)
    except Exception, e:
        sys.exit('Failed parsing %s\nError was: %s' % (rcpath, e))

rcpath = os.path.expanduser('~/.ketchuprc')
if os.path.isfile(rcpath):
    try:
        execfile(rcpath)
    except Exception, e:
        sys.exit('Failed parsing %s\nError was: %s' % (rcpath, e))

# Add local trees
for k,v in local_trees.items():
    version_info[k] = v

# Add local quilt patchsets
for p in local_quilt_patchsets:
    quilt_patchsets += p

# Environment variables override defaults and ketchuprc
kernel_url = os.environ.get("KETCHUP_URL", kernel_url)
archive = os.environ.get("KETCHUP_ARCH", archive)

# And finally command line overrides everything
if not os.path.exists(wget): wget = ""
if not os.path.exists(gpg): gpg = ""

options = {}
opts = [
    ('a', 'archive', archive, 'cache directory'),
    ('d', 'directory', '.', 'directory to update'),
    ('f', 'full-tarball', None, 'if unpacking a tarball, download the latest'),
    ('g', 'gpg-path', gpg, 'path for GnuPG'),
    ('G', 'no-gpg', None, 'disable GPG signature verification'),
    ('k', 'kernel-url', kernel_url, 'base url for kernel.org mirror'),
    ('l', 'list-trees', None, 'list supported trees'),
    ('m', 'show-makefile', None, 'output version in makefile <arg>'),
    ('n', 'dry-run', None, 'don\'t download or apply patches'),
    ('o', 'only-dl', None, 'don\'t apply patches'),
    ('q', 'quiet', None, 'reduce output'),
    ('r', 'rename-directory', None, 'rename updated directory to %s<v>'
     % rename_prefix),
    ('s', 'show-latest', None, 'output the latest version of <arg>'),
    ('u', 'show-url', None, 'output URL for <arg>'),
    ('w', 'wget', wget, 'command to use for wget'),
    ]

# Process args
def process_args(args):
    archive = options["archive"]
    kernel_url = options["kernel-url"]
    if options["no-gpg"]: options["gpg-path"] = ''

    if not os.path.exists(options["directory"]):
        qprint("Creating target directory", options["directory"])
        os.mkdir(options["directory"])
    os.chdir(options["directory"])

    if options["list-trees"]:
        l = version_info.keys()
        l.sort()
        for tree in l:
            if version_info[tree][3] == 0:
                lprint(tree, "(unsigned)")
            else:
                lprint(tree, "(signed)")
            lprint(" " + version_info[tree][4])
        sys.exit(0)

    if options["show-makefile"] and len(args) < 2:
        if not args:
            lprint(get_ver("Makefile"))
        else:
            lprint(get_ver(args[0]))
        sys.exit(0)

    if len(args) == 0 and default_tree:
        qprint("Using default tree \"%s\"" % (default_tree))
        args.append(default_tree)

    if len(args) != 1:
        error("No version given on command line and no default in configuration")
        sys.exit(-1)

    if options["show-latest"]:
        lprint(find_ver(args[0]))
        sys.exit(0)

    if options["show-url"]:
        for v in version_urls(find_ver(args[0])):
            if urllib.urlopen(v).getcode() != 404:
                lprint(v)
                sys.exit(0)
        error("Cannot find patch for %s" % args[0])
        sys.exit(-1)

    if not os.path.exists(options["archive"]):
        qprint("Creating cache directory", options["archive"])
        os.mkdir(options["archive"])

    if precommand and os.system(precommand):
        sys.exit('Precommand "%s" failed!' % precommand)

    try:
        a = get_ver('Makefile')
    except:
        a = None

    if not a and os.listdir("."):
        error("Can't find kernel version for non-empty directory")
        sys.exit(-1)

    # make sure the patch has a valid url / exists
    for v in version_urls(find_ver(args[0])):
        code = urllib.urlopen(v).getcode()
        if code != 404:
            break
    if code == 404:
        error("Cannot find patch for %s" % args[0])
        sys.exit(-1)

    b = find_ver(args[0])
    qprint("%s -> %s" % (a, b))
    transform(a, b)
    if options["rename-directory"] and not options["dry-run"] and not options["only-dl"] :
        rename_dir(b)

    if postcommand and os.system(postcommand):
        sys.exit('Postcommand "%s" failed!' % postcommand)

if __name__ == "__main__":
    args = fancyopts(sys.argv[1:], opts, options,
                     'ketchup [options] [ver]')
    process_args(args)
