summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDan McGee <dan@archlinux.org>2011-11-14 12:19:17 -0600
committerDan McGee <dan@archlinux.org>2011-11-14 12:27:38 -0600
commit12408702eaf89ea338670ba808da9ef49e35c562 (patch)
treebd68ff85cadd03f88b5d5c333d33e9f62c69a953
parentb7439cacf08288d8533910e08d093e9276167ed2 (diff)
downloadarchweb-12408702eaf89ea338670ba808da9ef49e35c562.tar.gz
archweb-12408702eaf89ea338670ba808da9ef49e35c562.zip
Allow population of signoff specs with SVN commit messages
This pulls them from the latest SVN commit on trunk. We don't have a failproof method of getting the exact right commit, but this should be close if it is run on a regular basis via cron (aka hourly). Note that running locally, I needed the development version of South to get the migration included here to apply because of information_schema changes in the current version of MySQL. Signed-off-by: Dan McGee <dan@archlinux.org>
-rw-r--r--packages/management/commands/populate_signoffs.py89
-rw-r--r--packages/management/commands/signoff_report.py2
-rw-r--r--packages/migrations/0011_auto__chg_field_signoffspecification_user.py165
-rw-r--r--packages/models.py2
-rw-r--r--packages/views/signoff.py5
-rw-r--r--settings.py4
-rw-r--r--templates/packages/signoffs.html2
7 files changed, 266 insertions, 3 deletions
diff --git a/packages/management/commands/populate_signoffs.py b/packages/management/commands/populate_signoffs.py
new file mode 100644
index 00000000..5b5acbaf
--- /dev/null
+++ b/packages/management/commands/populate_signoffs.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+"""
+populate_signoffs command
+
+Pull the latest commit message from SVN for a given package that is
+signoff-eligible and does not have an existing comment attached.
+
+Usage: ./manage.py populate_signoffs
+"""
+
+from datetime import datetime
+import logging
+import subprocess
+import sys
+from xml.etree.ElementTree import XML
+
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.core.management.base import NoArgsCommand
+
+from ...models import SignoffSpecification
+from ...utils import get_signoff_groups
+
+logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s -> %(levelname)s: %(message)s',
+ datefmt='%Y-%m-%d %H:%M:%S',
+ stream=sys.stderr)
+logger = logging.getLogger()
+
+class Command(NoArgsCommand):
+ help = "Pull the latest commit message from SVN for a given package that is signoff-eligible and does not have an existing comment attached"
+
+ def handle_noargs(self, **options):
+ v = int(options.get('verbosity', None))
+ if v == 0:
+ logger.level = logging.ERROR
+ elif v == 1:
+ logger.level = logging.INFO
+ elif v == 2:
+ logger.level = logging.DEBUG
+
+ return add_signoff_comments()
+
+def svn_log(pkgbase, repo):
+ path = '%s%s/%s/trunk/' % (settings.SVN_BASE_URL, repo.svn_root, pkgbase)
+ cmd = ['svn', 'log', '--limit=1', '--xml', path]
+ log_data = subprocess.check_output(cmd)
+ # the XML format is very very simple, especially with only one revision
+ xml = XML(log_data)
+ revision = int(xml.find('logentry').get('revision'))
+ date = datetime.strptime(xml.findtext('logentry/date'),
+ '%Y-%m-%dT%H:%M:%S.%fZ')
+ return {
+ 'revision': revision,
+ 'date': date,
+ 'author': xml.findtext('logentry/author'),
+ 'message': xml.findtext('logentry/msg'),
+ }
+
+def create_specification(package, log):
+ trimmed_message = log['message'].strip()
+ spec = SignoffSpecification(pkgbase=package.pkgbase,
+ pkgver=package.pkgver, pkgrel=package.pkgrel,
+ epoch=package.epoch, arch=package.arch, repo=package.repo,
+ comments=trimmed_message)
+ try:
+ spec.user = User.objects.get(username=log['author'])
+ except User.DoesNotExist:
+ pass
+
+ return spec
+
+def add_signoff_comments():
+ logger.info("getting all signoff groups")
+ groups = get_signoff_groups()
+ logger.info("%d signoff groups found", len(groups))
+
+ for group in groups:
+ if not group.default_spec:
+ continue
+
+ logger.debug("getting SVN log for %s (%s)", group.pkgbase, group.repo)
+ log = svn_log(group.pkgbase, group.repo)
+ logger.info("creating spec with SVN message for %s", group.pkgbase)
+ spec = create_specification(group.packages[0], log)
+ spec.save()
+
+# vim: set ts=4 sw=4 et:
diff --git a/packages/management/commands/signoff_report.py b/packages/management/commands/signoff_report.py
index 3357bc1e..3b67f518 100644
--- a/packages/management/commands/signoff_report.py
+++ b/packages/management/commands/signoff_report.py
@@ -22,7 +22,7 @@ import logging
from operator import attrgetter
import sys
-from main.models import Package, Repo
+from main.models import Repo
from packages.models import Signoff
from packages.utils import get_signoff_groups
diff --git a/packages/migrations/0011_auto__chg_field_signoffspecification_user.py b/packages/migrations/0011_auto__chg_field_signoffspecification_user.py
new file mode 100644
index 00000000..f6e3cdd9
--- /dev/null
+++ b/packages/migrations/0011_auto__chg_field_signoffspecification_user.py
@@ -0,0 +1,165 @@
+# encoding: utf-8
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ db.alter_column('packages_signoffspecification', 'user_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True))
+
+ def backwards(self, orm):
+ db.alter_column('packages_signoffspecification', 'user_id', self.gf('django.db.models.fields.related.ForeignKey')(default=1, to=orm['auth.User']))
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'main.arch': {
+ 'Meta': {'ordering': "['name']", 'object_name': 'Arch', 'db_table': "'arches'"},
+ 'agnostic': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
+ },
+ 'main.package': {
+ 'Meta': {'ordering': "('pkgname',)", 'object_name': 'Package', 'db_table': "'packages'"},
+ 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'to': "orm['main.Arch']"}),
+ 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'compressed_size': ('main.models.PositiveBigIntegerField', [], {}),
+ 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'files_last_update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'flag_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'installed_size': ('main.models.PositiveBigIntegerField', [], {}),
+ 'last_update': ('django.db.models.fields.DateTimeField', [], {}),
+ 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'packager_str': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'pgp_signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'pkgdesc': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
+ 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'repo': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'to': "orm['main.Repo']"}),
+ 'url': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'})
+ },
+ 'main.repo': {
+ 'Meta': {'ordering': "['name']", 'object_name': 'Repo', 'db_table': "'repos'"},
+ 'bugs_category': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'bugs_project': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'staging': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'svn_root': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'testing': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'packages.conflict': {
+ 'Meta': {'ordering': "['name']", 'object_name': 'Conflict'},
+ 'comparison': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'conflicts'", 'to': "orm['main.Package']"}),
+ 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'})
+ },
+ 'packages.license': {
+ 'Meta': {'ordering': "['name']", 'object_name': 'License'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'licenses'", 'to': "orm['main.Package']"})
+ },
+ 'packages.packagegroup': {
+ 'Meta': {'object_name': 'PackageGroup'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'groups'", 'to': "orm['main.Package']"})
+ },
+ 'packages.packagerelation': {
+ 'Meta': {'unique_together': "(('pkgbase', 'user', 'type'),)", 'object_name': 'PackageRelation'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'type': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_relations'", 'to': "orm['auth.User']"})
+ },
+ 'packages.provision': {
+ 'Meta': {'ordering': "['name']", 'object_name': 'Provision'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'provides'", 'to': "orm['main.Package']"}),
+ 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'})
+ },
+ 'packages.replacement': {
+ 'Meta': {'ordering': "['name']", 'object_name': 'Replacement'},
+ 'comparison': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'replaces'", 'to': "orm['main.Package']"}),
+ 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'})
+ },
+ 'packages.signoff': {
+ 'Meta': {'object_name': 'Signoff'},
+ 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}),
+ 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'repo': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Repo']"}),
+ 'revoked': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_signoffs'", 'to': "orm['auth.User']"})
+ },
+ 'packages.signoffspecification': {
+ 'Meta': {'object_name': 'SignoffSpecification'},
+ 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}),
+ 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'known_bad': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'repo': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Repo']"}),
+ 'required': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'})
+ }
+ }
+
+ complete_apps = ['packages']
diff --git a/packages/models.py b/packages/models.py
index b70c21bf..0d02ab31 100644
--- a/packages/models.py
+++ b/packages/models.py
@@ -73,7 +73,7 @@ class SignoffSpecification(models.Model):
epoch = models.PositiveIntegerField(default=0)
arch = models.ForeignKey('main.Arch')
repo = models.ForeignKey('main.Repo')
- user = models.ForeignKey(User)
+ user = models.ForeignKey(User, null=True)
created = models.DateTimeField(editable=False)
required = models.PositiveIntegerField(default=2,
help_text="How many signoffs are required for this package?")
diff --git a/packages/views/signoff.py b/packages/views/signoff.py
index 26b6e710..e57b4d9a 100644
--- a/packages/views/signoff.py
+++ b/packages/views/signoff.py
@@ -98,7 +98,10 @@ def _signoff_options_all(request, name, repo):
pkgver=package.pkgver, pkgrel=package.pkgrel,
epoch=package.epoch, arch=package.arch,
repo=package.repo)
+
+ if spec.user is None:
spec.user = request.user
+
form = SignoffOptionsForm(request.POST, instance=spec)
if form.is_valid():
form.save()
@@ -122,6 +125,8 @@ def signoff_options(request, name, repo, arch):
spec = SignoffSpecification(pkgbase=package.pkgbase,
pkgver=package.pkgver, pkgrel=package.pkgrel,
epoch=package.epoch, arch=package.arch, repo=package.repo)
+
+ if spec.user is None:
spec.user = request.user
if request.POST:
diff --git a/settings.py b/settings.py
index 51f9fcf6..80e024af 100644
--- a/settings.py
+++ b/settings.py
@@ -134,4 +134,8 @@ if DEBUG_TOOLBAR:
# URL to fetch a current list of available ISOs
ISO_LIST_URL = 'http://releng.archlinux.org/isos/'
+# URL for SVN access for fetching commit messages (note absence of packages or
+# community bit on the end, repo.svn_root is appended)
+SVN_BASE_URL = 'svn+ssh://svn.archlinux.org/srv/svn-'
+
# vim: set ts=4 sw=4 et:
diff --git a/templates/packages/signoffs.html b/templates/packages/signoffs.html
index f4511f75..bd84289c 100644
--- a/templates/packages/signoffs.html
+++ b/templates/packages/signoffs.html
@@ -71,7 +71,7 @@
{% if spec.required != 2 %}Required signoffs: {{ spec.required }}<br/>{% endif %}
{% if not spec.enabled %}Signoffs are not currently enabled<br/>{% endif %}
{% if spec.known_bad %}Package is known to be bad<br/>{% endif %}
- {{ spec.comments|default:""|linebreaks }}
+ {{ spec.comments|default:""|linebreaksbr }}
{% endwith %}{% endif %}</td>
</tr>
{% endfor %}