]> thyrsus.com Git - freecode-submit.git/commitdiff
Deal with the name change to freecode.net.
authorEric S. Raymond <esr@thyrsus.com>
Thu, 3 Nov 2011 10:22:36 +0000 (06:22 -0400)
committerEric S. Raymond <esr@thyrsus.com>
Thu, 3 Nov 2011 10:22:36 +0000 (06:22 -0400)
Makefile
NEWS
README
control
freecode-submit [new file with mode: 0755]
freecode-submit.xml [new file with mode: 0644]
freshmeat-submit [deleted file]
freshmeat-submit.xml [deleted file]

index 4adc57e42c0322bde5408394655289d350aaddc5..bddcfd285293627a2d1a750841b95c2524920e4e 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,44 +1,44 @@
-# Makefile for the freshmeat-submit tool
+# Makefile for the freecode-submit tool
 
-VERS=$(shell sed <freshmeat-submit -n -e '/version="\(.*\)"/s//\1/p')
+VERS=$(shell sed <freecode-submit -n -e '/version="\(.*\)"/s//\1/p')
 
-CODE    = freshmeat-submit
-DOCS    = README AUTHORS freshmeat-submit.xml COPYING
+CODE    = freecode-submit
+DOCS    = README AUTHORS freecode-submit.xml COPYING
 SOURCES = $(CODE) $(DOCS) Makefile control gold-mega.png
 
-all: freshmeat-submit.1
+all: freecode-submit.1
 
-freshmeat-submit.1: freshmeat-submit.xml
-       xmlto man freshmeat-submit.xml
+freecode-submit.1: freecode-submit.xml
+       xmlto man freecode-submit.xml
 
-freshmeat-submit.html: freshmeat-submit.xml
-       xmlto html-nochunks freshmeat-submit.xml
+freecode-submit.html: freecode-submit.xml
+       xmlto html-nochunks freecode-submit.xml
 
-install: freshmeat-submit.1 uninstall
-       cp freshmeat-submit ${ROOT}/usr/bin/freshmeat-submit 
+install: freecode-submit.1 uninstall
+       cp freecode-submit ${ROOT}/usr/bin/freecode-submit 
        install -m 755 -o 0 -g 0 -d $(ROOT)/usr/share/man/man1/
-       install -m 755 -o 0 -g 0 freshmeat-submit.1 $(ROOT)/usr/share/man/man1/freshmeat-submit.1
+       install -m 755 -o 0 -g 0 freecode-submit.1 $(ROOT)/usr/share/man/man1/freecode-submit.1
 
 uninstall:
-       rm -f ${ROOT}/usr/bin/freshmeat-submit 
-       rm -f ${ROOT}/usr/share/man/man1/freshmeat-submit.1
+       rm -f ${ROOT}/usr/bin/freecode-submit 
+       rm -f ${ROOT}/usr/share/man/man1/freecode-submit.1
 
-freshmeat-submit-$(VERS).tar.gz: $(SOURCES) freshmeat-submit.1
-       find $(SOURCES) freshmeat-submit.1 -type f | sed "s:^:freshmeat-submit-$(VERS)/:" >MANIFEST
-       (cd ..; ln -s freshmeat-submit freshmeat-submit-$(VERS))
-       (cd ..; tar -czf freshmeat-submit/freshmeat-submit-$(VERS).tar.gz `cat freshmeat-submit/MANIFEST`)
-       (cd ..; rm freshmeat-submit-$(VERS))
+freecode-submit-$(VERS).tar.gz: $(SOURCES) freecode-submit.1
+       find $(SOURCES) freecode-submit.1 -type f | sed "s:^:freecode-submit-$(VERS)/:" >MANIFEST
+       (cd ..; ln -s freecode-submit freecode-submit-$(VERS))
+       (cd ..; tar -czf freecode-submit/freecode-submit-$(VERS).tar.gz `cat freecode-submit/MANIFEST`)
+       (cd ..; rm freecode-submit-$(VERS))
 
 pychecker:
        @echo "Expect: Object (data) has no attribute (update)"
-       @ln -f freshmeat-submit freshmeat-submit.py
-       @-pychecker --only --limit 50 freshmeat-submit.py
-       @rm -f freshmeat-submit.py
+       @ln -f freecode-submit freecode-submit.py
+       @-pychecker --only --limit 50 freecode-submit.py
+       @rm -f freecode-submit.py
 
 clean:
-       rm -f *.pyc *.html freshmeat-submit.1 MANIFEST ChangeLog SHIPPER.* *~
+       rm -f *.pyc *.html freecode-submit.1 MANIFEST ChangeLog SHIPPER.* *~
 
-dist: freshmeat-submit-$(VERS).tar.gz
+dist: freecode-submit-$(VERS).tar.gz
 
-release: freshmeat-submit-$(VERS).tar.gz freshmeat-submit.html
+release: freecode-submit-$(VERS).tar.gz freecode-submit.html
        shipper -u -m -t; make clean
diff --git a/NEWS b/NEWS
index 2d5d8178e236b204f693111dc2678572f6286bf6..808fae4e059d1e56e453335f7cf3d5e82898c45c 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -1,8 +1,12 @@
-               freshmeat-submit project release news
+               freecode-submit project release news
+
+Thu Nov 03 2011 Eric S. Raymond <esr@snark.thyrsus.com> - 2.3
+  Deal with the Freshmeat to Freecode name change.
+  Means the name of this tool has to change.
 
 Thu Nov 24 2010 Eric S. Raymond <esr@snark.thyrsus.com> - 2.2
   Release withdrawal, previously broken, now works.
-  (This required a Freshmeat API change.)
+  (This required a site API change.)
 
 Thu Oct 21 2010 Eric S. Raymond <esr@snark.thyrsus.com> - 2.1
   Now has a query mode that can retrieve project metadata in editable form.
@@ -11,7 +15,7 @@ Thu Oct 21 2010 Eric S. Raymond <esr@snark.thyrsus.com> - 2.1
   License changed to BSD.
 
 Mon Oct 17 2010 Eric S. Raymond <esr@snark.thyrsus.com> - 2.0
-  Updated to work with the JSON-based 3.0 freshmeat.net API.
+  Updated to work with the JSON-based 3.0 API.
   Major rewrite and simplification of the code.
 
 Mon Aug  2 2004 Eric S. Raymond <esr@snark.thyrsus.com> - 1.6
diff --git a/README b/README
index 0d99ce12ba1d835d9b3977b42d73af2692e7ba59..419d5f47f7e365e14a3bff576be5a8dc7acb99af 100644 (file)
--- a/README
+++ b/README
@@ -1,7 +1,11 @@
-                       freshmeat-submit
+                       freecode-submit
 
-freshmeat-submit is a script that supports remote submitssion of release 
-updates to Freshmeat via its XML-RPC interface.  It is intended for use
+freecode-submit is a script that supports remote submitssion of release 
+updates to Freecode via its XML-RPC interface.  It is intended for use
 in project release scripts.
+
+(This project was formerly called freshmeat-submit.  It changed its name
+in 2011 when Freecode did.)
+
                                                        Eric S, Raymond
                                                        December 2003
diff --git a/control b/control
index 5bf409ddfba80160f6d57a534ced068123f50101..01c17ab67c577d9e11a730126919e9f5a5083bbe 100644 (file)
--- a/control
+++ b/control
@@ -1,18 +1,18 @@
 # This is not a real Debian control file, but the syntax is upward-compatible.
 # It's project metadata for the shipper tool
 
-Package: freshmeat-submit
+Package: freecode-submit
 
-Description: submit release information to freshmeat.net
- freshmeat-submit is a script that supports remote submission of
- release updates to Freshmeat via its JSON API.  It is intended for use
+Description: submit release information to freecode.net
+ freecode-submit is a script that supports remote submission of
+ release updates to Freecode via its JSON API.  It is intended for use
  in project release scripts.  It reads the metadata from an
  RFC-2822-like message on standard input, possibly with overrides by
  command-line switches.
 
-XBS-Destinations: freshmeat
+XBS-Destinations: freecode
 
-Homepage: http://www.catb.org/~esr/freshmeat-submit
+Homepage: http://www.catb.org/~esr/freecode-submit
 
 XBS-Logo: gold-mega.png
 
