ActionPythonとDagon

身辺の整理をしていたら、以前作ったものの中断しているプロジェクトがありました。もうこれ以上私が手を加えるつもりはないので、概要とソースの場所だけ説明しておきます。興味のある方はこれらのプログラムを自由にして構いません。

ActionPython

ActionPythonは、ActionScriptで実装したPython, になる予定でした。Flex上でPythonが動くことを目指して作り始めたような気がします(うろ覚えなので、正しい言い方ではないかもしれません)。

プログラムは、パーサの部分までできています。PythonのコードをASTに変換することができます(ただし、with文は除きます)。

ASTをAVMのバイトコードにコンパイルするところからはできていません(要するに肝心なところができていない)。

ソースは、http://bitbucket.org/SumiTomohiko/actionpython/overviewにあります。

Dagon

Dagonは、issues管理システムです。Mercurialリポジトリにこっそり専用のブランチを作成し、そこに置くファイルで管理します。こうすることで、issues管理も分散できるようになることを狙って作られました(Gitにも同じ考えのソフトウェアがあったはずで、Dagonはそれを真似しているのですが、名前は忘れました)。

MercurialPythonで書かれているので、DagonはMercurial内部のクラスを操作します。特に長いコードではないので、以下に掲載します。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
File Structure: File structure must be easy to merge or must avoid conflicts.

