#!/usr/bin/python # Generate an Atom feed from the commit history of a Git repository. # Adam Sampson # # This aims to conform to Atom 1.0 as defined in RFC 4287: # https://tools.ietf.org/html/rfc4287 import sys, os, xml.dom.minidom, codecs, argparse from offog import atomically_updated from changeparser import get_changes from atomwriter import add_plain, add_link, add_date def add_datetime(doc, node, name, dt): add_date(doc, node, name, dt.utctimetuple()) def write_atom_feed(commits, f, repo_name, repo_url, feed_url=None): doc = xml.dom.minidom.Document() feed = doc.createElement("feed") feed.setAttribute("xmlns", "http://www.w3.org/2005/Atom") add_plain(doc, feed, "title", repo_name) add_link(doc, feed, "link", {"rel": "alternate", "href": repo_url}) # Not having this violates a SHOULD. if feed_url is not None: add_link(doc, feed, "link", {"rel": "self", "href": feed_url}) add_plain(doc, feed, "id", feed_url) add_plain(doc, feed, "generator", "gitatom") # Assumes there's at least one commit -- updated isn't optional. add_datetime(doc, feed, "updated", commits[0]["authortime"]) for commit in commits: entry = doc.createElement("entry") add_plain(doc, entry, "id", repo_url + "#" + commit["id"]) add_datetime(doc, entry, "published", commit["authortime"]) add_datetime(doc, entry, "updated", commit["authortime"]) add_plain(doc, entry, "title", commit["subject"]) # This could be if there was a . add_plain(doc, entry, "content", commit["body"]) author = doc.createElement("author") add_plain(doc, author, "name", commit["authorname"]) add_plain(doc, author, "email", commit["authoremail"]) entry.appendChild(author) feed.appendChild(entry) doc.appendChild(feed) doc.writexml(codecs.getwriter("UTF-8")(f), "", " ", "\n", "UTF-8") doc.unlink() def main(argv): parser = argparse.ArgumentParser(description="Generate an Atom feed from a Git repository.") parser.add_argument("--limit", "-n", metavar="N", default=50, type=int, help="maximum number of commits to include (default: 50)") parser.add_argument("--output", "-o", metavar="FILE", required=True, help="feed output filename") parser.add_argument("--feed-url", metavar="URL", help="public URL for feed (default: none)") parser.add_argument("--repo-name", metavar="NAME", help="name for repository (default: repo basename)") parser.add_argument("--repo-url", metavar="URL", required=True, help="public URL for repository") parser.add_argument("repo_path", metavar="REPO-PATH", help="local path to repository") args = parser.parse_args(argv) repo_path = args.repo_path while repo_path.endswith("/"): repo_path = repo_path[:-1] repo_url = args.repo_url while repo_url.endswith("/"): repo_url = repo_url[:-1] repo_name = args.repo_name if repo_name is None: repo_name = os.path.basename(repo_path) if repo_name.endswith(".git"): repo_name = repo_name[:-4] commits = get_changes(repo_path, args.limit) with atomically_updated(args.output, "wb") as f: write_atom_feed(commits, f, repo_name, repo_url, args.feed_url) if __name__ == "__main__": main(sys.argv[1:])