diff --git a/freecode-submit b/freecode-submit
new file mode 100755 (executable)
index 0000000..f473ed7
--- /dev/null
@@ -0,0 +1,405 @@
+#!/usr/bin/env python
+#
+# freecode-submit -- script transactions with the Freecode server
+
+import sys, re, urllib2, json, netrc, email.Parser, optparse
+
+version="2.3"
+
+class Update:
+    "Encapsulate dictionaries describing a project metadata update."
+    def __init__(self):
+        self.name = None
+        self.per_project = {}
+        self.urlassoc = []
+        self.per_release = {}
+    def __repr__(self):
+        return "Update(" + repr(self.__dict__) + ")"
+
+# The Freecode API implementation is sensitive to the kind of HTTP request
+# method you use.  The general rule is:
+#
+# Reading records:                GET
+# Adding new records:             POST
+# Updating existing records:      PUT
+# Deleting existing records:      DELETE
+#
+# From http://help.freecode.net/faqs/api-7/data-api-intro:
+# 200 OK - Request was successful, the requested content is included
+# 201 Created - Creation of a new resource was successful
+# 401 Unauthorized - You need to provide an authentication code with this
+#     request
+# 403 Forbidden - You don't have permission to access this URI
+# 404 Not Found - The requested resource was not found
+# 409 Conflict - The validation of your submitted data failed, please check
+#     the response body for error pointers
+# 500 Server Error - The request hit a problem and was aborted, please report
+#     as a bug if it persists
+# 503 Service Unavailable - You hit your API credit limit
+
+class RequestWithMethod(urllib2.Request):
+    "Hack for forcing the method in a request - allows PUT and DELETE"
+    def __init__(self, method, *args, **kwargs):
+        # This assignment works directly in older Python versions
+        self._method = method
+        urllib2.Request.__init__(self, *args, **kwargs)
+    def get_method(self):
+        "Deduce the HTTP method to use."
+        # This method works in newer Pythons (2.6.5, possibly earlier).
+        if self._method:
+            return self._method
+        elif self.has_data():
+            return "POST"
+        else:
+            return "GET"
+
+class FreecodeSessionException(Exception):
+    def __init__(self, msg):
+        self.msg = msg
+
+class FreecodeSession:
+    "Encapsulate the state of a Freecode API session."
+    server = "http://freecode.net/"
+
+    def __init__(self, auth=None, verbose=0, emit_enable=True):
+        "Initialize Freecode session credentials."
+        self.auth = auth
+        self.verbose = verbose
+        self.emit_enable = emit_enable
+        self.project = None
+        # If user didn't supply credentials, fetch from ~/.netrc
+        if not self.auth:
+            try:
+                credentials = netrc.netrc()
+            except netrc.NetrcParseError, e:
+                raise FreecodeSessionException("ill-formed .netrc: %s:%s %s" \
+                                               % (e.filename, e.lineno, e.msg))
+            except IOError, e:
+                raise FreecodeSessionException(("missing .netrc file %s" % \
+                                                 str(e).split()[-1]))
+            ret = credentials.authenticators("freecode")
+            if not ret:
+                raise FreecodeSessionException("no credentials for Freecode")
+            login, self.auth, password = ret
+
+    def on_project(self, name):
+        "Select project by Freecode shortname."
+        if self.verbose:
+            print "Selecting project: %s" % name
+        self.project = name
+        query = FreecodeSession.server + "projects/%s.json?auth_code=%s" \
+                % (self.project, self.auth)
+        handle = urllib2.urlopen(query)
+        content = json.loads(handle.read())
+        self.project_data = content['project']
+        #if self.verbose:
+        #    print "Project data: %s" % self.project_data
+        self.permalink = self.project_data['permalink'] 
+        self.id = self.project_data['id'] 
+
+    def edit_request(self, url, method="GET", request={}, force=False):
+        "Wrap a JSON object with the auth code and ship it as a request"
+        url = FreecodeSession.server + url
+        data = {"auth_code" : self.auth}
+        data.update(request)
+        data = json.dumps(data)
+        headers = {"Content-Type" : "application/json"}
+        if self.verbose:
+            print "Request URL:", method, url
+        #if self.verbose:
+        #    print "Request headers:", headers
+        if self.verbose:
+            print "Request data:", data
+        if self.emit_enable or force:
+            req = RequestWithMethod(method=method,
+                                    url=url,
+                                    data=data,
+                                    headers=headers)
+            handle = urllib2.urlopen(req)
+            if self.verbose:
+                print handle.info()
+            content = handle.read()
+            if self.verbose:
+                print "Response:", content
+            return content
+
+    def publish_release(self, data):
+        "Add a new release to the current project."
+        if self.verbose:
+            print "Publishing %s release: %s" % (self.project, `data`)
+        self.edit_request("projects/" + self.permalink + "/releases.json",
+                          "POST",
+                          {"release": data})
+
+    def withdraw_release(self, version):
+        "Withdraw a specified release from the current project."
+        if self.verbose:
+            print "Withdrawing %s release: %s" % (self.project, version)
+        releases = self.edit_request("projects/%s/releases/pending.json" \
+                                     % self.permalink, force=True)
+        releases = json.loads(releases)
+        for release in releases:
+            properties = release["release"]
+            if properties.get("version") == version:
+                vid = properties["id"]
+                print "I see", properties
+                break
+        else:
+            raise FreecodeSessionException("couldn't find release %s"%version) 
+        deletelink = "projects/%s/releases/%s.json" % (self.permalink, vid)
+        self.edit_request(deletelink, "DELETE", {})
+
+    def update_core(self, coredata):
+        "Update the core data for a project."
+        if self.verbose:
+            print "Core data update for %s is: %s" % (self.project, coredata)
+        self.edit_request("projects/" + self.permalink + ".json",
+                          "PUT",
+                          {"project": coredata})
+
+    def update_urls(self, urlassoc):
+        "Update URL list for a project."
+        if self.verbose:
+            print "URL list update for %s is: %s" % (self.project, urlassoc)
+        # First, get permalinks for all existing URLs
+        query = FreecodeSession.server + "projects/%s/urls.json?auth_code=%s" \
+                % (self.permalink, self.auth)
+        handle = urllib2.urlopen(query)
+        content = json.loads(handle.read())
+        permadict = {}
+        for item in content:
+            inner = item['url']
+            permadict[inner['label']] = inner['permalink']
+        # OK, now run through the update list...
+        for (label, url) in urlassoc:
+            if label in permadict:
+                # updating where we need to
+                self.edit_request("projects/%s/urls/%s.json"
+                                  % (self.permalink, permadict[label]),
+                                  "PUT",
+                                  {"url" : {"label": label, "location" : url}})
+            else:
+                # and adding where we don't.
+                self.edit_request("projects/%s/urls.json"
+                                  % (self.permalink,),
+                                  "POST",
+                                  {"url" : {"label": label, "location" : url}})
+        # Delete URLs that weren't in the update list
+        remove = set(permadict.keys()) - set(map(lambda x: x[0], urlassoc))
+        for label in remove:
+            self.edit_request("projects/%s/urls/%s.json?auth_code=%s"
+                                  % (self.permalink, permadict[label], self.auth),
+                                  "DELETE", {})
+class FreecodeMetadataFactory:
+    "Factory class for producing Freecode records in JSON."
+    freecode_field_map = (
+        ("Project",          "P", "name"),                   # Project
+        ("Summary",          "S", "oneliner"),               # Project
+        ("Description",      "D", "description"),            # Project
+        ("License-List",     "L", "license_list"),           # Project
+        ("Project-Tag-List", "T", "project_tag_list"),       # Project
+        ("Version",          "v", "version"),                # Release
+        ("Changes",          "c", "changelog"),              # Release
+        ("Hide",             "x", "hidden_from_frontpage"),  # Release
+        ("Release-Tag-List", "t", "tag_list"),               # Release
+        # Note: Release Focus is no longer a real Freecode field.
+        # We remap it below.
+        ("Release-Focus",    "r", "release_focus"),          # Release
+        )
+    # Conventional values for release focus
+    focus_values = (
+        "Initial freecode announcement",
+        "Documentation",
+        "Code cleanup",
+        "Minor feature enhancements",
+        "Major feature enhancements",
+        "Minor bugfixes",
+        "Major bugfixes",
+        "Minor security fixes",
+        "Major security fixes",
+    )
+    # Which attributes have project scope, all others have release scupe
+    projectwide = ('name',
+                   'description',
+                   'oneliner',
+                   'license_list',
+                   'project_tag_list')
+
+    def __init__(self):
+        self.message_parser = email.Parser.Parser()
+        self.argument_parser = optparse.OptionParser( \
+            usage="usage: %prog [options]")
+        for (msg_field, shortopt, rpc_field) in FreecodeMetadataFactory.freecode_field_map:
+            self.argument_parser.add_option("-" + shortopt,
+                                            "--" + msg_field.lower(),
+                                            dest=rpc_field,
+                                            help="Set the %s field"%msg_field)
+        self.argument_parser.add_option('-q', '--query', dest='query',
+                          help='Query metadata for PROJECT',metavar="PROJECT")
+        self.argument_parser.add_option('-d', '--delete', dest='delete',
+                          default=False, action='store_true',
+                          help='Suppress reading fields from stdin.')
+        self.argument_parser.add_option('-n', '--no-stdin', dest='read',
+                          default=True, action='store_false',
+                          help='Suppress reading fields from stdin.')
+        self.argument_parser.add_option('-N', '--dryrun', dest='dryrun',
+                          default=False, action='store_true',
+                          help='Suppress reading fields from stdin.')
+        self.argument_parser.add_option('-V', '--verbose', dest='verbose',
+                          default=False, action='store_true',
+                          help='Enable verbose debugging.')
+        self.argument_parser.add_option('-?', '--showversion', dest='showversion',
+                          default=False, action='store_true',
+                          help='Show version and quit.')
+        
+    def header_to_field(self, hdr):
+        lhdr = hdr.lower().replace("-", "_")
+        for (alias, shortopt, field) in FreecodeMetadataFactory.freecode_field_map:
+            if lhdr == alias.lower().replace("-", "_").replace("/", "_"):
+                return field
+        raise FreecodeSessionException("Illegal field name %s" % hdr)
+
+    def getMetadata(self, stream):
+        "Return an Update object describing project and release attributes."
+        data = {}
+        urls = {}
+        (options, args) = self.argument_parser.parse_args()
+        # Stuff from stdin if present
+        prior_version = data.get("version")
+        if not (options.query or options.showversion) and options.read:
+            message = self.message_parser.parse(stream)
+            for (key, value) in message.items():
+                value = re.sub("\n +", " ", value).strip()
+                if key.endswith("-URL"):
+                    key = key.replace("-", " ")
+                    urls.update({key[:-4] : value})
+                else:
+                    if key.endswith("List"):
+                        value = map(lambda x: x.strip(), value.split())
+                    data.update({self.header_to_field(key) : value})
+            if not 'changelog' in data:
+                payload = message.get_payload().strip()
+                if payload:
+                    data['changelog'] = payload + "\n"
+            if prior_version and data.get("version") != prior_version:
+                raise FreecodeSessionException("Version conflict on stdin.")
+        # Merge in options from the command line;
+        # they override what's on stdin.
+        controls = ('query', 'delete', 'read', 'dryrun', 'verbose', 'showversion')
+        prior_version = data.get("version")
+        for (key, value) in options.__dict__.items():
+            if key not in controls and value != None:
+                data[key] = value
+                del options.__dict__[key]
+        if data.get("version") != prior_version and not options.delete:
+            raise FreecodeSessionException("Version conflict in options.")
+        # Hidden flag special handling
+        if "hidden_from_frontpage" in data:
+            data["hidden_from_frontpage"] = data["hidden_from_frontpage"] in ("Y", "y") 
+        # Release-Focus field special handling.
+        if "release_focus" in data:
+            if data["release_focus"] in '123456789':
+                data["release_focus"] = FreecodeMetadataFactory.focus_values[int(data["release_focus"])-1]
+            if data["release_focus"] not in FreecodeMetadataFactory.focus_values:
+                raise FreecodeSessionException("Invalid release focus")
+            if "tag_list" not in data:
+                data["tag_list"] = data["release_focus" ]
+            else:
+                data["tag_list"].append(data["release_focus" ])
+            del data["release_focus"]
+        # Now merge in the URLs, doing symbol substitution
+        urllist = []
+        for (label, furl) in urls.items():
+            for (k, v) in data.items():
+                if type(v) == type(""):
+                    furl = furl.replace('${' + k + '}', v)
+            urllist.append((label, furl))
+        # Sort out what things go where 
+        update = Update()
+        if options.showversion:
+            pass
+        elif options.query:
+            update.name = options.query
+        else:
+            update.name = data.pop('name')
+            update.urlassoc = urllist
+            for (k, v) in data.items():
+                if k in FreecodeMetadataFactory.projectwide:
+                    # Hack to get around a namespace collision
+                    if k == "project_release_tag":
+                        k = "release_tag"
+                    update.per_project[k] = v
+                else:
+                    update.per_release[k] = v
+        # Return this
+        return (options, update)
+
+if __name__ == "__main__":
+    try:
+        # First, gather update data from stdin and command-line switches
+        factory = FreecodeMetadataFactory()
+        (options, update) = factory.getMetadata(sys.stdin)
+        # Some switches shouldn't be passed to the server
+        query = 'query' in options.__dict__ and options.query
+        verbose = 'verbose' in options.__dict__ and options.verbose
+        delete  = 'delete' in options.__dict__ and options.delete
+        dryrun  = 'dryrun' in options.__dict__ and options.dryrun
+        showversion  = 'showversion' in options.__dict__ and options.showversion
+        if showversion:
+            print "freecode-submit", version
+            raise SystemExit, 0
+        # Time to ship the update.
+        # Establish session
+        session = FreecodeSession(verbose=int(verbose), emit_enable=not dryrun)
+        session.on_project(update.name)
+        if options.query:
+            print "Project: %s" % session.project_data["name"]
+            print "Summary: %s" % session.project_data["oneliner"]
+            print "Description: %s" % session.project_data["description"].replace("\n", "\n    ").rstrip()
+            print "License-List: %s" % ",".join(session.project_data["license_list"])
+            print "Project-Tag-List: %s" % ",".join(session.project_data["tag_list"])
+            for assoc in session.project_data['approved_urls']:
+                #print "Querying", assoc["redirector"]
+                #req = RequestWithMethod(method="HEAD",
+                #                        url=assoc["redirector"],
+                #                        data={},
+                #                        headers={})
+                #handle = urllib2.urlopen(req)
+                #print "==="
+                #print handle.info()
+                #print "==="
+                print "%s-URL: %s" % (assoc["label"].replace(" ","-"), assoc["redirector"])
+            if 'recent_releases' in session.project_data:
+                most_recent = session.project_data['recent_releases'][0]
+                print "Version: %s" % most_recent['version'] 
+                print "Tag-List: %s" % ",".join(most_recent['tag_list'])
+                if most_recent.get('hidden_from_frontpage'):
+                    print "Hide: Y"
+                else:
+                    print "Hide: N"
+                print ""
+                print most_recent["changelog"]
+        else:
+            # OK, now actually add or delete the release.
+            if update.per_project:
+                session.update_core(update.per_project)
+            if update.urlassoc:
+                session.update_urls(update.urlassoc)
+            if delete:
+                session.withdraw_release(update.per_release['version'])
+            elif update.per_release and update.per_release.keys()!=["version"]:
+                session.publish_release(update.per_release)
+    except FreecodeSessionException, e:
+        print >>sys.stderr,"freecode-submit:", e.msg
+        sys.exit(1)
+    except urllib2.HTTPError, f:
+        print >>sys.stderr,"freecode-submit: HTTPError %s" %  (f.code)
+        print >>sys.stderr,f.read()
+        sys.exit(1)
+    except urllib2.URLError, f:
+        print >>sys.stderr,"freecode-submit: URLError %s" %  (f.reason,)
+        print >>sys.stderr,f.read()
+        sys.exit(1)
+
+# end
+
diff --git a/freecode-submit.xml b/freecode-submit.xml
new file mode 100644 (file)
index 0000000..65b761c
--- /dev/null
@@ -0,0 +1,402 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!DOCTYPE refentry PUBLIC 
+   "-//OASIS//DTD DocBook XML V4.1.2//EN"
+   "docbook/docbookx.dtd">
+<refentry id='freecode-submit.1'>
+<refmeta>
+<refentrytitle>freecode-submit</refentrytitle>
+<manvolnum>1</manvolnum>
+<refmiscinfo class='source'>freecode-submit</refmiscinfo>
+<refmiscinfo class='manual'>Development Tools</refmiscinfo>
+<refmiscinfo class='date'>Oct 17 2010</refmiscinfo>
+</refmeta>
+<refnamediv id='name'>
+<refname>freecode-submit</refname>
+<refpurpose>submit release announcements and project metadata updates to freecode.net</refpurpose>
+</refnamediv>
+<refsynopsisdiv id='synopsis'>
+
+<cmdsynopsis>
+  <command>freecode-submit</command>
+    <arg>-q</arg>
+    <arg>-V</arg>
+    <arg>-n</arg>
+    <arg>-N</arg>
+    <arg>-d</arg>
+    <arg>--showversion</arg>
+    <arg><replaceable>data-options</replaceable></arg>
+    <arg rep='repeat'><replaceable>data-options</replaceable></arg>
+</cmdsynopsis>
+</refsynopsisdiv>
+
+<refsect1 id='description'><title>DESCRIPTION</title>
+
+<para><application>freecode-submit</application> is a command-line tool
+for submitting release announcements to freecode.net.  It is intended
+for batch-mode operation in project release scripts, and designed so
+that its input format can easily be generated by scripts.</para>
+
+<para><application>freecode-submit</application> accepts release
+information from a job card (an email-message-like data block on
+standard input) or from command-line flags. Values set with
+command-line flags override those collected from the job card. The
+accumulated data is entered into the freecode database via its
+API.</para>
+
+<para>A few flags control the operation of
+<application>freecode-submit</application> itself.</para>
+
+<variablelist>
+<varlistentry>
+<term>-q, --query</term>
+<listitem>
+<para>Query the specified project. Delivers to standard output a job
+card describing the project metadata that can be edited and fed back
+to this tool. The data with release rather than project scope is for
+the most recent release.</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>-d, --delete</term>
+<listitem>
+<para>Delete the specified release rather than submitting it.  With
+this option, all data other than Project and Release are
+ignored. Note: only works if the release has been submitted but not
+yet passed by a moderator; if the release is already posted, invoking
+this mode will throw an error.</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>-n, --no-stdin</term>
+<listitem>
+<para>Process command-line options only; don't read data from
+standard input.</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>-N, --dryrun</term>
+<listitem>
+<para>Generate JSON, but don't ship any modification requests to Freecode.
+Useful for debugging in combination with -V.  Note, this mode may generate
+requests which read data from Freecode in order to get permalink data.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>-V, --verbose</term>
+<listitem>
+<para>Enable verbose debugging of the transaction.  This
+option is mainly for developers.</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>--showversion</term>
+<listitem>
+<para>Show the version of this program and exit.</para>
+</listitem>
+</varlistentry>
+</variablelist>
+
+<para>Release information is accepted from sources in the
+following order:</para>
+
+<orderedlist>
+<listitem>
+<para>An RFC-2822-format job card, strongly resembling an ordinary
+email message, on standard input.  The message header fields each
+specify a particular item of release information; the body (if
+present) becomes the change announcement text. Within URL fields, the
+string ${version} will be replaced with the contents of the Version
+field.</para>
+</listitem>
+
+<listitem>
+<para>Command-line switches.</para>
+</listitem>
+</orderedlist>
+
+<para>Here is a table of metadata fields and their 
+corresponding command-line switches:</para>
+
+<informaltable>
+<tgroup cols='5' rowsep='1'>
+<thead>
+<row>
+  <entry>Message field</entry>
+  <entry>Scope</entry>
+  <entry>Short switch</entry>
+  <entry>Long switch</entry>
+  <entry>Explanation</entry>
+</row>
+</thead>
+<tbody>
+
+<row>
+<entry>Project:</entry>      <entry>Project</entry>
+<entry>-p</entry><entry>--project</entry>
+<entry>
+<para>Name of the project (freecode shortname) to operate on. You cannot
+change the project-name attribute with this program; to do that, you
+need to use the Freecode web interface.</para>
+</entry>
+</row>
+
+<row>
+<entry>Summary:</entry>  <entry>Project</entry>
+<entry>-S</entry><entry>--summary</entry>
+<entry>
+<para>One-line summary of the project.</para>
+</entry>
+</row>
+
+<row>
+<entry>Description:</entry>  <entry>Project</entry>
+<entry>-D</entry><entry>--description</entry>
+<entry>
+<para>Multiline description of the project. (Use RFC-822-style continuation 
+lines)</para>
+</entry>
+</row>
+
+<row>
+<entry>License-List:</entry>  <entry>Project</entry>
+<entry>-L</entry><entry>--license-list</entry>
+<entry>
+<para>Specify the licenses under which the project is issued.  This
+may be a comma-separated list.</para>
+</entry>
+</row>
+
+<row>
+<entry>Project-Tag-List:</entry><entry>Release</entry>
+<entry>-T</entry><entry>--project-tag-list</entry>
+<entry>
+<para>Tags to attach to this project. May be a comma-separated list.</para>
+</entry>
+</row>
+
+<row>
+<entry>Version:</entry>             <entry>Release</entry>
+<entry>-v</entry><entry>--version</entry>
+<entry>
+<para>Version string to be associated with the release.</para>
+</entry>
+</row>
+
+<row>
+<entry>Changes:</entry>      <entry>Release</entry>
+<entry>-c</entry><entry>--changes</entry>
+<entry>
+<para>The Changes field. If no Changes field or option is already
+present, the changes text is taken from the body of the RFC-822
+message on standard input.</para>
+</entry>
+</row>
+
+<row>
+<entry>Release-Focus:</entry><entry>Release</entry>
+<entry>-r</entry><entry>--release-focus</entry>
+<entry>
+<para>Purpose of this release.  See the table of release-focus types 
+below.</para>
+</entry>
+</row>
+
+<row>
+<entry>Release-Tag-List:</entry> <entry>Release</entry>
+<entry>-t</entry><entry>--tag-list</entry>
+<entry>
+<para>Tags to attach to this release. May be a comma-separated list.</para>
+</entry>
+</row>
+
+<row>
+<entry>Hide:</entry>         <entry>Release</entry>
+<entry>-x</entry><entry>--hide</entry>
+<entry>
+<para>If this field is present and has the value &ldquo;Y&rdquo;, this
+release will not be visible on the project page.</para>
+</entry>
+</row>
+
+</tbody>
+</tgroup>
+</informaltable>
+
+<para>Additionally, you can specify tagged URL fields for the
+Freecode project record with header lines ending in "-URL:".  The tag
+will be the header with the "-URL:" suffix stripped off. Some examples
+of how to set frequently-used URL types:</para>
+
+<informaltable>
+<tgroup cols='3' rowsep='1'>
+<thead>
+<row>
+  <entry>Message field</entry>
+  <entry>Tag</entry>
+  <entry>Usage</entry>
+</row>
+</thead>
+<tbody>
+
+<row>
+  <entry>Website-URL:</entry>
+  <entry>Website</entry>
+  <entry>Project website home page.  If you specify this, it will be used to 
+  activate the "More Information" link and Home icon on the project page.</entry>
+</row>
+
+<row>
+  <entry>Download-URL:</entry>
+  <entry>Download</entry>
+  <entry>Preferred download URL.  If you specify this, it will be used to 
+  activate the "Download" link anf foppy-disk on the project page.</entry>
+</row>
+
+<row>
+  <entry>Tar/GZ-URL:</entry>
+  <entry>Tar/GZ</entry>
+  <entry>tar archive compressed with gzip</entry>
+</row>
+
+<row>
+  <entry>Tar/BZ-URL:</entry>
+  <entry>Tar/BZ</entry>
+  <entry>tar archive compressed with bzip</entry>
+</row>
+
+<row>
+  <entry>Changelog-URL</entry>
+  <entry>Changelog</entry>
+  <entry>project change log</entry>
+</row>
+
+</tbody>
+</tgroup>
+</informaltable>
+
+<para>Note that the list of URLs you ship does not merely add to the 
+existing list but overwrites it - that is how you delete old URLs
+which are no longer valid.</para>
+
+<para>The Summary, Description, License-List, Project-Tag-List and all
+URL fields, when specified, actually change the record for the
+project.  Only the Tag-List, Changes, Version, Release-Focus, and Hide
+fields are actually stored for each version.</para>
+
+<para>Here are the release-focus types.  Either the string or the
+integer is acceptable.  Case of the string is significant.</para>
+
+<informaltable>
+<tgroup cols='2' rowsep='1'>
+<tbody>
+<row><entry>Initial freecode announcement</entry><entry>1</entry></row>
+<row><entry>Documentation</entry><entry>2</entry></row>
+<row><entry>Code cleanup</entry><entry>3</entry></row>
+<row><entry>Minor feature enhancements</entry><entry>4</entry></row>
+<row><entry>Major feature enhancements</entry><entry>5</entry></row>
+<row><entry>Minor bugfixes</entry><entry>6</entry></row>
+<row><entry>Major bugfixes</entry><entry>7</entry></row>
+<row><entry>Minor security fixes</entry><entry>8</entry></row>
+<row><entry>Major security fixes</entry><entry>9</entry></row>
+</tbody>
+</tgroup>
+</informaltable>
+
+<para>The following elements of Freecode project metadata cannot yet
+be modified using this tool: <varname>name</varname>,
+<varname>translation list</varname>, <varname>programming
+language list</varname>, <varname>operating system
+list</varname>, <varname>approved screenshots</varname>,
+and dependency records.</para>
+
+<para>Support for updating all these elements except
+<varname>name</varname> is planned in future releases.</para>
+</refsect1>
+
+<refsect1><title>USAGE</title>
+
+<para>Here is an example of a release-information record that could be
+fed to <application>freecode-submit</application> on standard input:</para>
+
+<programlisting>
+Project: doclifter
+Version: 1.1
+Release-Focus: Major feature enhancements
+Hide: N
+Website-URL: http://www.catb.org/~esr/doclifter/
+Tar/GZ-URL: http://www.catb.org/~esr/doclifter/doclifter-${version}.tar.gz
+
+Fixed a bug in conditional evaluation that twadmin(8) tickled.  Better
+detection of pure inclusions. Better blank-section elimination.
+Kleene star recognized as ... synonym in command synopses.  Correct
+some bugs in semantic-hint collection.  Recognize Qt manual pages and
+use their conventions.  Better lifting of mandoc-generated
+pages. Translate groff-style \[...] escapes, flag unknown ones.  Can
+now parse K&amp;R style function prototypes as well as ANSI ones. This
+version lifts 96% of 9829 manual pages in a full Red Hat 9
+installation with Fedora updates to *validated* XML-DocBook.
+</programlisting>
+</refsect1>
+
+<refsect1><title>WARNING</title>
+<para>Since this program was first written, Freecode has changed its
+data API significantly. While we have attempted to maintain backward
+compatibility, the data model has changed in significant ways.</para>
+
+<para>There is no longer a Branch attribute.</para>
+
+<para>The old Release-Focus field is gone.  As a special case, this
+pragram will validate it and use it to fill in the project tags list.</para>
+
+<para>The old License field is now License-List and can take a
+comma-separated list of licenses.</para>
+
+<para>All the old URL fields are gone.  They're replaced by
+the general mechanism for pairing URLs with labels.</para>
+
+<para>Earlier versions parsed RPM specfiles. This was poor
+separation of function and has been removed. That responsibility
+belong to the human or program generating the job card.</para>
+</refsect1>
+
+<refsect1><title>FILES</title>
+
+<variablelist>
+<varlistentry>
+<term><filename>~/.netrc</filename></term>
+<listitem>
+<para><application>freecode-submit</application> expects to find your
+Freecode authorization tag here.  The correct form for an entry looks like
+this:</para>
+
+<screen>
+machine freecode
+     account &lt;your_api_auth_code&gt;
+</screen>
+
+<para>For security, make sure your <filename>~/.netrc</filename> file
+is not world-readable; we recommend doing <command>chmod 600
+~/.netrc</command>.</para>
+</listitem>
+</varlistentry>
+</variablelist>
+
+</refsect1>
+<refsect1><title>BUGS</title>
+
+<para>The URLs returned in a project metadata query are Freecode redirectors,
+not the actual URLS supplied by previous project updates.  This is due to
+a limitation in the Freecode API which may be fixed in future versions
+of that API.</para>
+</refsect1>
+<refsect1><title>RETURN VALUES</title>
+
+<para>0 on success, 1 on failure.  In case of failure, any error message
+passed back by the Freecode API is issued to standard error.</para>
+
+</refsect1>
+<refsect1 id='author'><title>AUTHORS</title> 
+<para>Eric S. Raymond <email>esr@thyrsus.com</email>.</para> 
+</refsect1> 
+</refentry>
diff --git a/freshmeat-submit b/freshmeat-submit
deleted file mode 100755 (executable)
index 52ac4d0..0000000
+++ /dev/null
@@ -1,405 +0,0 @@
-#!/usr/bin/env python
-#
-# freshmeat-submit -- script transactions with the Freshmeat server
-
-import sys, re, urllib2, json, netrc, email.Parser, optparse
-
-version="2.2"
-
-class Update:
-    "Encapsulate dictionaries describing a project metadata update."
-    def __init__(self):
-        self.name = None
-        self.per_project = {}
-        self.urlassoc = []
-        self.per_release = {}
-    def __repr__(self):
-        return "Update(" + repr(self.__dict__) + ")"
-
-# The Freshmeat API implementation is sensitive to the kind of HTTP request
-# method you use.  The general rule is:
-#
-# Reading records:                GET
-# Adding new records:             POST
-# Updating existing records:      PUT
-# Deleting existing records:      DELETE
-#
-# From http://help.freshmeat.net/faqs/api-7/data-api-intro:
-# 200 OK - Request was successful, the requested content is included
-# 201 Created - Creation of a new resource was successful
-# 401 Unauthorized - You need to provide an authentication code with this
-#     request
-# 403 Forbidden - You don't have permission to access this URI
-# 404 Not Found - The requested resource was not found
-# 409 Conflict - The validation of your submitted data failed, please check
-#     the response body for error pointers
-# 500 Server Error - The request hit a problem and was aborted, please report
-#     as a bug if it persists
-# 503 Service Unavailable - You hit your API credit limit
-
-class RequestWithMethod(urllib2.Request):
-    "Hack for forcing the method in a request - allows PUT and DELETE"
-    def __init__(self, method, *args, **kwargs):
-        # This assignment works directly in older Python versions
-        self._method = method
-        urllib2.Request.__init__(self, *args, **kwargs)
-    def get_method(self):
-        "Deduce the HTTP method to use."
-        # This method works in newer Pythons (2.6.5, possibly earlier).
-        if self._method:
-            return self._method
-        elif self.has_data():
-            return "POST"
-        else:
-            return "GET"
-
-class FreshmeatSessionException(Exception):
-    def __init__(self, msg):
-        self.msg = msg
-
-class FreshmeatSession:
-    "Encapsulate the state of a Freshmeat API session."
-    server = "http://freshmeat.net/"
-
-    def __init__(self, auth=None, verbose=0, emit_enable=True):
-        "Initialize Freshmeat session credentials."
-        self.auth = auth
-        self.verbose = verbose
-        self.emit_enable = emit_enable
-        self.project = None
-        # If user didn't supply credentials, fetch from ~/.netrc
-        if not self.auth:
-            try:
-                credentials = netrc.netrc()
-            except netrc.NetrcParseError, e:
-                raise FreshmeatSessionException("ill-formed .netrc: %s:%s %s" \
-                                               % (e.filename, e.lineno, e.msg))
-            except IOError, e:
-                raise FreshmeatSessionException(("missing .netrc file %s" % \
-                                                 str(e).split()[-1]))
-            ret = credentials.authenticators("freshmeat")
-            if not ret:
-                raise FreshmeatSessionException("no credentials for Freshmeat")
-            login, self.auth, password = ret
-
-    def on_project(self, name):
-        "Select project by Freshmeat shortname."
-        if self.verbose:
-            print "Selecting project: %s" % name
-        self.project = name
-        query = FreshmeatSession.server + "projects/%s.json?auth_code=%s" \
-                % (self.project, self.auth)
-        handle = urllib2.urlopen(query)
-        content = json.loads(handle.read())
-        self.project_data = content['project']
-        #if self.verbose:
-        #    print "Project data: %s" % self.project_data
-        self.permalink = self.project_data['permalink'] 
-        self.id = self.project_data['id'] 
-
-    def edit_request(self, url, method="GET", request={}, force=False):
-        "Wrap a JSON object with the auth code and ship it as a request"
-        url = FreshmeatSession.server + url
-        data = {"auth_code" : self.auth}
-        data.update(request)
-        data = json.dumps(data)
-        headers = {"Content-Type" : "application/json"}
-        if self.verbose:
-            print "Request URL:", method, url
-        #if self.verbose:
-        #    print "Request headers:", headers
-        if self.verbose:
-            print "Request data:", data
-        if self.emit_enable or force:
-            req = RequestWithMethod(method=method,
-                                    url=url,
-                                    data=data,
-                                    headers=headers)
-            handle = urllib2.urlopen(req)
-            if self.verbose:
-                print handle.info()
-            content = handle.read()
-            if self.verbose:
-                print "Response:", content
-            return content
-
-    def publish_release(self, data):
-        "Add a new release to the current project."
-        if self.verbose:
-            print "Publishing %s release: %s" % (self.project, `data`)
-        self.edit_request("projects/" + self.permalink + "/releases.json",
-                          "POST",
-                          {"release": data})
-
-    def withdraw_release(self, version):
-        "Withdraw a specified release from the current project."
-        if self.verbose:
-            print "Withdrawing %s release: %s" % (self.project, version)
-        releases = self.edit_request("projects/%s/releases/pending.json" \
-                                     % self.permalink, force=True)
-        releases = json.loads(releases)
-        for release in releases:
-            properties = release["release"]
-            if properties.get("version") == version:
-                vid = properties["id"]
-                print "I see", properties
-                break
-        else:
-            raise FreshmeatSessionException("couldn't find release %s"%version) 
-        deletelink = "projects/%s/releases/%s.json" % (self.permalink, vid)
-        self.edit_request(deletelink, "DELETE", {})
-
-    def update_core(self, coredata):
-        "Update the core data for a project."
-        if self.verbose:
-            print "Core data update for %s is: %s" % (self.project, coredata)
-        self.edit_request("projects/" + self.permalink + ".json",
-                          "PUT",
-                          {"project": coredata})
-
-    def update_urls(self, urlassoc):
-        "Update URL list for a project."
-        if self.verbose:
-            print "URL list update for %s is: %s" % (self.project, urlassoc)
-        # First, get permalinks for all existing URLs
-        query = FreshmeatSession.server + "projects/%s/urls.json?auth_code=%s" \
-                % (self.permalink, self.auth)
-        handle = urllib2.urlopen(query)
-        content = json.loads(handle.read())
-        permadict = {}
-        for item in content:
-            inner = item['url']
-            permadict[inner['label']] = inner['permalink']
-        # OK, now run through the update list...
-        for (label, url) in urlassoc:
-            if label in permadict:
-                # updating where we need to
-                self.edit_request("projects/%s/urls/%s.json"
-                                  % (self.permalink, permadict[label]),
-                                  "PUT",
-                                  {"url" : {"label": label, "location" : url}})
-            else:
-                # and adding where we don't.
-                self.edit_request("projects/%s/urls.json"
-                                  % (self.permalink,),
-                                  "POST",
-                                  {"url" : {"label": label, "location" : url}})
-        # Delete URLs that weren't in the update list
-        remove = set(permadict.keys()) - set(map(lambda x: x[0], urlassoc))
-        for label in remove:
-            self.edit_request("projects/%s/urls/%s.json?auth_code=%s"
-                                  % (self.permalink, permadict[label], self.auth),
-                                  "DELETE", {})
-class FreshmeatMetadataFactory:
-    "Factory class for producing Freshmeat records in JSON."
-    freshmeat_field_map = (
-        ("Project",          "P", "name"),                   # Project
-        ("Summary",          "S", "oneliner"),               # Project
-        ("Description",      "D", "description"),            # Project
-        ("License-List",     "L", "license_list"),           # Project
-        ("Project-Tag-List", "T", "project_tag_list"),       # Project
-        ("Version",          "v", "version"),                # Release
-        ("Changes",          "c", "changelog"),              # Release
-        ("Hide",             "x", "hidden_from_frontpage"),  # Release
-        ("Release-Tag-List", "t", "tag_list"),               # Release
-        # Note: Release Focus is no longer a real Freshmeat field.
-        # We remap it below.
-        ("Release-Focus",    "r", "release_focus"),          # Release
-        )
-    # Conventional values for release focus
-    focus_values = (
-        "Initial freshmeat announcement",
-        "Documentation",
-        "Code cleanup",
-        "Minor feature enhancements",
-        "Major feature enhancements",
-        "Minor bugfixes",
-        "Major bugfixes",
-        "Minor security fixes",
-        "Major security fixes",
-    )
-    # Which attributes have project scope, all others have release scupe
-    projectwide = ('name',
-                   'description',
-                   'oneliner',
-                   'license_list',
-                   'project_tag_list')
-
-    def __init__(self):
-        self.message_parser = email.Parser.Parser()
-        self.argument_parser = optparse.OptionParser( \
-            usage="usage: %prog [options]")
-        for (msg_field, shortopt, rpc_field) in FreshmeatMetadataFactory.freshmeat_field_map:
-            self.argument_parser.add_option("-" + shortopt,
-                                            "--" + msg_field.lower(),
-                                            dest=rpc_field,
-                                            help="Set the %s field"%msg_field)
-        self.argument_parser.add_option('-q', '--query', dest='query',
-                          help='Query metadata for PROJECT',metavar="PROJECT")
-        self.argument_parser.add_option('-d', '--delete', dest='delete',
-                          default=False, action='store_true',
-                          help='Suppress reading fields from stdin.')
-        self.argument_parser.add_option('-n', '--no-stdin', dest='read',
-                          default=True, action='store_false',
-                          help='Suppress reading fields from stdin.')
-        self.argument_parser.add_option('-N', '--dryrun', dest='dryrun',
-                          default=False, action='store_true',
-                          help='Suppress reading fields from stdin.')
-        self.argument_parser.add_option('-V', '--verbose', dest='verbose',
-                          default=False, action='store_true',
-                          help='Enable verbose debugging.')
-        self.argument_parser.add_option('-?', '--showversion', dest='showversion',
-                          default=False, action='store_true',
-                          help='Show version and quit.')
-        
-    def header_to_field(self, hdr):
-        lhdr = hdr.lower().replace("-", "_")
-        for (alias, shortopt, field) in FreshmeatMetadataFactory.freshmeat_field_map:
-            if lhdr == alias.lower().replace("-", "_").replace("/", "_"):
-                return field
-        raise FreshmeatSessionException("Illegal field name %s" % hdr)
-
-    def getMetadata(self, stream):
-        "Return an Update object describing project and release attributes."
-        data = {}
-        urls = {}
-        (options, args) = self.argument_parser.parse_args()
-        # Stuff from stdin if present
-        prior_version = data.get("version")
-        if not (options.query or options.showversion) and options.read:
-            message = self.message_parser.parse(stream)
-            for (key, value) in message.items():
-                value = re.sub("\n +", " ", value).strip()
-                if key.endswith("-URL"):
-                    key = key.replace("-", " ")
-                    urls.update({key[:-4] : value})
-                else:
-                    if key.endswith("List"):
-                        value = map(lambda x: x.strip(), value.split())
-                    data.update({self.header_to_field(key) : value})
-            if not 'changelog' in data:
-                payload = message.get_payload().strip()
-                if payload:
-                    data['changelog'] = payload + "\n"
-            if prior_version and data.get("version") != prior_version:
-                raise FreshmeatSessionException("Version conflict on stdin.")
-        # Merge in options from the command line;
-        # they override what's on stdin.
-        controls = ('query', 'delete', 'read', 'dryrun', 'verbose', 'showversion')
-        prior_version = data.get("version")
-        for (key, value) in options.__dict__.items():
-            if key not in controls and value != None:
-                data[key] = value
-                del options.__dict__[key]
-        if data.get("version") != prior_version and not options.delete:
-            raise FreshmeatSessionException("Version conflict in options.")
-        # Hidden flag special handling
-        if "hidden_from_frontpage" in data:
-            data["hidden_from_frontpage"] = data["hidden_from_frontpage"] in ("Y", "y") 
-        # Release-Focus field special handling.
-        if "release_focus" in data:
-            if data["release_focus"] in '123456789':
-                data["release_focus"] = FreshmeatMetadataFactory.focus_values[int(data["release_focus"])-1]
-            if data["release_focus"] not in FreshmeatMetadataFactory.focus_values:
-                raise FreshmeatSessionException("Invalid release focus")
-            if "tag_list" not in data:
-                data["tag_list"] = data["release_focus" ]
-            else:
-                data["tag_list"].append(data["release_focus" ])
-            del data["release_focus"]
-        # Now merge in the URLs, doing symbol substitution
-        urllist = []
-        for (label, furl) in urls.items():
-            for (k, v) in data.items():
-                if type(v) == type(""):
-                    furl = furl.replace('${' + k + '}', v)
-            urllist.append((label, furl))
-        # Sort out what things go where 
-        update = Update()
-        if options.showversion:
-            pass
-        elif options.query:
-            update.name = options.query
-        else:
-            update.name = data.pop('name')
-            update.urlassoc = urllist
-            for (k, v) in data.items():
-                if k in FreshmeatMetadataFactory.projectwide:
-                    # Hack to get around a namespace collision
-                    if k == "project_release_tag":
-                        k = "release_tag"
-                    update.per_project[k] = v
-                else:
-                    update.per_release[k] = v
-        # Return this
-        return (options, update)
-
-if __name__ == "__main__":
-    try:
-        # First, gather update data from stdin and command-line switches
-        factory = FreshmeatMetadataFactory()
-        (options, update) = factory.getMetadata(sys.stdin)
-        # Some switches shouldn't be passed to the server
-        query = 'query' in options.__dict__ and options.query
-        verbose = 'verbose' in options.__dict__ and options.verbose
-        delete  = 'delete' in options.__dict__ and options.delete
-        dryrun  = 'dryrun' in options.__dict__ and options.dryrun
-        showversion  = 'showversion' in options.__dict__ and options.showversion
-        if showversion:
-            print "freshmeat-submit", version
-            raise SystemExit, 0
-        # Time to ship the update.
-        # Establish session
-        session = FreshmeatSession(verbose=int(verbose), emit_enable=not dryrun)
-        session.on_project(update.name)
-        if options.query:
-            print "Project: %s" % session.project_data["name"]
-            print "Summary: %s" % session.project_data["oneliner"]
-            print "Description: %s" % session.project_data["description"].replace("\n", "\n    ").rstrip()
-            print "License-List: %s" % ",".join(session.project_data["license_list"])
-            print "Project-Tag-List: %s" % ",".join(session.project_data["tag_list"])
-            for assoc in session.project_data['approved_urls']:
-                #print "Querying", assoc["redirector"]
-                #req = RequestWithMethod(method="HEAD",
-                #                        url=assoc["redirector"],
-                #                        data={},
-                #                        headers={})
-                #handle = urllib2.urlopen(req)
-                #print "==="
-                #print handle.info()
-                #print "==="
-                print "%s-URL: %s" % (assoc["label"].replace(" ","-"), assoc["redirector"])
-            if 'recent_releases' in session.project_data:
-                most_recent = session.project_data['recent_releases'][0]
-                print "Version: %s" % most_recent['version'] 
-                print "Tag-List: %s" % ",".join(most_recent['tag_list'])
-                if most_recent.get('hidden_from_frontpage'):
-                    print "Hide: Y"
-                else:
-                    print "Hide: N"
-                print ""
-                print most_recent["changelog"]
-        else:
-            # OK, now actually add or delete the release.
-            if update.per_project:
-                session.update_core(update.per_project)
-            if update.urlassoc:
-                session.update_urls(update.urlassoc)
-            if delete:
-                session.withdraw_release(update.per_release['version'])
-            elif update.per_release and update.per_release.keys()!=["version"]:
-                session.publish_release(update.per_release)
-    except FreshmeatSessionException, e:
-        print >>sys.stderr,"freshmeat-submit:", e.msg
-        sys.exit(1)
-    except urllib2.HTTPError, f:
-        print >>sys.stderr,"freshmeat-submit: HTTPError %s" %  (f.code)
-        print >>sys.stderr,f.read()
-        sys.exit(1)
-    except urllib2.URLError, f:
-        print >>sys.stderr,"freshmeat-submit: URLError %s" %  (f.reason,)
-        print >>sys.stderr,f.read()
-        sys.exit(1)
-
-# end
-
diff --git a/freshmeat-submit.xml b/freshmeat-submit.xml
deleted file mode 100644 (file)
index 2e6cc2d..0000000
+++ /dev/null
@@ -1,402 +0,0 @@
-<?xml version="1.0" encoding="ISO-8859-1"?>
-<!DOCTYPE refentry PUBLIC 
-   "-//OASIS//DTD DocBook XML V4.1.2//EN"
-   "docbook/docbookx.dtd">
-<refentry id='freshmeat-submit.1'>
-<refmeta>
-<refentrytitle>freshmeat-submit</refentrytitle>
-<manvolnum>1</manvolnum>
-<refmiscinfo class='source'>freshmeat-submit</refmiscinfo>
-<refmiscinfo class='manual'>Development Tools</refmiscinfo>
-<refmiscinfo class='date'>Oct 17 2010</refmiscinfo>
-</refmeta>
-<refnamediv id='name'>
-<refname>freshmeat-submit</refname>
-<refpurpose>submit release announcements and project metadata updates to freshmeat.net</refpurpose>
-</refnamediv>
-<refsynopsisdiv id='synopsis'>
-
-<cmdsynopsis>
-  <command>freshmeat-submit</command>
-    <arg>-q</arg>
-    <arg>-V</arg>
-    <arg>-n</arg>
-    <arg>-N</arg>
-    <arg>-d</arg>
-    <arg>--showversion</arg>
-    <arg><replaceable>data-options</replaceable></arg>
-    <arg rep='repeat'><replaceable>data-options</replaceable></arg>
-</cmdsynopsis>
-</refsynopsisdiv>
-
-<refsect1 id='description'><title>DESCRIPTION</title>
-
-<para><application>freshmeat-submit</application> is a command-line tool
-for submitting release announcements to freshmeat.net.  It is intended
-for batch-mode operation in project release scripts, and designed so
-that its input format can easily be generated by scripts.</para>
-
-<para><application>freshmeat-submit</application> accepts release
-information from a job card (an email-message-like data block on
-standard input) or from command-line flags. Values set with
-command-line flags override those collected from the job card. The
-accumulated data is entered into the freshmeat database via its
-API.</para>
-
-<para>A few flags control the operation of
-<application>freshmeat-submit</application> itself.</para>
-
-<variablelist>
-<varlistentry>
-<term>-q, --query</term>
-<listitem>
-<para>Query the specified project. Delivers to standard output a job
-card describing the project metadata that can be edited and fed back
-to this tool. The data with release rather than project scope is for
-the most recent release.</para>
-</listitem>
-</varlistentry>
-<varlistentry>
-<term>-d, --delete</term>
-<listitem>
-<para>Delete the specified release rather than submitting it.  With
-this option, all data other than Project and Release are
-ignored. Note: only works if the release has been submitted but not
-yet passed by a moderator; if the release is already posted, invoking
-this mode will throw an error.</para>
-</listitem>
-</varlistentry>
-<varlistentry>
-<term>-n, --no-stdin</term>
-<listitem>
-<para>Process command-line options only; don't read data from
-standard input.</para>
-</listitem>
-</varlistentry>
-<varlistentry>
-<term>-N, --dryrun</term>
-<listitem>
-<para>Generate JSON, but don't ship any modification requests to Freshmeat.
-Useful for debugging in combination with -V.  Note, this mode may generate
-requests which read data from Freshmeat in order to get permalink data.
-</para>
-</listitem>
-</varlistentry>
-<varlistentry>
-<term>-V, --verbose</term>
-<listitem>
-<para>Enable verbose debugging of the transaction.  This
-option is mainly for developers.</para>
-</listitem>
-</varlistentry>
-<varlistentry>
-<term>--showversion</term>
-<listitem>
-<para>Show the version of this program and exit.</para>
-</listitem>
-</varlistentry>
-</variablelist>
-
-<para>Release information is accepted from sources in the
-following order:</para>
-
-<orderedlist>
-<listitem>
-<para>An RFC-2822-format job card, strongly resembling an ordinary
-email message, on standard input.  The message header fields each
-specify a particular item of release information; the body (if
-present) becomes the change announcement text. Within URL fields, the
-string ${version} will be replaced with the contents of the Version
-field.</para>
-</listitem>
-
-<listitem>
-<para>Command-line switches.</para>
-</listitem>
-</orderedlist>
-
-<para>Here is a table of metadata fields and their 
-corresponding command-line switches:</para>
-
-<informaltable>
-<tgroup cols='5' rowsep='1'>
-<thead>
-<row>
-  <entry>Message field</entry>
-  <entry>Scope</entry>
-  <entry>Short switch</entry>
-  <entry>Long switch</entry>
-  <entry>Explanation</entry>
-</row>
-</thead>
-<tbody>
-
-<row>
-<entry>Project:</entry>      <entry>Project</entry>
-<entry>-p</entry><entry>--project</entry>
-<entry>
-<para>Name of the project (freshmeat shortname) to operate on. You cannot
-change the project-name attribute with this program; to do that, you
-need to use the Freshmeat web interface.</para>
-</entry>
-</row>
-
-<row>
-<entry>Summary:</entry>  <entry>Project</entry>
-<entry>-S</entry><entry>--summary</entry>
-<entry>
-<para>One-line summary of the project.</para>
-</entry>
-</row>
-
-<row>
-<entry>Description:</entry>  <entry>Project</entry>
-<entry>-D</entry><entry>--description</entry>
-<entry>
-<para>Multiline description of the project. (Use RFC-822-style continuation 
-lines)</para>
-</entry>
-</row>
-
-<row>
-<entry>License-List:</entry>  <entry>Project</entry>
-<entry>-L</entry><entry>--license-list</entry>
-<entry>
-<para>Specify the licenses under which the project is issued.  This
-may be a comma-separated list.</para>
-</entry>
-</row>
-
-<row>
-<entry>Project-Tag-List:</entry><entry>Release</entry>
-<entry>-T</entry><entry>--project-tag-list</entry>
-<entry>
-<para>Tags to attach to this project. May be a comma-separated list.</para>
-</entry>
-</row>
-
-<row>
-<entry>Version:</entry>             <entry>Release</entry>
-<entry>-v</entry><entry>--version</entry>
-<entry>
-<para>Version string to be associated with the release.</para>
-</entry>
-</row>
-
-<row>
-<entry>Changes:</entry>      <entry>Release</entry>
-<entry>-c</entry><entry>--changes</entry>
-<entry>
-<para>The Changes field. If no Changes field or option is already
-present, the changes text is taken from the body of the RFC-822
-message on standard input.</para>
-</entry>
-</row>
-
-<row>
-<entry>Release-Focus:</entry><entry>Release</entry>
-<entry>-r</entry><entry>--release-focus</entry>
-<entry>
-<para>Purpose of this release.  See the table of release-focus types 
-below.</para>
-</entry>
-</row>
-
-<row>
-<entry>Release-Tag-List:</entry> <entry>Release</entry>
-<entry>-t</entry><entry>--tag-list</entry>
-<entry>
-<para>Tags to attach to this release. May be a comma-separated list.</para>
-</entry>
-</row>
-
-<row>
-<entry>Hide:</entry>         <entry>Release</entry>
-<entry>-x</entry><entry>--hide</entry>
-<entry>
-<para>If this field is present and has the value &ldquo;Y&rdquo;, this
-release will not be visible on the project page.</para>
-</entry>
-</row>
-
-</tbody>
-</tgroup>
-</informaltable>
-
-<para>Additionally, you can specify tagged URL fields for the
-Freshmeat project record with header lines ending in "-URL:".  The tag
-will be the header with the "-URL:" suffix stripped off. Some examples
-of how to set frequently-used URL types:</para>
-
-<informaltable>
-<tgroup cols='3' rowsep='1'>
-<thead>
-<row>
-  <entry>Message field</entry>
-  <entry>Tag</entry>
-  <entry>Usage</entry>
-</row>
-</thead>
-<tbody>
-
-<row>
-  <entry>Website-URL:</entry>
-  <entry>Website</entry>
-  <entry>Project website home page.  If you specify this, it will be used to 
-  activate the "More Information" link and Home icon on the project page.</entry>
-</row>
-
-<row>
-  <entry>Download-URL:</entry>
-  <entry>Download</entry>
-  <entry>Preferred download URL.  If you specify this, it will be used to 
-  activate the "Download" link anf foppy-disk on the project page.</entry>
-</row>
-
-<row>
-  <entry>Tar/GZ-URL:</entry>
-  <entry>Tar/GZ</entry>
-  <entry>tar archive compressed with gzip</entry>
-</row>
-
-<row>
-  <entry>Tar/BZ-URL:</entry>
-  <entry>Tar/BZ</entry>
-  <entry>tar archive compressed with bzip</entry>
-</row>
-
-<row>
-  <entry>Changelog-URL</entry>
-  <entry>Changelog</entry>
-  <entry>project change log</entry>
-</row>
-
-</tbody>
-</tgroup>
-</informaltable>
-
-<para>Note that the list of URLs you ship does not merely add to the 
-existing list but overwrites it - that is how you delete old URLs
-which are no longer valid.</para>
-
-<para>The Summary, Description, License-List, Project-Tag-List and all
-URL fields, when specified, actually change the record for the
-project.  Only the Tag-List, Changes, Version, Release-Focus, and Hide
-fields are actually stored for each version.</para>
-
-<para>Here are the release-focus types.  Either the string or the
-integer is acceptable.  Case of the string is significant.</para>
-
-<informaltable>
-<tgroup cols='2' rowsep='1'>
-<tbody>
-<row><entry>Initial freshmeat announcement</entry><entry>1</entry></row>
-<row><entry>Documentation</entry><entry>2</entry></row>
-<row><entry>Code cleanup</entry><entry>3</entry></row>
-<row><entry>Minor feature enhancements</entry><entry>4</entry></row>
-<row><entry>Major feature enhancements</entry><entry>5</entry></row>
-<row><entry>Minor bugfixes</entry><entry>6</entry></row>
-<row><entry>Major bugfixes</entry><entry>7</entry></row>
-<row><entry>Minor security fixes</entry><entry>8</entry></row>
-<row><entry>Major security fixes</entry><entry>9</entry></row>
-</tbody>
-</tgroup>
-</informaltable>
-
-<para>The following elements of Freshmeat project metadata cannot yet
-be modified using this tool: <varname>name</varname>,
-<varname>translation list</varname>, <varname>programming
-language list</varname>, <varname>operating system
-list</varname>, <varname>approved screenshots</varname>,
-and dependency records.</para>
-
-<para>Support for updating all these elements except
-<varname>name</varname> is planned in future releases.</para>
-</refsect1>
-
-<refsect1><title>USAGE</title>
-
-<para>Here is an example of a release-information record that could be
-fed to <application>freshmeat-submit</application> on standard input:</para>
-
-<programlisting>
-Project: doclifter
-Version: 1.1
-Release-Focus: Major feature enhancements
-Hide: N
-Website-URL: http://www.catb.org/~esr/doclifter/
-Tar/GZ-URL: http://www.catb.org/~esr/doclifter/doclifter-${version}.tar.gz
-
-Fixed a bug in conditional evaluation that twadmin(8) tickled.  Better
-detection of pure inclusions. Better blank-section elimination.
-Kleene star recognized as ... synonym in command synopses.  Correct
-some bugs in semantic-hint collection.  Recognize Qt manual pages and
-use their conventions.  Better lifting of mandoc-generated
-pages. Translate groff-style \[...] escapes, flag unknown ones.  Can
-now parse K&amp;R style function prototypes as well as ANSI ones. This
-version lifts 96% of 9829 manual pages in a full Red Hat 9
-installation with Fedora updates to *validated* XML-DocBook.
-</programlisting>
-</refsect1>
-
-<refsect1><title>WARNING</title>
-<para>Since this program was first written, Freshmeat has changed its
-data API significantly. While we have attempted to maintain backward
-compatibility, the data model has changed in significant ways.</para>
-
-<para>There is no longer a Branch attribute.</para>
-
-<para>The old Release-Focus field is gone.  As a special case, this
-pragram will validate it and use it to fill in the project tags list.</para>
-
-<para>The old License field is now License-List and can take a
-comma-separated list of licenses.</para>
-
-<para>All the old URL fields are gone.  They're replaced by
-the general mechanism for pairing URLs with labels.</para>
-
-<para>Earlier versions parsed RPM specfiles. This was poor
-separation of function and has been removed. That responsibility
-belong to the human or program generating the job card.</para>
-</refsect1>
-
-<refsect1><title>FILES</title>
-
-<variablelist>
-<varlistentry>
-<term><filename>~/.netrc</filename></term>
-<listitem>
-<para><application>freshmeat-submit</application> expects to find your
-Freshmeat authorization tag here.  The correct form for an entry looks like
-this:</para>
-
-<screen>
-machine freshmeat
-     account &lt;your_api_auth_code&gt;
-</screen>
-
-<para>For security, make sure your <filename>~/.netrc</filename> file
-is not world-readable; we recommend doing <command>chmod 600
-~/.netrc</command>.</para>
-</listitem>
-</varlistentry>
-</variablelist>
-
-</refsect1>
-<refsect1><title>BUGS</title>
-
-<para>The URLs returned in a project metadata query are Freshmeat redirectors,
-not the actual URLS supplied by previous project updates.  This is due to
-a limitation in the Freshmeat API which may be fixed in future versions
-of that API.</para>
-</refsect1>
-<refsect1><title>RETURN VALUES</title>
-
-<para>0 on success, 1 on failure.  In case of failure, any error message
-passed back by the Freshmeat API is issued to standard error.</para>
-
-</refsect1>
-<refsect1 id='author'><title>AUTHORS</title> 
-<para>Eric S. Raymond <email>esr@thyrsus.com</email>.</para> 
-</refsect1> 
-</refentry>