repository root (dagon branch)
 `+- dagon
      `+- ticket directory (SHA1 name)
       |   `+- main
       |    |   * title
       |    |   * author
       |    |   * date
       |    |   * state (new, assigned, resolved, closed, reopened, not a bug)
       |    +- SHA1 name
       |    |   * author
       |    |   * date
       |    |   * body (multiline)
       :    :
"""

from hashlib import sha1
from optparse import OptionParser
from os.path import basename, dirname, join
from sys import argv, exit

from mercurial.cmdutil import match
from mercurial.context import memctx, memfilectx
from mercurial.error import LookupError
from mercurial.hg import repository
from mercurial.ui import ui
from mercurial.util import datestr, parsedate

branch_name = "dagon"
dagon_root = "dagon"

def hash(s1, s2):
    m = sha1()
    m.update(s1)
    m.update(s2)
    return m.hexdigest()

class Ticket(object):

    def __init__(self):
        self.id = ""
        self.title = ""
        self.user = ""
        self.date = None
        self.desc = ""
        self.assigned = ""
        self.status = ""

    @classmethod
    def from_data(self, data):
        ticket = Ticket()
        desc = []
        in_desc = False
        for s in data.split("\n"):
            if in_desc:
                if s == ".":
                    ticket.desc = "\n".join(desc).strip()
                    in_desc = False
                else:
                    if s.startswith("."):
                        s = s[1:]
                    desc.append(s)
                continue

            n = s.find(":")
            if n < 0:
                continue
            key = s[:n].strip()
            value = s[n + 1:].strip()
            if key == "title":
                ticket.title = value
            elif key == "user":
                ticket.user = value
            elif key == "date":
                ticket.date = parsedate(value)
            elif key == "description":
                desc = [value]
                in_desc = True
            elif key == "assigned":
                ticket.assigned = value
            elif key == "status":
                ticket.status = value

        return ticket

    def create_key_value(self, key, value):
        return "%(key)s: %(value)s" % locals()

    def description2data(self, desc):
        lines = []
        for line in desc.split("\n"):
            if line.startswith("."):
                s = "." + line
            else:
                s = line
            lines.append(s)
        lines.append(".")
        return "\n".join(lines)

    def to_data(self):
        data = []
        data.append(self.create_key_value("title", self.title))
        data.append(self.create_key_value("user", self.user))
        data.append(self.create_key_value("date", datestr(self.date)))
        data.append(self.create_key_value("assigned", self.assigned))
        data.append(self.create_key_value("status", self.status))
        data.append(self.create_key_value("desc", self.description2data(self.desc)))
        return "\n".join(data)

def assign_ticket_directory(ticket, head):
    if ticket.id != "":
        return ticket.id
    if head is None:
        return hash("", ticket.title)
    return hash(head, ticket.title)

def save_ticket(path, ticket):
    repos = repository(ui(), path=path)
    heads = repos.branchheads(branch_name)
    try:
        head = heads[0]
    except IndexError:
        head = None
    dir = assign_ticket_directory(ticket, head)
    log = "saved ticket %s" % (dir, )
    path = join(dagon_root, dir, "main")

    def filectxfn(repo, memctx, path):
        if ticket.user is None:
            ticket.user = ctx.user()
        if ticket.date is None:
            ticket.date = ctx.date()
        return memfilectx(path, ticket.to_data(), False, False, None)

    ctx = memctx(repos, (head, None), log, [path], filectxfn, extra={ "branch": branch_name })
    repos.commitctx(ctx)

def find_ticket(path, id):
    repos = repository(ui(), path=path)
    heads = repos.branchheads(branch_name)
    try:
        head = heads[0]
    except IndexError:
        return None;
    changeset = repos[head]

    for file in changeset.walk(match(repos)):
        name = basename(file)
        if name != "main":
            continue
        node = basename(dirname(file))
        if not node.startswith(id):
            continue
        data = changeset.filectx(file).data()
        ticket = Ticket.from_data(data)
        ticket.id = node
        return ticket

    return None

def do_list(path, args):
    repos = repository(ui(), path=path)
    heads = repos.branchheads(branch_name)
    try:
        head = heads[0]
    except IndexError:
        return;
    changeset = repos[head]

    tickets = []
    for file in changeset.walk(match(repos)):
        name = basename(file)
        if name != "main":
            continue
        id = basename(dirname(file))
        data = changeset.filectx(file).data()
        ticket = Ticket.from_data(data)
        ticket.id = id
        tickets.append(ticket)
    tickets.sort(key=lambda ticket: - ticket.date[0])
    for ticket in tickets:
        l = "%(id)s %(title)-47s %(status)-8s %(assigned)s" % { "id": ticket.id[:6], "title": ticket.title, "status": ticket.status, "assigned": ticket.assigned }
        print l.strip()

def help_new():
    print "dagon new <title>"

def do_new(path, args):
    parser = OptionParser()
    parser.add_option("-d", "--desc", dest="desc", help="description")
    (options, args) = parser.parse_args(args)
    desc = options.desc or ""

    try:
        title = args[0]
    except IndexError:
        help_new()
        exit(-1)

    ticket = Ticket()
    ticket.title = title
    ticket.desc = desc
    ticket.assigned = ""
    ticket.status = "new"

    save_ticket(path, ticket)

def help_assign():
    print "dagon assign <id> <name>"

def do_assign(path, args):
    try:
        id = args[0]
        name = args[1]
    except IndexError:
        help_assign()
        exit(-1)
    ticket = find_ticket(path, id)
    if ticket is None:
        raise Exception("can't find ticket of %(id)s" % { "id": id })
    ticket.assigned = name
    if ticket.status == "new":
        ticket.status = "assigned"
    save_ticket(path, ticket)

def help_status():
    print "dagon status <id> <status>"

def do_status(path, args):
    try:
        id = args[0]
        status = args[1]
    except IndexError:
        help_status()
        exit(-1)
    ticket = find_ticket(path, id)
    if ticket is None:
        raise Exception("can't find ticket of %(id)s" % { "id": id })
    ticket.status = status
    save_ticket(path, ticket)

def main(args):
    parser = OptionParser()
    parser.add_option("-R", "--repository", dest="path", help="repository path")
    (options, args) = parser.parse_args(args)
    path = options.path or ""
    try:
        cmd = args[0]
    except IndexError:
        parser.print_help()
        exit(-1)

    commands = { "list": do_list, "new": do_new, "status": do_status, "assign": do_assign }
    commands[cmd](path, args[1:])

    """
    def filectxfn(repo, memctx, path):
        print filectxfn
        return memfilectx(path, data + "42", False, False, None)

    repos = repository(ui(), path=path)
    heads = repos.branchheads(branch_name)
    try:
        head = heads[0]
        from mercurial.node import short
        print short(head)
        recent_changeset = repos[head]
        print recent_changeset.files()
        print type(recent_changeset)
        path = "dagon.dat"

        from mercurial.cmdutil import match
        for s in recent_changeset.walk(match(repos)):
            print s

        try:
            data = recent_changeset.filectx(path).data()
        except LookupError:
            print "lookup error"
            data = ""
    except:
        head = None
        data = ""
    # TODO: do command
    ctx = memctx(repos, (head, None), "update", [path], filectxfn, extra={ "branch": branch_name })
    repos.commitctx(ctx)
    """

main(argv[1:])

# vim: tabstop=4 shiftwidth=4 expandtab softtabstop=4 filetype=python

以上を含めたファイルは、http://bitbucket.org/SumiTomohiko/dagonにあります。