From ad2762cf775a8dde508de47164d6429f3fd724f1 Mon Sep 17 00:00:00 2001 From: Jeremy Kerr Date: Sun, 24 May 2015 16:57:33 +0800 Subject: Move to a more recent django project structure This change updates patchwor to the newer project struture: we've moved the actual application out of the apps/ directory, and the patchwork-specific templates to under the patchwork application. This gives us the manage.py script in the top-level now. Signed-off-by: Jeremy Kerr --- patchwork/__init__.py | 0 patchwork/admin.py | 50 ++ patchwork/bin/__init__.py | 0 patchwork/bin/bash_completion | 29 + patchwork/bin/parsemail-batch.sh | 45 ++ patchwork/bin/parsemail.py | 455 +++++++++++++ patchwork/bin/parsemail.sh | 29 + patchwork/bin/patchwork-cron.py | 15 + patchwork/bin/pwclient | 744 +++++++++++++++++++++ patchwork/bin/rehash.py | 34 + patchwork/bin/update-patchwork-status.py | 70 ++ patchwork/context_processors.py | 32 + patchwork/filters.py | 471 +++++++++++++ patchwork/fixtures/default_projects.xml | 18 + patchwork/fixtures/initial_data.xml | 55 ++ patchwork/forms.py | 237 +++++++ patchwork/models.py | 386 +++++++++++ patchwork/paginator.py | 88 +++ patchwork/parser.py | 267 ++++++++ patchwork/requestcontext.py | 89 +++ patchwork/settings/__init__.py | 0 patchwork/settings/base.py | 115 ++++ patchwork/settings/dev.py | 52 ++ patchwork/settings/prod.py | 62 ++ patchwork/templates/patchwork/activation_email.txt | 11 + .../patchwork/activation_email_subject.txt | 1 + patchwork/templates/patchwork/bundle.html | 47 ++ patchwork/templates/patchwork/bundles.html | 59 ++ patchwork/templates/patchwork/confirm-error.html | 19 + patchwork/templates/patchwork/filters.html | 183 +++++ patchwork/templates/patchwork/help/about.html | 17 + patchwork/templates/patchwork/help/index.html | 2 + patchwork/templates/patchwork/help/pwclient.html | 23 + patchwork/templates/patchwork/list.html | 25 + patchwork/templates/patchwork/login.html | 27 + patchwork/templates/patchwork/logout.html | 8 + patchwork/templates/patchwork/mail-form.html | 38 ++ patchwork/templates/patchwork/mail-settings.html | 37 + patchwork/templates/patchwork/optin-request.html | 50 ++ patchwork/templates/patchwork/optin-request.mail | 12 + patchwork/templates/patchwork/optin.html | 19 + patchwork/templates/patchwork/optout-request.html | 51 ++ patchwork/templates/patchwork/optout-request.mail | 12 + patchwork/templates/patchwork/optout.html | 22 + patchwork/templates/patchwork/pagination.html | 45 ++ .../patch-change-notification-subject.text | 1 + .../patchwork/patch-change-notification.mail | 20 + patchwork/templates/patchwork/patch-list.html | 268 ++++++++ patchwork/templates/patchwork/patch.html | 199 ++++++ patchwork/templates/patchwork/profile.html | 144 ++++ patchwork/templates/patchwork/project.html | 58 ++ patchwork/templates/patchwork/projects.html | 27 + patchwork/templates/patchwork/pwclient | 1 + patchwork/templates/patchwork/pwclientrc | 15 + patchwork/templates/patchwork/register.mail | 11 + .../templates/patchwork/registration-confirm.html | 13 + .../templates/patchwork/registration_form.html | 121 ++++ patchwork/templates/patchwork/todo-list.html | 17 + patchwork/templates/patchwork/todo-lists.html | 29 + .../templates/patchwork/user-link-confirm.html | 19 + patchwork/templates/patchwork/user-link.html | 32 + patchwork/templates/patchwork/user-link.mail | 12 + patchwork/templatetags/__init__.py | 0 patchwork/templatetags/filter.py | 36 + patchwork/templatetags/listurl.py | 136 ++++ patchwork/templatetags/order.py | 66 ++ patchwork/templatetags/patch.py | 65 ++ patchwork/templatetags/person.py | 43 ++ patchwork/templatetags/pwurl.py | 76 +++ patchwork/templatetags/syntax.py | 75 +++ patchwork/tests/__init__.py | 34 + patchwork/tests/mail/0001-git-pull-request.mbox | 348 ++++++++++ .../tests/mail/0002-git-pull-request-wrapped.mbox | 349 ++++++++++ .../mail/0003-git-pull-request-with-diff.mbox | 141 ++++ .../tests/mail/0004-git-pull-request-git+ssh.mbox | 348 ++++++++++ .../tests/mail/0005-git-pull-request-ssh.mbox | 348 ++++++++++ .../tests/mail/0006-git-pull-request-http.mbox | 348 ++++++++++ patchwork/tests/mail/0007-cvs-format-diff.mbox | 134 ++++ patchwork/tests/mail/0008-git-rename.mbox | 24 + .../tests/mail/0009-git-rename-with-diff.mbox | 32 + patchwork/tests/mail/0010-invalid-charset.mbox | 90 +++ .../tests/mail/0011-no-newline-at-end-of-file.mbox | 45 ++ patchwork/tests/patches/0001-add-line.patch | 7 + patchwork/tests/patches/0002-utf-8.patch | 7 + patchwork/tests/test_bundles.py | 646 ++++++++++++++++++ patchwork/tests/test_confirm.py | 67 ++ patchwork/tests/test_encodings.py | 87 +++ patchwork/tests/test_expiry.py | 121 ++++ patchwork/tests/test_filters.py | 45 ++ patchwork/tests/test_list.py | 116 ++++ patchwork/tests/test_mail_settings.py | 299 +++++++++ patchwork/tests/test_mboxviews.py | 209 ++++++ patchwork/tests/test_notifications.py | 255 +++++++ patchwork/tests/test_patchparser.py | 554 +++++++++++++++ patchwork/tests/test_person.py | 55 ++ patchwork/tests/test_registration.py | 210 ++++++ patchwork/tests/test_updates.py | 118 ++++ patchwork/tests/test_user.py | 195 ++++++ patchwork/tests/test_xmlrpc.py | 55 ++ patchwork/tests/utils.py | 138 ++++ patchwork/urls.py | 103 +++ patchwork/utils.py | 248 +++++++ patchwork/views/__init__.py | 220 ++++++ patchwork/views/base.py | 122 ++++ patchwork/views/bundle.py | 221 ++++++ patchwork/views/mail.py | 119 ++++ patchwork/views/patch.py | 107 +++ patchwork/views/project.py | 38 ++ patchwork/views/user.py | 216 ++++++ patchwork/views/xmlrpc.py | 450 +++++++++++++ 110 files changed, 12804 insertions(+) create mode 100644 patchwork/__init__.py create mode 100644 patchwork/admin.py create mode 100644 patchwork/bin/__init__.py create mode 100644 patchwork/bin/bash_completion create mode 100755 patchwork/bin/parsemail-batch.sh create mode 100755 patchwork/bin/parsemail.py create mode 100755 patchwork/bin/parsemail.sh create mode 100755 patchwork/bin/patchwork-cron.py create mode 100755 patchwork/bin/pwclient create mode 100755 patchwork/bin/rehash.py create mode 100755 patchwork/bin/update-patchwork-status.py create mode 100644 patchwork/context_processors.py create mode 100644 patchwork/filters.py create mode 100644 patchwork/fixtures/default_projects.xml create mode 100644 patchwork/fixtures/initial_data.xml create mode 100644 patchwork/forms.py create mode 100644 patchwork/models.py create mode 100644 patchwork/paginator.py create mode 100644 patchwork/parser.py create mode 100644 patchwork/requestcontext.py create mode 100644 patchwork/settings/__init__.py create mode 100644 patchwork/settings/base.py create mode 100644 patchwork/settings/dev.py create mode 100644 patchwork/settings/prod.py create mode 100644 patchwork/templates/patchwork/activation_email.txt create mode 100644 patchwork/templates/patchwork/activation_email_subject.txt create mode 100644 patchwork/templates/patchwork/bundle.html create mode 100644 patchwork/templates/patchwork/bundles.html create mode 100644 patchwork/templates/patchwork/confirm-error.html create mode 100644 patchwork/templates/patchwork/filters.html create mode 100644 patchwork/templates/patchwork/help/about.html create mode 100644 patchwork/templates/patchwork/help/index.html create mode 100644 patchwork/templates/patchwork/help/pwclient.html create mode 100644 patchwork/templates/patchwork/list.html create mode 100644 patchwork/templates/patchwork/login.html create mode 100644 patchwork/templates/patchwork/logout.html create mode 100644 patchwork/templates/patchwork/mail-form.html create mode 100644 patchwork/templates/patchwork/mail-settings.html create mode 100644 patchwork/templates/patchwork/optin-request.html create mode 100644 patchwork/templates/patchwork/optin-request.mail create mode 100644 patchwork/templates/patchwork/optin.html create mode 100644 patchwork/templates/patchwork/optout-request.html create mode 100644 patchwork/templates/patchwork/optout-request.mail create mode 100644 patchwork/templates/patchwork/optout.html create mode 100644 patchwork/templates/patchwork/pagination.html create mode 100644 patchwork/templates/patchwork/patch-change-notification-subject.text create mode 100644 patchwork/templates/patchwork/patch-change-notification.mail create mode 100644 patchwork/templates/patchwork/patch-list.html create mode 100644 patchwork/templates/patchwork/patch.html create mode 100644 patchwork/templates/patchwork/profile.html create mode 100644 patchwork/templates/patchwork/project.html create mode 100644 patchwork/templates/patchwork/projects.html create mode 120000 patchwork/templates/patchwork/pwclient create mode 100644 patchwork/templates/patchwork/pwclientrc create mode 100644 patchwork/templates/patchwork/register.mail create mode 100644 patchwork/templates/patchwork/registration-confirm.html create mode 100644 patchwork/templates/patchwork/registration_form.html create mode 100644 patchwork/templates/patchwork/todo-list.html create mode 100644 patchwork/templates/patchwork/todo-lists.html create mode 100644 patchwork/templates/patchwork/user-link-confirm.html create mode 100644 patchwork/templates/patchwork/user-link.html create mode 100644 patchwork/templates/patchwork/user-link.mail create mode 100644 patchwork/templatetags/__init__.py create mode 100644 patchwork/templatetags/filter.py create mode 100644 patchwork/templatetags/listurl.py create mode 100644 patchwork/templatetags/order.py create mode 100644 patchwork/templatetags/patch.py create mode 100644 patchwork/templatetags/person.py create mode 100644 patchwork/templatetags/pwurl.py create mode 100644 patchwork/templatetags/syntax.py create mode 100644 patchwork/tests/__init__.py create mode 100644 patchwork/tests/mail/0001-git-pull-request.mbox create mode 100644 patchwork/tests/mail/0002-git-pull-request-wrapped.mbox create mode 100644 patchwork/tests/mail/0003-git-pull-request-with-diff.mbox create mode 100644 patchwork/tests/mail/0004-git-pull-request-git+ssh.mbox create mode 100644 patchwork/tests/mail/0005-git-pull-request-ssh.mbox create mode 100644 patchwork/tests/mail/0006-git-pull-request-http.mbox create mode 100644 patchwork/tests/mail/0007-cvs-format-diff.mbox create mode 100644 patchwork/tests/mail/0008-git-rename.mbox create mode 100644 patchwork/tests/mail/0009-git-rename-with-diff.mbox create mode 100644 patchwork/tests/mail/0010-invalid-charset.mbox create mode 100644 patchwork/tests/mail/0011-no-newline-at-end-of-file.mbox create mode 100644 patchwork/tests/patches/0001-add-line.patch create mode 100644 patchwork/tests/patches/0002-utf-8.patch create mode 100644 patchwork/tests/test_bundles.py create mode 100644 patchwork/tests/test_confirm.py create mode 100644 patchwork/tests/test_encodings.py create mode 100644 patchwork/tests/test_expiry.py create mode 100644 patchwork/tests/test_filters.py create mode 100644 patchwork/tests/test_list.py create mode 100644 patchwork/tests/test_mail_settings.py create mode 100644 patchwork/tests/test_mboxviews.py create mode 100644 patchwork/tests/test_notifications.py create mode 100644 patchwork/tests/test_patchparser.py create mode 100644 patchwork/tests/test_person.py create mode 100644 patchwork/tests/test_registration.py create mode 100644 patchwork/tests/test_updates.py create mode 100644 patchwork/tests/test_user.py create mode 100644 patchwork/tests/test_xmlrpc.py create mode 100644 patchwork/tests/utils.py create mode 100644 patchwork/urls.py create mode 100644 patchwork/utils.py create mode 100644 patchwork/views/__init__.py create mode 100644 patchwork/views/base.py create mode 100644 patchwork/views/bundle.py create mode 100644 patchwork/views/mail.py create mode 100644 patchwork/views/patch.py create mode 100644 patchwork/views/project.py create mode 100644 patchwork/views/user.py create mode 100644 patchwork/views/xmlrpc.py (limited to 'patchwork') diff --git a/patchwork/__init__.py b/patchwork/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/patchwork/admin.py b/patchwork/admin.py new file mode 100644 index 0000000..5297903 --- /dev/null +++ b/patchwork/admin.py @@ -0,0 +1,50 @@ +from django.contrib import admin +from patchwork.models import Project, Person, UserProfile, State, Patch, \ + Comment, Bundle + +class ProjectAdmin(admin.ModelAdmin): + list_display = ('name', 'linkname','listid', 'listemail') +admin.site.register(Project, ProjectAdmin) + +class PersonAdmin(admin.ModelAdmin): + list_display = ('__unicode__', 'has_account') + search_fields = ('name', 'email') + def has_account(self, person): + return bool(person.user) + has_account.boolean = True + has_account.admin_order_field = 'user' + has_account.short_description = 'Account' +admin.site.register(Person, PersonAdmin) + +class UserProfileAdmin(admin.ModelAdmin): + search_fields = ('user__username', 'user__first_name', 'user__last_name') +admin.site.register(UserProfile, UserProfileAdmin) + +class StateAdmin(admin.ModelAdmin): + list_display = ('name', 'action_required') +admin.site.register(State, StateAdmin) + +class PatchAdmin(admin.ModelAdmin): + list_display = ('name', 'submitter', 'project', 'state', 'date', + 'archived', 'is_pull_request') + list_filter = ('project', 'state', 'archived') + search_fields = ('name', 'submitter__name', 'submitter__email') + date_hierarchy = 'date' + def is_pull_request(self, patch): + return bool(patch.pull_url) + is_pull_request.boolean = True + is_pull_request.admin_order_field = 'pull_url' + is_pull_request.short_description = 'Pull' +admin.site.register(Patch, PatchAdmin) + +class CommentAdmin(admin.ModelAdmin): + list_display = ('patch', 'submitter', 'date') + search_fields = ('patch__name', 'submitter__name', 'submitter__email') + date_hierarchy = 'date' +admin.site.register(Comment, CommentAdmin) + +class BundleAdmin(admin.ModelAdmin): + list_display = ('name', 'owner', 'project', 'public') + list_filter = ('public', 'project') + search_fields = ('name', 'owner') +admin.site.register(Bundle, BundleAdmin) diff --git a/patchwork/bin/__init__.py b/patchwork/bin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/patchwork/bin/bash_completion b/patchwork/bin/bash_completion new file mode 100644 index 0000000..a120a76 --- /dev/null +++ b/patchwork/bin/bash_completion @@ -0,0 +1,29 @@ +# Autocompletion for bash. + +_pwclient() { + local cur prev words cword split + + if declare -f _init_completion >/dev/null; then + _init_completion -s || return + else + cur=$(_get_cword) + prev=${COMP_WORDS[COMP_CWORD-1]} + fi + + case "${COMP_CWORD}" in + 0|1) return 0;; + esac + + projects="$(sed -r -e '/\[options\]/d;' \ + -e '/^\[(.+)\]$/!d;' \ + -e 's//\1/;' ~/.pwclientrc 2>/dev/null)" + + case "${prev}" in + -p) COMPREPLY=( $(compgen -W "${projects}" -- "${cur}" ) );; + esac + + return 0 +} +complete -F _pwclient pwclient + +# vim: ft=sh diff --git a/patchwork/bin/parsemail-batch.sh b/patchwork/bin/parsemail-batch.sh new file mode 100755 index 0000000..31ef4f0 --- /dev/null +++ b/patchwork/bin/parsemail-batch.sh @@ -0,0 +1,45 @@ +#!/bin/sh +# +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +PATCHWORK_BINDIR=`dirname $0` + +if [ $# -ne 1 ] +then + echo "usage: $0 " >&2 + exit 1 +fi + +mail_dir="$1" + +echo "dir: $mail_dir" + +if [ ! -d "$mail_dir" ] +then + echo "$mail_dir should be a directory"? >&2 + exit 1 +fi + +ls -1rt "$mail_dir" | +while read line; +do + echo $line + $PATCHWORK_BINDIR/parsemail.sh < "$mail_dir/$line" +done diff --git a/patchwork/bin/parsemail.py b/patchwork/bin/parsemail.py new file mode 100755 index 0000000..19e6e57 --- /dev/null +++ b/patchwork/bin/parsemail.py @@ -0,0 +1,455 @@ +#!/usr/bin/env python +# +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import sys +import re +import datetime +import time +import operator +import codecs +from email import message_from_file +try: + from email.header import Header, decode_header + from email.utils import parsedate_tz, mktime_tz +except ImportError: + # Python 2.4 compatibility + from email.Header import Header, decode_header + from email.Utils import parsedate_tz, mktime_tz + +from patchwork.parser import parse_patch +from patchwork.models import Patch, Project, Person, Comment, State, \ + get_default_initial_patch_state +from django.contrib.auth.models import User + +list_id_headers = ['List-ID', 'X-Mailing-List', 'X-list'] + +whitespace_re = re.compile('\s+') +def normalise_space(str): + return whitespace_re.sub(' ', str).strip() + +def clean_header(header): + """ Decode (possibly non-ascii) headers """ + + def decode(fragment): + (frag_str, frag_encoding) = fragment + if frag_encoding: + return frag_str.decode(frag_encoding) + return frag_str.decode() + + fragments = map(decode, decode_header(header)) + + return normalise_space(u' '.join(fragments)) + +def find_project(mail): + project = None + listid_res = [re.compile('.*<([^>]+)>.*', re.S), + re.compile('^([\S]+)$', re.S)] + + for header in list_id_headers: + if header in mail: + + for listid_re in listid_res: + match = listid_re.match(mail.get(header)) + if match: + break + + if not match: + continue + + listid = match.group(1) + + try: + project = Project.objects.get(listid = listid) + break + except: + pass + + return project + +def find_author(mail): + + from_header = clean_header(mail.get('From')) + (name, email) = (None, None) + + # tuple of (regex, fn) + # - where fn returns a (name, email) tuple from the match groups resulting + # from re.match().groups() + from_res = [ + # for "Firstname Lastname" style addresses + (re.compile('"?(.*?)"?\s*<([^>]+)>'), (lambda g: (g[0], g[1]))), + + # for example@example.com (Firstname Lastname) style addresses + (re.compile('"?(.*?)"?\s*\(([^\)]+)\)'), (lambda g: (g[1], g[0]))), + + # everything else + (re.compile('(.*)'), (lambda g: (None, g[0]))), + ] + + for regex, fn in from_res: + match = regex.match(from_header) + if match: + (name, email) = fn(match.groups()) + break + + if email is None: + raise Exception("Could not parse From: header") + + email = email.strip() + if name is not None: + name = name.strip() + + new_person = False + + try: + person = Person.objects.get(email__iexact = email) + except Person.DoesNotExist: + person = Person(name = name, email = email) + new_person = True + + return (person, new_person) + +def mail_date(mail): + t = parsedate_tz(mail.get('Date', '')) + if not t: + return datetime.datetime.utcnow() + return datetime.datetime.utcfromtimestamp(mktime_tz(t)) + +def mail_headers(mail): + return reduce(operator.__concat__, + ['%s: %s\n' % (k, Header(v, header_name = k, \ + continuation_ws = '\t').encode()) \ + for (k, v) in mail.items()]) + +def find_pull_request(content): + git_re = re.compile('^The following changes since commit.*' + + '^are available in the git repository at:\n' + '^\s*([\S]+://[^\n]+)$', + re.DOTALL | re.MULTILINE) + match = git_re.search(content) + if match: + return match.group(1) + return None + +def try_decode(payload, charset): + try: + payload = unicode(payload, charset) + except UnicodeDecodeError: + return None + return payload + +def find_content(project, mail): + patchbuf = None + commentbuf = '' + pullurl = None + + for part in mail.walk(): + if part.get_content_maintype() != 'text': + continue + + payload = part.get_payload(decode=True) + subtype = part.get_content_subtype() + + if not isinstance(payload, unicode): + charset = part.get_content_charset() + + # Check that we have a charset that we understand. Otherwise, + # ignore it and fallback to our standard set. + if charset is not None: + try: + codec = codecs.lookup(charset) + except LookupError: + charset = None + + # If there is no charset or if it is unknown, then try some common + # charsets before we fail. + if charset is None: + try_charsets = ['utf-8', 'windows-1252', 'iso-8859-1'] + else: + try_charsets = [charset] + + for cset in try_charsets: + decoded_payload = try_decode(payload, cset) + if decoded_payload is not None: + break + payload = decoded_payload + + # Could not find a valid decoded payload. Fail. + if payload is None: + return (None, None) + + if subtype in ['x-patch', 'x-diff']: + patchbuf = payload + + elif subtype == 'plain': + c = payload + + if not patchbuf: + (patchbuf, c) = parse_patch(payload) + + if not pullurl: + pullurl = find_pull_request(payload) + + if c is not None: + commentbuf += c.strip() + '\n' + + patch = None + comment = None + + if pullurl or patchbuf: + name = clean_subject(mail.get('Subject'), [project.linkname]) + patch = Patch(name = name, pull_url = pullurl, content = patchbuf, + date = mail_date(mail), headers = mail_headers(mail)) + + if commentbuf: + if patch: + cpatch = patch + else: + cpatch = find_patch_for_comment(project, mail) + if not cpatch: + return (None, None) + comment = Comment(patch = cpatch, date = mail_date(mail), + content = clean_content(commentbuf), + headers = mail_headers(mail)) + + return (patch, comment) + +def find_patch_for_comment(project, mail): + # construct a list of possible reply message ids + refs = [] + if 'In-Reply-To' in mail: + refs.append(mail.get('In-Reply-To')) + + if 'References' in mail: + rs = mail.get('References').split() + rs.reverse() + for r in rs: + if r not in refs: + refs.append(r) + + for ref in refs: + patch = None + + # first, check for a direct reply + try: + patch = Patch.objects.get(project = project, msgid = ref) + return patch + except Patch.DoesNotExist: + pass + + # see if we have comments that refer to a patch + try: + comment = Comment.objects.get(patch__project = project, msgid = ref) + return comment.patch + except Comment.DoesNotExist: + pass + + + return None + +split_re = re.compile('[,\s]+') + +def split_prefixes(prefix): + """ Turn a prefix string into a list of prefix tokens + + >>> split_prefixes('PATCH') + ['PATCH'] + >>> split_prefixes('PATCH,RFC') + ['PATCH', 'RFC'] + >>> split_prefixes('') + [] + >>> split_prefixes('PATCH,') + ['PATCH'] + >>> split_prefixes('PATCH ') + ['PATCH'] + >>> split_prefixes('PATCH,RFC') + ['PATCH', 'RFC'] + >>> split_prefixes('PATCH 1/2') + ['PATCH', '1/2'] + """ + matches = split_re.split(prefix) + return [ s for s in matches if s != '' ] + +re_re = re.compile('^(re|fwd?)[:\s]\s*', re.I) +prefix_re = re.compile('^\[([^\]]*)\]\s*(.*)$') + +def clean_subject(subject, drop_prefixes = None): + """ Clean a Subject: header from an incoming patch. + + Removes Re: and Fwd: strings, as well as [PATCH]-style prefixes. By + default, only [PATCH] is removed, and we keep any other bracketed data + in the subject. If drop_prefixes is provided, remove those too, + comparing case-insensitively. + + >>> clean_subject('meep') + 'meep' + >>> clean_subject('Re: meep') + 'meep' + >>> clean_subject('[PATCH] meep') + 'meep' + >>> clean_subject('[PATCH] meep \\n meep') + 'meep meep' + >>> clean_subject('[PATCH RFC] meep') + '[RFC] meep' + >>> clean_subject('[PATCH,RFC] meep') + '[RFC] meep' + >>> clean_subject('[PATCH,1/2] meep') + '[1/2] meep' + >>> clean_subject('[PATCH RFC 1/2] meep') + '[RFC,1/2] meep' + >>> clean_subject('[PATCH] [RFC] meep') + '[RFC] meep' + >>> clean_subject('[PATCH] [RFC,1/2] meep') + '[RFC,1/2] meep' + >>> clean_subject('[PATCH] [RFC] [1/2] meep') + '[RFC,1/2] meep' + >>> clean_subject('[PATCH] rewrite [a-z] regexes') + 'rewrite [a-z] regexes' + >>> clean_subject('[PATCH] [RFC] rewrite [a-z] regexes') + '[RFC] rewrite [a-z] regexes' + >>> clean_subject('[foo] [bar] meep', ['foo']) + '[bar] meep' + >>> clean_subject('[FOO] [bar] meep', ['foo']) + '[bar] meep' + """ + + subject = clean_header(subject) + + if drop_prefixes is None: + drop_prefixes = [] + else: + drop_prefixes = [ s.lower() for s in drop_prefixes ] + + drop_prefixes.append('patch') + + # remove Re:, Fwd:, etc + subject = re_re.sub(' ', subject) + + subject = normalise_space(subject) + + prefixes = [] + + match = prefix_re.match(subject) + + while match: + prefix_str = match.group(1) + prefixes += [ p for p in split_prefixes(prefix_str) \ + if p.lower() not in drop_prefixes] + + subject = match.group(2) + match = prefix_re.match(subject) + + subject = normalise_space(subject) + + subject = subject.strip() + if prefixes: + subject = '[%s] %s' % (','.join(prefixes), subject) + + return subject + +sig_re = re.compile('^(-- |_+)\n.*', re.S | re.M) +def clean_content(str): + """ Try to remove signature (-- ) and list footer (_____) cruft """ + str = sig_re.sub('', str) + return str.strip() + +def get_state(state_name): + """ Return the state with the given name or the default State """ + if state_name: + try: + return State.objects.get(name__iexact=state_name) + except State.DoesNotExist: + pass + return get_default_initial_patch_state() + +def get_delegate(delegate_email): + """ Return the delegate with the given email or None """ + if delegate_email: + try: + return User.objects.get(email__iexact=delegate_email) + except User.DoesNotExist: + pass + return None + +def parse_mail(mail): + + # some basic sanity checks + if 'From' not in mail: + return 0 + + if 'Subject' not in mail: + return 0 + + if 'Message-Id' not in mail: + return 0 + + hint = mail.get('X-Patchwork-Hint', '').lower() + if hint == 'ignore': + return 0; + + project = find_project(mail) + if project is None: + print "no project found" + return 0 + + msgid = mail.get('Message-Id').strip() + + (author, save_required) = find_author(mail) + + (patch, comment) = find_content(project, mail) + + if patch: + # we delay the saving until we know we have a patch. + if save_required: + author.save() + save_required = False + patch.submitter = author + patch.msgid = msgid + patch.project = project + patch.state = get_state(mail.get('X-Patchwork-State', '').strip()) + patch.delegate = get_delegate( + mail.get('X-Patchwork-Delegate', '').strip()) + try: + patch.save() + except Exception, ex: + print str(ex) + + if comment: + if save_required: + author.save() + # looks like the original constructor for Comment takes the pk + # when the Comment is created. reset it here. + if patch: + comment.patch = patch + comment.submitter = author + comment.msgid = msgid + try: + comment.save() + except Exception, ex: + print str(ex) + + return 0 + +def main(args): + mail = message_from_file(sys.stdin) + return parse_mail(mail) + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/patchwork/bin/parsemail.sh b/patchwork/bin/parsemail.sh new file mode 100755 index 0000000..d9ad005 --- /dev/null +++ b/patchwork/bin/parsemail.sh @@ -0,0 +1,29 @@ +#!/bin/sh +# +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +BIN_DIR=`dirname $0` +PATCHWORK_BASE=`readlink -e $BIN_DIR/../..` + +PYTHONPATH="$PATCHWORK_BASE":"$PATCHWORK_BASE/lib/python:$PYTHONPATH" \ + DJANGO_SETTINGS_MODULE=settings \ + "$PATCHWORK_BASE/patchwork/bin/parsemail.py" + +exit 0 diff --git a/patchwork/bin/patchwork-cron.py b/patchwork/bin/patchwork-cron.py new file mode 100755 index 0000000..148e97c --- /dev/null +++ b/patchwork/bin/patchwork-cron.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +import sys +from patchwork.utils import send_notifications, do_expiry + +def main(args): + errors = send_notifications() + for (recipient, error) in errors: + print "Failed sending to %s: %s" % (recipient.email, ex) + + do_expiry() + +if __name__ == '__main__': + sys.exit(main(sys.argv)) + diff --git a/patchwork/bin/pwclient b/patchwork/bin/pwclient new file mode 100755 index 0000000..8d1f476 --- /dev/null +++ b/patchwork/bin/pwclient @@ -0,0 +1,744 @@ +#!/usr/bin/env python +# +# Patchwork command line client +# Copyright (C) 2008 Nate Case +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os +import sys +import xmlrpclib +import argparse +import string +import tempfile +import subprocess +import base64 +import ConfigParser +import shutil +import re + +# Default Patchwork remote XML-RPC server URL +# This script will check the PW_XMLRPC_URL environment variable +# for the URL to access. If that is unspecified, it will fallback to +# the hardcoded default value specified here. +DEFAULT_URL = "http://patchwork/xmlrpc/" +CONFIG_FILE = os.path.expanduser('~/.pwclientrc') + +class Filter: + """Filter for selecting patches.""" + def __init__(self): + # These fields refer to specific objects, so they are special + # because we have to resolve them to IDs before passing the + # filter to the server + self.state = "" + self.project = "" + + # The dictionary that gets passed to via XML-RPC + self.d = {} + + def add(self, field, value): + if field == 'state': + self.state = value + elif field == 'project': + self.project = value + else: + # OK to add directly + self.d[field] = value + + def resolve_ids(self, rpc): + """Resolve State, Project, and Person IDs based on filter strings.""" + if self.state != "": + id = state_id_by_name(rpc, self.state) + if id == 0: + sys.stderr.write("Note: No State found matching %s*, " \ + "ignoring filter\n" % self.state) + else: + self.d['state_id'] = id + + if self.project != None: + id = project_id_by_name(rpc, self.project) + if id == 0: + sys.stderr.write("Note: No Project found matching %s, " \ + "ignoring filter\n" % self.project) + else: + self.d['project_id'] = id + + def __str__(self): + """Return human-readable description of the filter.""" + return str(self.d) + +class BasicHTTPAuthTransport(xmlrpclib.SafeTransport): + + def __init__(self, username = None, password = None, use_https = False): + self.username = username + self.password = password + self.use_https = use_https + xmlrpclib.SafeTransport.__init__(self) + + def authenticated(self): + return self.username != None and self.password != None + + def send_host(self, connection, host): + xmlrpclib.Transport.send_host(self, connection, host) + if not self.authenticated(): + return + credentials = '%s:%s' % (self.username, self.password) + auth = 'Basic ' + base64.encodestring(credentials).strip() + connection.putheader('Authorization', auth) + + def make_connection(self, host): + if self.use_https: + fn = xmlrpclib.SafeTransport.make_connection + else: + fn = xmlrpclib.Transport.make_connection + return fn(self, host) + +def project_id_by_name(rpc, linkname): + """Given a project short name, look up the Project ID.""" + if len(linkname) == 0: + return 0 + projects = rpc.project_list(linkname, 0) + for project in projects: + if project['linkname'] == linkname: + return project['id'] + return 0 + +def state_id_by_name(rpc, name): + """Given a partial state name, look up the state ID.""" + if len(name) == 0: + return 0 + states = rpc.state_list(name, 0) + for state in states: + if state['name'].lower().startswith(name.lower()): + return state['id'] + return 0 + +def person_ids_by_name(rpc, name): + """Given a partial name or email address, return a list of the + person IDs that match.""" + if len(name) == 0: + return [] + people = rpc.person_list(name, 0) + return map(lambda x: x['id'], people) + +def list_patches(patches, format_str=None): + """Dump a list of patches to stdout.""" + if format_str: + format_field_re = re.compile("%{([a-z0-9_]+)}") + + def patch_field(matchobj): + fieldname = matchobj.group(1) + + if fieldname == "_msgid_": + # naive way to strip < and > from message-id + val = string.strip(str(patch["msgid"]), "<>") + else: + val = str(patch[fieldname]) + + return val + + for patch in patches: + print(format_field_re.sub(patch_field, format_str)) + else: + print("%-7s %-12s %s" % ("ID", "State", "Name")) + print("%-7s %-12s %s" % ("--", "-----", "----")) + for patch in patches: + print("%-7d %-12s %s" % (patch['id'], patch['state'], patch['name'])) + +def action_list(rpc, filter, submitter_str, delegate_str, format_str=None): + filter.resolve_ids(rpc) + + if submitter_str != None: + ids = person_ids_by_name(rpc, submitter_str) + if len(ids) == 0: + sys.stderr.write("Note: Nobody found matching *%s*\n" % \ + submitter_str) + else: + for id in ids: + person = rpc.person_get(id) + print "Patches submitted by %s <%s>:" % \ + (unicode(person['name']).encode("utf-8"), \ + unicode(person['email']).encode("utf-8")) + f = filter + f.add("submitter_id", id) + patches = rpc.patch_list(f.d) + list_patches(patches, format_str) + return + + if delegate_str != None: + ids = person_ids_by_name(rpc, delegate_str) + if len(ids) == 0: + sys.stderr.write("Note: Nobody found matching *%s*\n" % \ + delegate_str) + else: + for id in ids: + person = rpc.person_get(id) + print "Patches delegated to %s <%s>:" % \ + (person['name'], person['email']) + f = filter + f.add("delegate_id", id) + patches = rpc.patch_list(f.d) + list_patches(patches, format_str) + return + + patches = rpc.patch_list(filter.d) + list_patches(patches, format_str) + +def action_projects(rpc): + projects = rpc.project_list("", 0) + print("%-5s %-24s %s" % ("ID", "Name", "Description")) + print("%-5s %-24s %s" % ("--", "----", "-----------")) + for project in projects: + print("%-5d %-24s %s" % (project['id'], \ + project['linkname'], \ + project['name'])) + +def action_states(rpc): + states = rpc.state_list("", 0) + print("%-5s %s" % ("ID", "Name")) + print("%-5s %s" % ("--", "----")) + for state in states: + print("%-5d %s" % (state['id'], state['name'])) + +def action_info(rpc, patch_id): + patch = rpc.patch_get(patch_id) + s = "Information for patch id %d" % (patch_id) + print(s) + print('-' * len(s)) + for key, value in sorted(patch.iteritems()): + print("- %- 14s: %s" % (key, unicode(value).encode("utf-8"))) + +def action_get(rpc, patch_id): + patch = rpc.patch_get(patch_id) + s = rpc.patch_get_mbox(patch_id) + + if patch == {} or len(s) == 0: + sys.stderr.write("Unable to get patch %d\n" % patch_id) + sys.exit(1) + + base_fname = fname = os.path.basename(patch['filename']) + i = 0 + while os.path.exists(fname): + fname = "%s.%d" % (base_fname, i) + i += 1 + + try: + f = open(fname, "w") + except: + sys.stderr.write("Unable to open %s for writing\n" % fname) + sys.exit(1) + + try: + f.write(unicode(s).encode("utf-8")) + f.close() + print "Saved patch to %s" % fname + except: + sys.stderr.write("Failed to write to %s\n" % fname) + sys.exit(1) + +def action_apply(rpc, patch_id, apply_cmd=None): + patch = rpc.patch_get(patch_id) + if patch == {}: + sys.stderr.write("Error getting information on patch ID %d\n" % \ + patch_id) + sys.exit(1) + + if apply_cmd is None: + print "Applying patch #%d to current directory" % patch_id + apply_cmd = ['patch', '-p1'] + else: + print "Applying patch #%d using %s" % ( + patch_id, repr(' '.join(apply_cmd))) + + print "Description: %s" % patch['name'] + s = rpc.patch_get_mbox(patch_id) + if len(s) > 0: + proc = subprocess.Popen(apply_cmd, stdin = subprocess.PIPE) + proc.communicate(unicode(s).encode('utf-8')) + return proc.returncode + else: + sys.stderr.write("Error: No patch content found\n") + sys.exit(1) + +def action_update_patch(rpc, patch_id, state = None, archived = None, commit = None): + patch = rpc.patch_get(patch_id) + if patch == {}: + sys.stderr.write("Error getting information on patch ID %d\n" % \ + patch_id) + sys.exit(1) + + params = {} + + if state: + state_id = state_id_by_name(rpc, state) + if state_id == 0: + sys.stderr.write("Error: No State found matching %s*\n" % state) + sys.exit(1) + params['state'] = state_id + + if commit: + params['commit_ref'] = commit + + if archived: + params['archived'] = archived == 'yes' + + success = False + try: + success = rpc.patch_set(patch_id, params) + except xmlrpclib.Fault, f: + sys.stderr.write("Error updating patch: %s\n" % f.faultString) + + if not success: + sys.stderr.write("Patch not updated\n") + +def patch_id_from_hash(rpc, project, hash): + try: + patch = rpc.patch_get_by_project_hash(project, hash) + except xmlrpclib.Fault: + # the server may not have the newer patch_get_by_project_hash function, + # so fall back to hash-only. + patch = rpc.patch_get_by_hash(hash) + + if patch == {}: + sys.stderr.write("No patch has the hash provided\n") + sys.exit(1) + + patch_id = patch['id'] + # be super paranoid + try: + patch_id = int(patch_id) + except: + sys.stderr.write("Invalid patch ID obtained from server\n") + sys.exit(1) + return patch_id + +auth_actions = ['update'] + +# unfortunately we currently have to revert to this ugly hack.. +class _RecursiveHelpAction(argparse._HelpAction): + + def __call__(self, parser, namespace, values, option_string=None): + parser.print_help() + print + + subparsers_actions = [ + action for action in parser._actions + if isinstance(action, argparse._SubParsersAction) + ] + hash_n_id_actions = set(['hash', 'id', 'help']) + for subparsers_action in subparsers_actions: + for choice, subparser in subparsers_action.choices.items(): + # gross but the whole thing is.. + if (len(subparser._actions) == 3 \ + and set([a.dest for a in subparser._actions]) \ + == hash_n_id_actions) \ + or len(subparser._actions) == 0: + continue + print("command '{}'".format(choice)) + print(subparser.format_help()) + + parser.exit() + +def main(): + hash_parser = argparse.ArgumentParser(add_help=False, version=False) + hash_parser.add_argument( + '-h', metavar='HASH', dest='hash', action='store', + help='''Lookup by patch hash''' + ) + hash_parser.add_argument( + 'id', metavar='ID', nargs='*', action='store', type=int, + help='Patch ID', + ) + hash_parser.add_argument( + '-p', metavar='PROJECT', + help='''Lookup patch in project''' + ) + + filter_parser = argparse.ArgumentParser(add_help=False, version=False) + filter_parser.add_argument( + '-s', metavar='STATE', + help='''Filter by patch state (e.g., 'New', 'Accepted', etc.)''' + ) + filter_parser.add_argument( + '-a', choices=['yes','no'], + help='''Filter by patch archived state''' + ) + filter_parser.add_argument( + '-p', metavar='PROJECT', + help='''Filter by project name (see 'projects' for list)''' + ) + filter_parser.add_argument( + '-w', metavar='WHO', + help='''Filter by submitter (name, e-mail substring search)''' + ) + filter_parser.add_argument( + '-d', metavar='WHO', + help='''Filter by delegate (name, e-mail substring search)''' + ) + filter_parser.add_argument( + '-n', metavar='MAX#', + type=int, + help='''Restrict number of results''' + ) + filter_parser.add_argument( + '-m', metavar='MESSAGEID', + help='''Filter by Message-Id''' + ) + filter_parser.add_argument( + '-f', metavar='FORMAT', + help='''Print output in the given format. You can use tags matching ''' + '''fields, e.g. %%{id}, %%{state}, or %%{msgid}.''' + ) + filter_parser.add_argument( + 'patch_name', metavar='STR', nargs='?', + help='substring to search for patches by name', + ) + help_parser = argparse.ArgumentParser(add_help=False, version=False) + help_parser.add_argument( + '--help', action='help', help=argparse.SUPPRESS, + #help='''show this help message and exit''' + ) + + action_parser = argparse.ArgumentParser( + prog='pwclient', + add_help=False, + version=False, + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog='''(apply | get | info | view | update) (-h HASH | ID [ID ...])''', + ) + action_parser.add_argument( + '--help', + #action='help', + action=_RecursiveHelpAction, + help='''Print this help text''' + ) + + subparsers = action_parser.add_subparsers( + title='Commands', + metavar='' + ) + apply_parser = subparsers.add_parser( + 'apply', parents=[hash_parser, help_parser], + add_help=False, + help='''Apply a patch (in the current dir, using -p1)''' + ) + apply_parser.set_defaults(subcmd='apply') + git_am_parser = subparsers.add_parser( + 'git-am', parents=[hash_parser, help_parser], + add_help=False, + help='''Apply a patch to current git branch using "git am".''' + ) + git_am_parser.set_defaults(subcmd='git_am') + git_am_parser.add_argument( + '-s', '--signoff', + action='store_true', + help='''pass --signoff to git-am''' + ) + get_parser = subparsers.add_parser( + 'get', parents=[hash_parser, help_parser], + add_help=False, + help='''Download a patch and save it locally''' + ) + get_parser.set_defaults(subcmd='get') + info_parser = subparsers.add_parser( + 'info', parents=[hash_parser, help_parser], + add_help=False, + help='''Display patchwork info about a given patch ID''' + ) + info_parser.set_defaults(subcmd='info') + projects_parser = subparsers.add_parser( + 'projects', + add_help=False, + help='''List all projects''' + ) + projects_parser.set_defaults(subcmd='projects') + states_parser = subparsers.add_parser( + 'states', + add_help=False, + help='''Show list of potential patch states''' + ) + states_parser.set_defaults(subcmd='states') + view_parser = subparsers.add_parser( + 'view', parents=[hash_parser, help_parser], + add_help=False, + help='''View a patch''' + ) + view_parser.set_defaults(subcmd='view') + update_parser = subparsers.add_parser( + 'update', parents=[hash_parser, help_parser], + add_help=False, + help='''Update patch''', + epilog='''Using a COMMIT-REF allows for only one ID to be specified''', + ) + update_parser.add_argument( + '-c', metavar='COMMIT-REF', + help='''commit reference hash''' + ) + update_parser.add_argument( + '-s', metavar='STATE', + required=True, + help='''Set patch state (e.g., 'Accepted', 'Superseded' etc.)''' + ) + update_parser.add_argument( + '-a', choices=['yes', 'no'], + help='''Set patch archived state''' + ) + update_parser.set_defaults(subcmd='update') + list_parser = subparsers.add_parser("list", + add_help=False, + #aliases=['search'], + parents=[filter_parser, help_parser], + help='''List patches, using the optional filters specified + below and an optional substring to search for patches + by name''' + ) + list_parser.set_defaults(subcmd='list') + search_parser = subparsers.add_parser("search", + add_help=False, + parents=[filter_parser, help_parser], + help='''Alias for "list"''' + ) + # Poor man's argparse aliases: + # We register the "search" parser but effectively use "list" for the + # help-text. + search_parser.set_defaults(subcmd='list') + if len(sys.argv) < 2: + action_parser.print_help() + sys.exit(0) + + args = action_parser.parse_args() + args = dict(vars(args)) + action = args.get('subcmd') + + if args.get('hash') and len(args.get('id')): + # mimic mutual exclusive group + sys.stderr.write("Error: [-h HASH] and [ID [ID ...]] " + + "are mutually exlusive\n") + locals()[action + '_parser'].print_help() + sys.exit(1) + + # set defaults + filt = Filter() + commit_str = None + url = DEFAULT_URL + + archived_str = args.get('a') + state_str = args.get('s') + project_str = args.get('p') + submitter_str = args.get('w') + delegate_str = args.get('d') + format_str = args.get('f') + hash_str = args.get('hash') + patch_ids = args.get('id') + msgid_str = args.get('m') + if args.get('c'): + # update multiple IDs with a single commit-hash does not make sense + if action == 'update' and patch_ids and len(patch_ids) > 1: + sys.stderr.write( + "Declining update with COMMIT-REF on multiple IDs\n" + ) + update_parser.print_help() + sys.exit(1) + commit_str = args.get('c') + + if args.get('n') != None: + try: + filt.add("max_count", args.get('n')) + except: + sys.stderr.write("Invalid maximum count '%s'\n" % args.get('n')) + action_parser.print_help() + sys.exit(1) + + do_signoff = args.get('signoff') + + # grab settings from config files + config = ConfigParser.ConfigParser() + config.read([CONFIG_FILE]) + + if not config.has_section('options'): + sys.stderr.write('~/.pwclientrc is in the old format. Migrating it...') + + old_project = config.get('base','project') + + new_config = ConfigParser.ConfigParser() + new_config.add_section('options') + + new_config.set('options','default',old_project) + new_config.add_section(old_project) + + new_config.set(old_project,'url',config.get('base','url')) + if config.has_option('auth', 'username'): + new_config.set(old_project,'username',config.get('auth','username')) + if config.has_option('auth', 'password'): + new_config.set(old_project,'password',config.get('auth','password')) + + old_config_file = CONFIG_FILE + '.orig' + shutil.copy2(CONFIG_FILE,old_config_file) + + with open(CONFIG_FILE, 'wb') as fd: + new_config.write(fd) + + sys.stderr.write(' Done.\n') + sys.stderr.write('Your old ~/.pwclientrc was saved to %s\n' % old_config_file) + sys.stderr.write('and was converted to the new format. You may want to\n') + sys.stderr.write('inspect it before continuing.\n') + sys.exit(1) + + if not project_str: + try: + project_str = config.get('options', 'default') + except: + sys.stderr.write("No default project configured in ~/.pwclientrc\n") + action_parser.print_help() + sys.exit(1) + + if not config.has_section(project_str): + sys.stderr.write("No section for project %s\n" % project_str) + sys.exit(1) + if not config.has_option(project_str, 'url'): + sys.stderr.write("No URL for project %s\n" % project_str) + sys.exit(1) + if not do_signoff and config.has_option('options', 'signoff'): + do_signoff = config.getboolean('options', 'signoff') + if not do_signoff and config.has_option(project_str, 'signoff'): + do_signoff = config.getboolean(project_str, 'signoff') + + url = config.get(project_str, 'url') + + transport = None + if action in auth_actions: + if config.has_option(project_str, 'username') and \ + config.has_option(project_str, 'password'): + + use_https = url.startswith('https') + + transport = BasicHTTPAuthTransport( \ + config.get(project_str, 'username'), + config.get(project_str, 'password'), + use_https) + + else: + sys.stderr.write(("The %s action requires authentication, " + "but no username or password\nis configured\n") % action) + sys.exit(1) + + if project_str: + filt.add("project", project_str) + + if state_str: + filt.add("state", state_str) + + if archived_str: + filt.add("archived", archived_str == 'yes') + + if msgid_str: + filt.add("msgid", msgid_str) + + try: + rpc = xmlrpclib.Server(url, transport = transport) + except: + sys.stderr.write("Unable to connect to %s\n" % url) + sys.exit(1) + + # It should be safe to assume hash_str is not zero, but who knows.. + if hash_str != None: + patch_ids = [patch_id_from_hash(rpc, project_str, hash_str)] + + # helper for non_empty() to print correct helptext + h = locals()[action + '_parser'] + + # Require either hash_str or IDs for + def non_empty(h, patch_ids): + """Error out if no patch IDs were specified""" + if patch_ids == None or len(patch_ids) < 1: + sys.stderr.write("Error: Missing Argument! " + + "Either [-h HASH] or [ID [ID ...]] are required\n") + if h: + h.print_help() + sys.exit(1) + return patch_ids + + if action == 'list' or action == 'search': + if args.get('patch_name') != None: + filt.add("name__icontains", args.get('patch_name')) + action_list(rpc, filt, submitter_str, delegate_str, format_str) + + elif action.startswith('project'): + action_projects(rpc) + + elif action.startswith('state'): + action_states(rpc) + + elif action == 'view': + pager = os.environ.get('PAGER') + if pager: + pager = subprocess.Popen( + pager.split(), stdin=subprocess.PIPE + ) + if pager: + i = list() + for patch_id in non_empty(h, patch_ids): + s = rpc.patch_get_mbox(patch_id) + if len(s) > 0: + i.append(unicode(s).encode("utf-8")) + if len(i) > 0: + pager.communicate(input="\n".join(i)) + pager.stdin.close() + else: + for patch_id in non_empty(h, patch_ids): + s = rpc.patch_get_mbox(patch_id) + if len(s) > 0: + print unicode(s).encode("utf-8") + + elif action == 'info': + for patch_id in non_empty(h, patch_ids): + action_info(rpc, patch_id) + + elif action == 'get': + for patch_id in non_empty(h, patch_ids): + action_get(rpc, patch_id) + + elif action == 'apply': + for patch_id in non_empty(h, patch_ids): + ret = action_apply(rpc, patch_id) + if ret: + sys.stderr.write("Apply failed with exit status %d\n" % ret) + sys.exit(1) + + elif action == 'git_am': + cmd = ['git', 'am'] + if do_signoff: + cmd.append('-s') + for patch_id in non_empty(h, patch_ids): + ret = action_apply(rpc, patch_id, cmd) + if ret: + sys.stderr.write("'git am' failed with exit status %d\n" % ret) + sys.exit(1) + + elif action == 'update': + for patch_id in non_empty(h, patch_ids): + action_update_patch(rpc, patch_id, state = state_str, + archived = archived_str, commit = commit_str + ) + + else: + sys.stderr.write("Unknown action '%s'\n" % action) + action_parser.print_help() + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/patchwork/bin/rehash.py b/patchwork/bin/rehash.py new file mode 100755 index 0000000..c44e49b --- /dev/null +++ b/patchwork/bin/rehash.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +# +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from patchwork.models import Patch +import sys + +if __name__ == '__main__': + if len(sys.argv) > 1: + patches = Patch.objects.filter(id__in = sys.argv[1:]) + else: + patches = Patch.objects.all() + + for patch in patches: + print patch.id, patch.name + patch.hash = None + patch.save() diff --git a/patchwork/bin/update-patchwork-status.py b/patchwork/bin/update-patchwork-status.py new file mode 100755 index 0000000..2da5d23 --- /dev/null +++ b/patchwork/bin/update-patchwork-status.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +import sys +import subprocess +from optparse import OptionParser + +def commits(options, revlist): + cmd = ['git', 'rev-list', revlist] + proc = subprocess.Popen(cmd, stdout = subprocess.PIPE, cwd = options.repodir) + + revs = [] + + for line in proc.stdout.readlines(): + revs.append(line.strip()) + + return revs + +def commit(options, rev): + cmd = ['git', 'diff', '%(rev)s^..%(rev)s' % {'rev': rev}] + proc = subprocess.Popen(cmd, stdout = subprocess.PIPE, cwd = options.repodir) + + buf = proc.communicate()[0] + + return buf + + +def main(args): + parser = OptionParser(usage = '%prog [options] revspec') + parser.add_option("-p", "--project", dest = "project", action = 'store', + help="use project PROJECT", metavar="PROJECT") + parser.add_option("-d", "--dir", dest = "repodir", action = 'store', + help="use git repo in DIR", metavar="DIR") + + (options, args) = parser.parse_args(args[1:]) + + if len(args) != 1: + parser.error("incorrect number of arguments") + + revspec = args[0] + revs = commits(options, revspec) + + for rev in revs: + print rev + print commit(options, rev) + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) + + diff --git a/patchwork/context_processors.py b/patchwork/context_processors.py new file mode 100644 index 0000000..f4ab5a9 --- /dev/null +++ b/patchwork/context_processors.py @@ -0,0 +1,32 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +from patchwork.models import Bundle +from patchwork.utils import order_map, get_order + +def bundle(request): + user = request.user + if not user.is_authenticated(): + return {} + return {'bundles': Bundle.objects.filter(owner = user)} + + +def patchlists(request): + diff --git a/patchwork/filters.py b/patchwork/filters.py new file mode 100644 index 0000000..8c9690e --- /dev/null +++ b/patchwork/filters.py @@ -0,0 +1,471 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +from patchwork.models import Person, State +from django.utils.safestring import mark_safe +from django.utils.html import escape +from django.contrib.auth.models import User +from urllib import quote + +class Filter(object): + def __init__(self, filters): + self.filters = filters + self.applied = False + self.forced = False + + def name(self): + """The 'name' of the filter, to be displayed in the filter UI""" + return self.name + + def condition(self): + """The current condition of the filter, to be displayed in the + filter UI""" + return self.key + + def key(self): + """The key for this filter, to appear in the querystring. A key of + None will remove the param=ley pair from the querystring.""" + return None + + def set_status(self, *kwargs): + """Views can call this to force a specific filter status. For example, + a user's todo page needs to setup the delegate filter to show + that user's delegated patches""" + pass + + def parse(self, dict): + if self.param not in dict.keys(): + return + self._set_key(dict[self.param]) + + def url_without_me(self): + return self.filters.querystring_without_filter(self) + + def form_function(self): + return 'function(form) { return "unimplemented" }' + + def form(self): + if self.forced: + return mark_safe('%s' % (self.param, + self.condition())) + return self.condition() + return self._form() + + def kwargs(self): + return {} + + def __str__(self): + return '%s: %s' % (self.name, self.kwargs()) + + +class SubmitterFilter(Filter): + param = 'submitter' + def __init__(self, filters): + super(SubmitterFilter, self).__init__(filters) + self.name = 'Submitter' + self.person = None + self.person_match = None + + def _set_key(self, str): + self.person = None + self.person_match = None + submitter_id = None + try: + submitter_id = int(str) + except ValueError: + pass + except: + return + + if submitter_id: + self.person = Person.objects.get(id = int(str)) + self.applied = True + return + + + people = Person.objects.filter(name__icontains = str) + + if not people: + return + + self.person_match = str + self.applied = True + + def kwargs(self): + if self.person: + user = self.person.user + if user: + return {'submitter__in': + Person.objects.filter(user = user).values('pk').query} + return {'submitter': self.person} + + if self.person_match: + return {'submitter__name__icontains': self.person_match} + return {} + + def condition(self): + if self.person: + return self.person.name + elif self.person_match: + return self.person_match + return '' + + def _form(self): + name = '' + if self.person: + name = self.person.name + return mark_safe((' ' % escape(name)) + + '') + + def key(self): + if self.person: + return self.person.id + return self.person_match + +class StateFilter(Filter): + param = 'state' + any_key = '*' + action_req_str = 'Action Required' + + def __init__(self, filters): + super(StateFilter, self).__init__(filters) + self.name = 'State' + self.state = None + self.applied = True + + def _set_key(self, str): + self.state = None + + if str == self.any_key: + self.applied = False + return + + try: + self.state = State.objects.get(id=int(str)) + except: + return + + self.applied = True + + def kwargs(self): + if self.state is not None: + return {'state': self.state} + else: + return {'state__in': \ + State.objects.filter(action_required = True) \ + .values('pk').query} + + def condition(self): + if self.state: + return self.state.name + return self.action_req_str + + def key(self): + if self.state is not None: + return self.state.id + if not self.applied: + return '*' + return None + + def _form(self): + str = '' + return mark_safe(str); + + def form_function(self): + return 'function(form) { return form.x.value }' + + def url_without_me(self): + qs = self.filters.querystring_without_filter(self) + if qs != '?': + qs += '&' + return qs + '%s=%s' % (self.param, self.any_key) + +class SearchFilter(Filter): + param = 'q' + def __init__(self, filters): + super(SearchFilter, self).__init__(filters) + self.name = 'Search' + self.param = 'q' + self.search = None + + def _set_key(self, str): + str = str.strip() + if str == '': + return + self.search = str + self.applied = True + + def kwargs(self): + return {'name__icontains': self.search} + + def condition(self): + return self.search + + def key(self): + return self.search + + def _form(self): + value = '' + if self.search: + value = escape(self.search) + return mark_safe('' %\ + (self.param, value)) + + def form_function(self): + return mark_safe('function(form) { return form.x.value }') + +class ArchiveFilter(Filter): + param = 'archive' + def __init__(self, filters): + super(ArchiveFilter, self).__init__(filters) + self.name = 'Archived' + self.archive_state = False + self.applied = True + self.param_map = { + True: 'true', + False: '', + None: 'both' + } + self.description_map = { + True: 'Yes', + False: 'No', + None: 'Both' + } + + def _set_key(self, str): + self.archive_state = False + self.applied = True + for (k, v) in self.param_map.iteritems(): + if str == v: + self.archive_state = k + if self.archive_state == None: + self.applied = False + + def kwargs(self): + if self.archive_state == None: + return {} + return {'archived': self.archive_state} + + def condition(self): + return self.description_map[self.archive_state] + + def key(self): + if self.archive_state == False: + return None + return self.param_map[self.archive_state] + + def _form(self): + s = '' + for b in [False, True, None]: + label = self.description_map[b] + selected = '' + if self.archive_state == b: + selected = 'checked="true"' + s += ('%(label)s' + \ + '    ') % \ + {'label': label, + 'param': self.param, + 'selected': selected, + 'value': self.param_map[b] + } + return mark_safe(s) + + def url_without_me(self): + qs = self.filters.querystring_without_filter(self) + if qs != '?': + qs += '&' + return qs + 'archive=both' + + +class DelegateFilter(Filter): + param = 'delegate' + no_delegate_key = '-' + no_delegate_str = 'Nobody' + AnyDelegate = 1 + + def __init__(self, filters): + super(DelegateFilter, self).__init__(filters) + self.name = 'Delegate' + self.param = 'delegate' + self.delegate = None + + def _set_key(self, str): + if str == self.no_delegate_key: + self.applied = True + self.delegate = None + return + + applied = False + try: + self.delegate = User.objects.get(id = str) + self.applied = True + except: + pass + + def kwargs(self): + if not self.applied: + return {} + return {'delegate': self.delegate} + + def condition(self): + if self.delegate: + return self.delegate.profile.name() + return self.no_delegate_str + + def _form(self): + delegates = User.objects.filter(profile__maintainer_projects = + self.filters.project) + + str = '' + + return mark_safe(str) + + def key(self): + if self.delegate: + return self.delegate.id + if self.applied: + return self.no_delegate_key + return None + + def set_status(self, *args, **kwargs): + if 'delegate' in kwargs: + self.applied = self.forced = True + self.delegate = kwargs['delegate'] + if self.AnyDelegate in args: + self.applied = False + self.forced = True + +filterclasses = [SubmitterFilter, \ + StateFilter, + SearchFilter, + ArchiveFilter, + DelegateFilter] + +class Filters: + + def __init__(self, request): + self._filters = map(lambda c: c(self), filterclasses) + self.dict = request.GET + self.project = None + + for f in self._filters: + f.parse(self.dict) + + def set_project(self, project): + self.project = project + + def filter_conditions(self): + kwargs = {} + for f in self._filters: + if f.applied: + kwargs.update(f.kwargs()) + return kwargs + + def apply(self, queryset): + kwargs = self.filter_conditions() + if not kwargs: + return queryset + return queryset.filter(**kwargs) + + def params(self): + return [ (f.param, f.key()) for f in self._filters \ + if f.key() is not None ] + + def querystring(self, remove = None): + params = dict(self.params()) + + for (k, v) in self.dict.iteritems(): + if k not in params: + params[k] = v + + if remove is not None: + if remove.param in params.keys(): + del params[remove.param] + + pairs = params.iteritems() + + def sanitise(s): + if not isinstance(s, basestring): + s = unicode(s) + return quote(s.encode('utf-8')) + + return '?' + '&'.join(['%s=%s' % (sanitise(k), sanitise(v)) + for (k, v) in pairs]) + + def querystring_without_filter(self, filter): + return self.querystring(filter) + + def applied_filters(self): + return filter(lambda x: x.applied, self._filters) + + def available_filters(self): + return self._filters + + def set_status(self, filterclass, *args, **kwargs): + for f in self._filters: + if isinstance(f, filterclass): + f.set_status(*args, **kwargs) + return diff --git a/patchwork/fixtures/default_projects.xml b/patchwork/fixtures/default_projects.xml new file mode 100644 index 0000000..c67fa56 --- /dev/null +++ b/patchwork/fixtures/default_projects.xml @@ -0,0 +1,18 @@ + + + + + + cbe-oss-dev + Cell Broadband Engine development + cbe-oss-dev.ozlabs.org + cbe-oss-dev@ozlabs.org + + + linuxppc-dev + Linux PPC development + linuxppc-dev.ozlabs.org + linuxppc-dev@ozlabs.org + + + diff --git a/patchwork/fixtures/initial_data.xml b/patchwork/fixtures/initial_data.xml new file mode 100644 index 0000000..86e1105 --- /dev/null +++ b/patchwork/fixtures/initial_data.xml @@ -0,0 +1,55 @@ + + + + + + New + 0 + True + + + Under Review + 1 + True + + + Accepted + 2 + False + + + Rejected + 3 + False + + + RFC + 4 + False + + + Not Applicable + 5 + False + + + Changes Requested + 6 + False + + + Awaiting Upstream + 7 + False + + + Superseded + 8 + False + + + Deferred + 9 + False + + diff --git a/patchwork/forms.py b/patchwork/forms.py new file mode 100644 index 0000000..0327958 --- /dev/null +++ b/patchwork/forms.py @@ -0,0 +1,237 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +from django.contrib.auth.models import User +from django import forms + +from patchwork.models import Patch, State, Bundle, UserProfile + +class RegistrationForm(forms.Form): + first_name = forms.CharField(max_length = 30, required = False) + last_name = forms.CharField(max_length = 30, required = False) + username = forms.RegexField(regex = r'^\w+$', max_length=30, + label=u'Username') + email = forms.EmailField(max_length=100, label=u'Email address') + password = forms.CharField(widget=forms.PasswordInput(), + label='Password') + + def clean_username(self): + value = self.cleaned_data['username'] + try: + user = User.objects.get(username__iexact = value) + except User.DoesNotExist: + return self.cleaned_data['username'] + raise forms.ValidationError('This username is already taken. ' + \ + 'Please choose another.') + + def clean_email(self): + value = self.cleaned_data['email'] + try: + user = User.objects.get(email__iexact = value) + except User.DoesNotExist: + return self.cleaned_data['email'] + raise forms.ValidationError('This email address is already in use ' + \ + 'for the account "%s".\n' % user.username) + + def clean(self): + return self.cleaned_data + +class LoginForm(forms.Form): + username = forms.CharField(max_length = 30) + password = forms.CharField(widget = forms.PasswordInput) + +class BundleForm(forms.ModelForm): + name = forms.RegexField(regex = r'^[^/]+$', max_length=50, label=u'Name', + error_messages = {'invalid': 'Bundle names can\'t contain slashes'}) + + class Meta: + model = Bundle + fields = ['name', 'public'] + +class CreateBundleForm(BundleForm): + def __init__(self, *args, **kwargs): + super(CreateBundleForm, self).__init__(*args, **kwargs) + + class Meta: + model = Bundle + fields = ['name'] + + def clean_name(self): + name = self.cleaned_data['name'] + count = Bundle.objects.filter(owner = self.instance.owner, \ + name = name).count() + if count > 0: + raise forms.ValidationError('A bundle called %s already exists' \ + % name) + return name + +class DeleteBundleForm(forms.Form): + name = 'deletebundleform' + form_name = forms.CharField(initial = name, widget = forms.HiddenInput) + bundle_id = forms.IntegerField(widget = forms.HiddenInput) + +class DelegateField(forms.ModelChoiceField): + def __init__(self, project, *args, **kwargs): + queryset = User.objects.filter(profile__in = \ + UserProfile.objects \ + .filter(maintainer_projects = project) \ + .values('pk').query) + super(DelegateField, self).__init__(queryset, *args, **kwargs) + + +class PatchForm(forms.ModelForm): + def __init__(self, instance = None, project = None, *args, **kwargs): + if (not project) and instance: + project = instance.project + if not project: + raise Exception("meep") + super(PatchForm, self).__init__(instance = instance, *args, **kwargs) + self.fields['delegate'] = DelegateField(project, required = False) + + class Meta: + model = Patch + fields = ['state', 'archived', 'delegate'] + +class UserProfileForm(forms.ModelForm): + class Meta: + model = UserProfile + fields = ['primary_project', 'patches_per_page'] + +class OptionalDelegateField(DelegateField): + no_change_choice = ('*', 'no change') + to_field_name = None + + def __init__(self, no_change_choice = None, *args, **kwargs): + self.filter = None + if (no_change_choice): + self.no_change_choice = no_change_choice + super(OptionalDelegateField, self). \ + __init__(initial = self.no_change_choice[0], *args, **kwargs) + + def _get_choices(self): + choices = list( + super(OptionalDelegateField, self)._get_choices()) + choices.append(self.no_change_choice) + return choices + + choices = property(_get_choices, forms.ChoiceField._set_choices) + + def is_no_change(self, value): + return value == self.no_change_choice[0] + + def clean(self, value): + if value == self.no_change_choice[0]: + return value + return super(OptionalDelegateField, self).clean(value) + +class OptionalModelChoiceField(forms.ModelChoiceField): + no_change_choice = ('*', 'no change') + to_field_name = None + + def __init__(self, no_change_choice = None, *args, **kwargs): + self.filter = None + if (no_change_choice): + self.no_change_choice = no_change_choice + super(OptionalModelChoiceField, self). \ + __init__(initial = self.no_change_choice[0], *args, **kwargs) + + def _get_choices(self): + choices = list( + super(OptionalModelChoiceField, self)._get_choices()) + choices.append(self.no_change_choice) + return choices + + choices = property(_get_choices, forms.ChoiceField._set_choices) + + def is_no_change(self, value): + return value == self.no_change_choice[0] + + def clean(self, value): + if value == self.no_change_choice[0]: + return value + return super(OptionalModelChoiceField, self).clean(value) + +class MultipleBooleanField(forms.ChoiceField): + no_change_choice = ('*', 'no change') + def __init__(self, *args, **kwargs): + super(MultipleBooleanField, self).__init__(*args, **kwargs) + self.choices = [self.no_change_choice] + \ + [(True, 'Archived'), (False, 'Unarchived')] + + def is_no_change(self, value): + return value == self.no_change_choice[0] + + # TODO: Check whether it'd be worth to use a TypedChoiceField here; I + # think that'd allow us to get rid of the custom valid_value() and + # to_python() methods. + def valid_value(self, value): + if value in [v1 for (v1, v2) in self.choices]: + return True + return False + + def to_python(self, value): + if value is None or self.is_no_change(value): + return self.no_change_choice[0] + elif value == 'True': + return True + elif value == 'False': + return False + else: + raise ValueError('Unknown value: %s' % value) + +class MultiplePatchForm(forms.Form): + action = 'update' + state = OptionalModelChoiceField(queryset = State.objects.all()) + archived = MultipleBooleanField() + + def __init__(self, project, *args, **kwargs): + super(MultiplePatchForm, self).__init__(*args, **kwargs) + self.fields['delegate'] = OptionalDelegateField(project = project, + required = False) + + def save(self, instance, commit = True): + opts = instance.__class__._meta + if self.errors: + raise ValueError("The %s could not be changed because the data " + "didn't validate." % opts.object_name) + data = self.cleaned_data + # Update the instance + for f in opts.fields: + if not f.name in data: + continue + + field = self.fields.get(f.name, None) + if not field: + continue + + if field.is_no_change(data[f.name]): + continue + + setattr(instance, f.name, data[f.name]) + + if commit: + instance.save() + return instance + +class EmailForm(forms.Form): + email = forms.EmailField(max_length = 200) + +UserPersonLinkForm = EmailForm +OptinoutRequestForm = EmailForm diff --git a/patchwork/models.py b/patchwork/models.py new file mode 100644 index 0000000..54b8656 --- /dev/null +++ b/patchwork/models.py @@ -0,0 +1,386 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from django.db import models +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from django.contrib.sites.models import Site +from django.conf import settings +from patchwork.parser import hash_patch + +import re +import datetime, time +import random + +class Person(models.Model): + email = models.CharField(max_length=255, unique = True) + name = models.CharField(max_length=255, null = True, blank = True) + user = models.ForeignKey(User, null = True, blank = True, + on_delete = models.SET_NULL) + + def __unicode__(self): + if self.name: + return u'%s <%s>' % (self.name, self.email) + else: + return self.email + + def link_to_user(self, user): + self.name = user.profile.name() + self.user = user + + class Meta: + verbose_name_plural = 'People' + +class Project(models.Model): + linkname = models.CharField(max_length=255, unique=True) + name = models.CharField(max_length=255, unique=True) + listid = models.CharField(max_length=255, unique=True) + listemail = models.CharField(max_length=200) + web_url = models.CharField(max_length=2000, blank=True) + scm_url = models.CharField(max_length=2000, blank=True) + webscm_url = models.CharField(max_length=2000, blank=True) + send_notifications = models.BooleanField(default=False) + + def __unicode__(self): + return self.name + + def is_editable(self, user): + if not user.is_authenticated(): + return False + return self in user.profile.maintainer_projects.all() + + class Meta: + ordering = ['linkname'] + + +class UserProfile(models.Model): + user = models.OneToOneField(User, unique = True, related_name='profile') + primary_project = models.ForeignKey(Project, null = True, blank = True) + maintainer_projects = models.ManyToManyField(Project, + related_name = 'maintainer_project') + send_email = models.BooleanField(default = False, + help_text = 'Selecting this option allows patchwork to send ' + + 'email on your behalf') + patches_per_page = models.PositiveIntegerField(default = 100, + null = False, blank = False, + help_text = 'Number of patches to display per page') + + def name(self): + if self.user.first_name or self.user.last_name: + names = filter(bool, [self.user.first_name, self.user.last_name]) + return u' '.join(names) + return self.user.username + + def contributor_projects(self): + submitters = Person.objects.filter(user = self.user) + return Project.objects.filter(id__in = + Patch.objects.filter( + submitter__in = submitters) + .values('project_id').query) + + def sync_person(self): + pass + + def n_todo_patches(self): + return self.todo_patches().count() + + def todo_patches(self, project = None): + + # filter on project, if necessary + if project: + qs = Patch.objects.filter(project = project) + else: + qs = Patch.objects + + qs = qs.filter(archived = False) \ + .filter(delegate = self.user) \ + .filter(state__in = + State.objects.filter(action_required = True) + .values('pk').query) + return qs + + def __unicode__(self): + return self.name() + +def _user_saved_callback(sender, created, instance, **kwargs): + try: + profile = instance.profile + except UserProfile.DoesNotExist: + profile = UserProfile(user = instance) + profile.save() + +models.signals.post_save.connect(_user_saved_callback, sender = User) + +class State(models.Model): + name = models.CharField(max_length = 100) + ordering = models.IntegerField(unique = True) + action_required = models.BooleanField(default = True) + + def __unicode__(self): + return self.name + + class Meta: + ordering = ['ordering'] + +class HashField(models.CharField): + __metaclass__ = models.SubfieldBase + + def __init__(self, algorithm = 'sha1', *args, **kwargs): + self.algorithm = algorithm + try: + import hashlib + def _construct(string = ''): + return hashlib.new(self.algorithm, string) + self.construct = _construct + self.n_bytes = len(hashlib.new(self.algorithm).hexdigest()) + except ImportError: + modules = { 'sha1': 'sha', 'md5': 'md5'} + + if algorithm not in modules.keys(): + raise NameError("Unknown algorithm '%s'" % algorithm) + + self.construct = __import__(modules[algorithm]).new + + self.n_bytes = len(self.construct().hexdigest()) + + kwargs['max_length'] = self.n_bytes + super(HashField, self).__init__(*args, **kwargs) + + def db_type(self, connection=None): + return 'char(%d)' % self.n_bytes + +def get_default_initial_patch_state(): + return State.objects.get(ordering=0) + +class Patch(models.Model): + project = models.ForeignKey(Project) + msgid = models.CharField(max_length=255) + name = models.CharField(max_length=255) + date = models.DateTimeField(default=datetime.datetime.now) + submitter = models.ForeignKey(Person) + delegate = models.ForeignKey(User, blank = True, null = True) + state = models.ForeignKey(State, default=get_default_initial_patch_state) + archived = models.BooleanField(default = False) + headers = models.TextField(blank = True) + content = models.TextField(null = True, blank = True) + pull_url = models.CharField(max_length=255, null = True, blank = True) + commit_ref = models.CharField(max_length=255, null = True, blank = True) + hash = HashField(null = True, blank = True) + + def __unicode__(self): + return self.name + + def comments(self): + return Comment.objects.filter(patch = self) + + def save(self): + try: + s = self.state + except: + self.state = State.objects.get(ordering = 0) + + if self.hash is None and self.content is not None: + self.hash = hash_patch(self.content).hexdigest() + + super(Patch, self).save() + + def is_editable(self, user): + if not user.is_authenticated(): + return False + + if self.submitter.user == user or self.delegate == user: + return True + + return self.project.is_editable(user) + + def filename(self): + fname_re = re.compile('[^-_A-Za-z0-9\.]+') + str = fname_re.sub('-', self.name) + return str.strip('-') + '.patch' + + @models.permalink + def get_absolute_url(self): + return ('patchwork.views.patch.patch', (), {'patch_id': self.id}) + + class Meta: + verbose_name_plural = 'Patches' + ordering = ['date'] + unique_together = [('msgid', 'project')] + +class Comment(models.Model): + patch = models.ForeignKey(Patch) + msgid = models.CharField(max_length=255) + submitter = models.ForeignKey(Person) + date = models.DateTimeField(default = datetime.datetime.now) + headers = models.TextField(blank = True) + content = models.TextField() + + response_re = re.compile( \ + '^(Tested|Reviewed|Acked|Signed-off|Nacked|Reported)-by: .*$', + re.M | re.I) + + def patch_responses(self): + return ''.join([ match.group(0) + '\n' for match in + self.response_re.finditer(self.content)]) + + class Meta: + ordering = ['date'] + unique_together = [('msgid', 'patch')] + +class Bundle(models.Model): + owner = models.ForeignKey(User) + project = models.ForeignKey(Project) + name = models.CharField(max_length = 50, null = False, blank = False) + patches = models.ManyToManyField(Patch, through = 'BundlePatch') + public = models.BooleanField(default = False) + + def n_patches(self): + return self.patches.all().count() + + def ordered_patches(self): + return self.patches.order_by('bundlepatch__order') + + def append_patch(self, patch): + # todo: use the aggregate queries in django 1.1 + orders = BundlePatch.objects.filter(bundle = self).order_by('-order') \ + .values('order') + + if len(orders) > 0: + max_order = orders[0]['order'] + else: + max_order = 0 + + # see if the patch is already in this bundle + if BundlePatch.objects.filter(bundle = self, patch = patch).count(): + raise Exception("patch is already in bundle") + + bp = BundlePatch.objects.create(bundle = self, patch = patch, + order = max_order + 1) + bp.save() + + class Meta: + unique_together = [('owner', 'name')] + + def public_url(self): + if not self.public: + return None + site = Site.objects.get_current() + return 'http://%s%s' % (site.domain, + reverse('patchwork.views.bundle.bundle', + kwargs = { + 'username': self.owner.username, + 'bundlename': self.name + })) + + @models.permalink + def get_absolute_url(self): + return ('patchwork.views.bundle.bundle', (), { + 'username': self.owner.username, + 'bundlename': self.name, + }) + +class BundlePatch(models.Model): + patch = models.ForeignKey(Patch) + bundle = models.ForeignKey(Bundle) + order = models.IntegerField() + + class Meta: + unique_together = [('bundle', 'patch')] + ordering = ['order'] + +class EmailConfirmation(models.Model): + validity = datetime.timedelta(days = settings.CONFIRMATION_VALIDITY_DAYS) + type = models.CharField(max_length = 20, choices = [ + ('userperson', 'User-Person association'), + ('registration', 'Registration'), + ('optout', 'Email opt-out'), + ]) + email = models.CharField(max_length = 200) + user = models.ForeignKey(User, null = True) + key = HashField() + date = models.DateTimeField(default = datetime.datetime.now) + active = models.BooleanField(default = True) + + def deactivate(self): + self.active = False + self.save() + + def is_valid(self): + return self.date + self.validity > datetime.datetime.now() + + def save(self): + max = 1 << 32 + if self.key == '': + str = '%s%s%d' % (self.user, self.email, random.randint(0, max)) + self.key = self._meta.get_field('key').construct(str).hexdigest() + super(EmailConfirmation, self).save() + +class EmailOptout(models.Model): + email = models.CharField(max_length = 200, primary_key = True) + + def __unicode__(self): + return self.email + + @classmethod + def is_optout(cls, email): + email = email.lower().strip() + return cls.objects.filter(email = email).count() > 0 + +class PatchChangeNotification(models.Model): + patch = models.ForeignKey(Patch, primary_key = True) + last_modified = models.DateTimeField(default = datetime.datetime.now) + orig_state = models.ForeignKey(State) + +def _patch_change_callback(sender, instance, **kwargs): + # we only want notification of modified patches + if instance.pk is None: + return + + if instance.project is None or not instance.project.send_notifications: + return + + try: + orig_patch = Patch.objects.get(pk = instance.pk) + except Patch.DoesNotExist: + return + + # If there's no interesting changes, abort without creating the + # notification + if orig_patch.state == instance.state: + return + + notification = None + try: + notification = PatchChangeNotification.objects.get(patch = instance) + except PatchChangeNotification.DoesNotExist: + pass + + if notification is None: + notification = PatchChangeNotification(patch = instance, + orig_state = orig_patch.state) + + elif notification.orig_state == instance.state: + # If we're back at the original state, there is no need to notify + notification.delete() + return + + notification.last_modified = datetime.datetime.now() + notification.save() + +models.signals.pre_save.connect(_patch_change_callback, sender = Patch) diff --git a/patchwork/paginator.py b/patchwork/paginator.py new file mode 100644 index 0000000..31c0190 --- /dev/null +++ b/patchwork/paginator.py @@ -0,0 +1,88 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +from django.core import paginator +from django.conf import settings + +DEFAULT_PATCHES_PER_PAGE = 100 +LONG_PAGE_THRESHOLD = 30 +LEADING_PAGE_RANGE_DISPLAYED = TRAILING_PAGE_RANGE_DISPLAYED = 10 +LEADING_PAGE_RANGE = TRAILING_PAGE_RANGE = 8 +NUM_PAGES_OUTSIDE_RANGE = 2 +ADJACENT_PAGES = 4 + +# parts from: +# http://blog.localkinegrinds.com/2007/09/06/digg-style-pagination-in-django/ + +class Paginator(paginator.Paginator): + def __init__(self, request, objects): + + patches_per_page = settings.DEFAULT_PATCHES_PER_PAGE + + if request.user.is_authenticated(): + patches_per_page = request.user.profile.patches_per_page + + n = request.META.get('ppp') + if n: + try: + patches_per_page = int(n) + except ValueError: + pass + + super(Paginator, self).__init__(objects, patches_per_page) + + try: + page_no = int(request.GET.get('page')) + self.current_page = self.page(int(page_no)) + except Exception: + page_no = 1 + self.current_page = self.page(page_no) + + self.leading_set = self.trailing_set = [] + + pages = self.num_pages + + if pages <= LEADING_PAGE_RANGE_DISPLAYED: + self.adjacent_set = [n for n in range(1, pages + 1) \ + if n > 0 and n <= pages] + elif page_no <= LEADING_PAGE_RANGE: + self.adjacent_set = [n for n in \ + range(1, LEADING_PAGE_RANGE_DISPLAYED + 1) \ + if n > 0 and n <= pages] + self.leading_set = [n + pages for n in \ + range(0, -NUM_PAGES_OUTSIDE_RANGE, -1)] + elif page_no > pages - TRAILING_PAGE_RANGE: + self.adjacent_set = [n for n in \ + range(pages - TRAILING_PAGE_RANGE_DISPLAYED + 1, \ + pages + 1) if n > 0 and n <= pages] + self.trailing_set = [n + 1 for n in range(0, \ + NUM_PAGES_OUTSIDE_RANGE)] + else: + self.adjacent_set = [n for n in range(page_no - ADJACENT_PAGES, \ + page_no + ADJACENT_PAGES + 1) if n > 0 and n <= pages] + self.leading_set = [n + pages for n in \ + range(0, -NUM_PAGES_OUTSIDE_RANGE, -1)] + self.trailing_set = [n + 1 for n in \ + range(0, NUM_PAGES_OUTSIDE_RANGE)] + + + self.leading_set.reverse() + self.long_page = \ + len(self.current_page.object_list) >= LONG_PAGE_THRESHOLD diff --git a/patchwork/parser.py b/patchwork/parser.py new file mode 100644 index 0000000..a51a7b6 --- /dev/null +++ b/patchwork/parser.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python +# +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +import re + +try: + import hashlib + sha1_hash = hashlib.sha1 +except ImportError: + import sha + sha1_hash = sha.sha + +_hunk_re = re.compile('^\@\@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? \@\@') +_filename_re = re.compile('^(---|\+\+\+) (\S+)') + +def parse_patch(text): + patchbuf = '' + commentbuf = '' + buf = '' + + # state specified the line we just saw, and what to expect next + state = 0 + # 0: text + # 1: suspected patch header (diff, ====, Index:) + # 2: patch header line 1 (---) + # 3: patch header line 2 (+++) + # 4: patch hunk header line (@@ line) + # 5: patch hunk content + # 6: patch meta header (rename from/rename to) + # + # valid transitions: + # 0 -> 1 (diff, ===, Index:) + # 0 -> 2 (---) + # 1 -> 2 (---) + # 2 -> 3 (+++) + # 3 -> 4 (@@ line) + # 4 -> 5 (patch content) + # 5 -> 1 (run out of lines from @@-specifed count) + # 1 -> 6 (rename from / rename to) + # 6 -> 2 (---) + # 6 -> 1 (other text) + # + # Suspected patch header is stored into buf, and appended to + # patchbuf if we find a following hunk. Otherwise, append to + # comment after parsing. + + # line counts while parsing a patch hunk + lc = (0, 0) + hunk = 0 + + + for line in text.split('\n'): + line += '\n' + + if state == 0: + if line.startswith('diff ') or line.startswith('===') \ + or line.startswith('Index: '): + state = 1 + buf += line + + elif line.startswith('--- '): + state = 2 + buf += line + + else: + commentbuf += line + + elif state == 1: + buf += line + if line.startswith('--- '): + state = 2 + + if line.startswith('rename from ') or line.startswith('rename to '): + state = 6 + + elif state == 2: + if line.startswith('+++ '): + state = 3 + buf += line + + elif hunk: + state = 1 + buf += line + + else: + state = 0 + commentbuf += buf + line + buf = '' + + elif state == 3: + match = _hunk_re.match(line) + if match: + + def fn(x): + if not x: + return 1 + return int(x) + + lc = map(fn, match.groups()) + + state = 4 + patchbuf += buf + line + buf = '' + + elif line.startswith('--- '): + patchbuf += buf + line + buf = '' + state = 2 + + elif hunk and line.startswith('\ No newline at end of file'): + # If we had a hunk and now we see this, it's part of the patch, + # and we're still expecting another @@ line. + patchbuf += line + + elif hunk: + state = 1 + buf += line + + else: + state = 0 + commentbuf += buf + line + buf = '' + + elif state == 4 or state == 5: + if line.startswith('-'): + lc[0] -= 1 + elif line.startswith('+'): + lc[1] -= 1 + elif line.startswith('\ No newline at end of file'): + # Special case: Not included as part of the hunk's line count + pass + else: + lc[0] -= 1 + lc[1] -= 1 + + patchbuf += line + + if lc[0] <= 0 and lc[1] <= 0: + state = 3 + hunk += 1 + else: + state = 5 + + elif state == 6: + if line.startswith('rename to ') or line.startswith('rename from '): + patchbuf += buf + line + buf = '' + + elif line.startswith('--- '): + patchbuf += buf + line + buf = '' + state = 2 + + else: + buf += line + state = 1 + + else: + raise Exception("Unknown state %d! (line '%s')" % (state, line)) + + commentbuf += buf + + if patchbuf == '': + patchbuf = None + + if commentbuf == '': + commentbuf = None + + return (patchbuf, commentbuf) + +def hash_patch(str): + # normalise spaces + str = str.replace('\r', '') + str = str.strip() + '\n' + + prefixes = ['-', '+', ' '] + hash = sha1_hash() + + for line in str.split('\n'): + + if len(line) <= 0: + continue + + hunk_match = _hunk_re.match(line) + filename_match = _filename_re.match(line) + + if filename_match: + # normalise -p1 top-directories + if filename_match.group(1) == '---': + filename = 'a/' + else: + filename = 'b/' + filename += '/'.join(filename_match.group(2).split('/')[1:]) + + line = filename_match.group(1) + ' ' + filename + + elif hunk_match: + # remove line numbers, but leave line counts + def fn(x): + if not x: + return 1 + return int(x) + line_nos = map(fn, hunk_match.groups()) + line = '@@ -%d +%d @@' % tuple(line_nos) + + elif line[0] in prefixes: + # if we have a +, - or context line, leave as-is + pass + + else: + # other lines are ignored + continue + + hash.update(line.encode('utf-8') + '\n') + + return hash + + +def main(args): + from optparse import OptionParser + + parser = OptionParser() + parser.add_option('-p', '--patch', action = 'store_true', + dest = 'print_patch', help = 'print parsed patch') + parser.add_option('-c', '--comment', action = 'store_true', + dest = 'print_comment', help = 'print parsed comment') + parser.add_option('-#', '--hash', action = 'store_true', + dest = 'print_hash', help = 'print patch hash') + + (options, args) = parser.parse_args() + + # decode from (assumed) UTF-8 + content = sys.stdin.read().decode('utf-8') + + (patch, comment) = parse_patch(content) + + if options.print_hash and patch: + print hash_patch(patch).hexdigest() + + if options.print_patch and patch: + print "Patch: ------\n" + patch + + if options.print_comment and comment: + print "Comment: ----\n" + comment + +if __name__ == '__main__': + import sys + sys.exit(main(sys.argv)) diff --git a/patchwork/requestcontext.py b/patchwork/requestcontext.py new file mode 100644 index 0000000..3b1afaf --- /dev/null +++ b/patchwork/requestcontext.py @@ -0,0 +1,89 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from django.template import RequestContext +from django.utils.html import escape +from django.contrib.sites.models import Site +from django.conf import settings +from patchwork.filters import Filters +from patchwork.models import Bundle, Project + +def bundle(request): + user = request.user + if not user.is_authenticated(): + return {} + return {'bundles': Bundle.objects.filter(owner = user)} + +def _params_as_qs(params): + return '&'.join([ '%s=%s' % (escape(k), escape(v)) for k, v in params ]) + +def _params_as_hidden_fields(params): + return '\n'.join([ '' % \ + (escape(k), escape(v)) for k, v in params ]) + +class PatchworkRequestContext(RequestContext): + def __init__(self, request, project = None, + dict = None, processors = None, + list_view = None, list_view_params = {}): + self._project = project + self.filters = Filters(request) + if processors is None: + processors = [] + processors.append(bundle) + super(PatchworkRequestContext, self). \ + __init__(request, dict, processors); + + self.update({ + 'filters': self.filters, + 'messages': [], + }) + if list_view: + params = self.filters.params() + for param in ['order', 'page']: + value = request.REQUEST.get(param, None) + if value: + params.append((param, value)) + self.update({ + 'list_view': { + 'view': list_view, + 'view_params': list_view_params, + 'params': params + }}) + + self.projects = Project.objects.all() + + self.update({ + 'project': self.project, + 'site': Site.objects.get_current(), + 'settings': settings, + 'other_projects': len(self.projects) > 1 + }) + + def _set_project(self, project): + self._project = project + self.filters.set_project(project) + self.update({'project': self._project}) + + def _get_project(self): + return self._project + + project = property(_get_project, _set_project) + + def add_message(self, message): + self['messages'].append(message) diff --git a/patchwork/settings/__init__.py b/patchwork/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/patchwork/settings/base.py b/patchwork/settings/base.py new file mode 100644 index 0000000..9b52989 --- /dev/null +++ b/patchwork/settings/base.py @@ -0,0 +1,115 @@ +""" +Base settings for patchwork project. +""" + +import os + +import django + +ROOT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), + os.pardir, os.pardir) + +# +# Core settings +# https://docs.djangoproject.com/en/1.6/ref/settings/#core-settings +# + +# Models + +INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.admin', + 'django.contrib.staticfiles', + 'patchwork', +] + +# HTTP + +MIDDLEWARE_CLASSES = [ + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', +] + +if django.VERSION < (1, 7): + MIDDLEWARE_CLASSES.append('django.middleware.doc.XViewMiddleware') +else: + MIDDLEWARE_CLASSES.append( + 'django.contrib.admindocs.middleware.XViewMiddleware') + +# Globalization + +TIME_ZONE = 'Australia/Canberra' + +LANGUAGE_CODE = 'en-au' + +USE_I18N = True + +# URLs + +ROOT_URLCONF = 'patchwork.urls' + +# Templates + +TEMPLATE_DIRS = ( + os.path.join(ROOT_DIR, 'templates'), +) + + +# +# Auth settings +# https://docs.djangoproject.com/en/1.6/ref/settings/#auth +# + +LOGIN_URL = '/user/login/' +LOGIN_REDIRECT_URL = '/user/' + + +# +# Sites settings +# https://docs.djangoproject.com/en/1.6/ref/settings/#sites +# + +SITE_ID = 1 + + +# +# Static files settings +# https://docs.djangoproject.com/en/1.6/ref/settings/#static-files +# + +STATIC_URL = '/static/' + +STATICFILES_DIRS = [ + os.path.join(ROOT_DIR, 'htdocs'), +] + + +# +# Patchwork settings +# + +DEFAULT_PATCHES_PER_PAGE = 100 +DEFAULT_FROM_EMAIL = 'Patchwork ' + +CONFIRMATION_VALIDITY_DAYS = 7 + +NOTIFICATION_DELAY_MINUTES = 10 +NOTIFICATION_FROM_EMAIL = DEFAULT_FROM_EMAIL + +# Set to True to enable the Patchwork XML-RPC interface +ENABLE_XMLRPC = False + +# Set to True to enable redirections or URLs from previous versions +# of patchwork +COMPAT_REDIR = True + +# Set to True to always generate https:// links instead of guessing +# the scheme based on current access. This is useful if SSL protocol +# is terminated upstream of the server (e.g. at the load balancer) +FORCE_HTTPS_LINKS = False diff --git a/patchwork/settings/dev.py b/patchwork/settings/dev.py new file mode 100644 index 0000000..6e373cc --- /dev/null +++ b/patchwork/settings/dev.py @@ -0,0 +1,52 @@ +""" +Development settings for patchwork project. + +These are also used in unit tests. + +Design based on: + http://www.revsys.com/blog/2014/nov/21/recommended-django-project-layout/ +""" + +import django + +from base import * + +# +# Core settings +# https://docs.djangoproject.com/en/1.6/ref/settings/#core-settings +# + +# Security + +SECRET_KEY = '00000000000000000000000000000000000000000000000000' + +# Debugging + +DEBUG = True + +# Templates + +TEMPLATE_DEBUG = True + +# Database + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'HOST': 'localhost', + 'PORT': '', + 'USER': os.environ['PW_TEST_DB_USER'], + 'PASSWORD': os.environ['PW_TEST_DB_PASS'], + 'NAME': 'patchwork', + 'TEST_CHARSET': 'utf8', + }, +} + +if django.VERSION >= (1, 7): + TEST_RUNNER = 'django.test.runner.DiscoverRunner' + +# +# Patchwork settings +# + +ENABLE_XMLRPC = True diff --git a/patchwork/settings/prod.py b/patchwork/settings/prod.py new file mode 100644 index 0000000..d71f3df --- /dev/null +++ b/patchwork/settings/prod.py @@ -0,0 +1,62 @@ +""" +Sample production-ready settings for patchwork project. + +Most of these are commented out as they will be installation dependent. + +Design based on: + http://www.revsys.com/blog/2014/nov/21/recommended-django-project-layout/ +""" + +from base import * + +# +# Core settings +# https://docs.djangoproject.com/en/1.6/ref/settings/#core-settings +# + +# Security + +# SECRET_KEY = '00000000000000000000000000000000000000000000000000' + +# Email + +# ADMINS = ( +# ('Jeremy Kerr', 'jk@ozlabs.org'), +# ) + +# Database + +# DATABASES = { +# 'default': { +# 'ENGINE': 'django.db.backends.postgresql_psycopg2', +# 'NAME': 'patchwork', +# }, +# } + +# File Uploads + +# MEDIA_ROOT = os.path.join( +# ROOT_DIR, 'lib', 'python', 'django', 'contrib', 'admin', 'media') + + +# +# Static files settings +# https://docs.djangoproject.com/en/1.6/ref/settings/#static-files +# + +# STATIC_ROOT = '/srv/patchwork/htdocs' + + +# +# Custom user overrides (for legacy) +# + +try: + from local_settings import * +except ImportError, ex: + import sys + sys.stderr.write(\ + ("settings.py: error importing local settings file:\n" + \ + "\t%s\n" + \ + "Do you have a local_settings.py module?\n") % str(ex)) + raise diff --git a/patchwork/templates/patchwork/activation_email.txt b/patchwork/templates/patchwork/activation_email.txt new file mode 100644 index 0000000..caf514a --- /dev/null +++ b/patchwork/templates/patchwork/activation_email.txt @@ -0,0 +1,11 @@ +Hi, + +This email is to confirm your account on the patchwork patch-tracking +system. You can activate your account by visiting the url: + + http://{{site.domain}}{% url 'patchwork.views.confirm' key=confirmation.key %} + +If you didn't request a user account on patchwork, then you can ignore +this mail. + +Happy patchworking. diff --git a/patchwork/templates/patchwork/activation_email_subject.txt b/patchwork/templates/patchwork/activation_email_subject.txt new file mode 100644 index 0000000..c409f38 --- /dev/null +++ b/patchwork/templates/patchwork/activation_email_subject.txt @@ -0,0 +1 @@ +Patchwork account confirmation diff --git a/patchwork/templates/patchwork/bundle.html b/patchwork/templates/patchwork/bundle.html new file mode 100644 index 0000000..4a96b6b --- /dev/null +++ b/patchwork/templates/patchwork/bundle.html @@ -0,0 +1,47 @@ +{% extends "base.html" %} + +{% load person %} +{% load static %} + +{% block headers %} + + + +{% endblock %} +{% block title %}{{project.name}}{% endblock %} +{% block heading %}bundle: {{bundle.owner.username}} / +{{bundle.name}}{% endblock %} + +{% block body %} + +

This bundle contains patches for the {{ bundle.project.linkname }} +project.

+ +

Download bundle as mbox

+ +{% if bundleform %} +
+ {% csrf_token %} + + + + + + + +{{ bundleform }} + + + +
Bundle settings
+ + +
+
+ +
+{% endif %} + +{% include "patchwork/patch-list.html" %} + +{% endblock %} diff --git a/patchwork/templates/patchwork/bundles.html b/patchwork/templates/patchwork/bundles.html new file mode 100644 index 0000000..11fb89d --- /dev/null +++ b/patchwork/templates/patchwork/bundles.html @@ -0,0 +1,59 @@ +{% extends "base.html" %} + +{% load static %} + +{% block title %}Bundles{% endblock %} +{% block heading %}Bundles{% endblock %} + +{% block body %} + +{% if bundles %} + + + + + + + + +{% for bundle in bundles %} + + + + + + + + + +{% endfor %} +
NameProjectPublic LinkPatches + DownloadDelete
{{ bundle.name }}{{ bundle.project.linkname }} + {% if bundle.public %} + {{ bundle.public_url }} + {% endif %} + {{ bundle.n_patches }}download +
+ {% csrf_token %} + {{ bundle.delete_form.as_p }} + +
+
+{% endif %} + +

Bundles are groups of related patches. You can create bundles by +selecting patches from a project, then using the 'create bundle' form +to give your bundle a name. Each bundle can be public or private; public +bundles are given a persistent URL, based you your username and the name +of the bundle. Private bundles are only visible to you.

+ +{% if not bundles %} +

You have no bundles.

+{% endif %} +{% endblock %} diff --git a/patchwork/templates/patchwork/confirm-error.html b/patchwork/templates/patchwork/confirm-error.html new file mode 100644 index 0000000..81292e2 --- /dev/null +++ b/patchwork/templates/patchwork/confirm-error.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block title %}Confirmation{% endblock %} +{% block heading %}Confirmation{% endblock %} + + +{% block body %} + +{% if error == 'inactive' %} +

This confirmation has already been processed; you've probably visited this +page before.

+{% endif %} + +{% if error == 'expired' %} +

The confirmation has expired. If you'd still like to perform the +{{conf.get_type_display}} process, you'll need to resubmit the request.

+{% endif %} + +{% endblock %} diff --git a/patchwork/templates/patchwork/filters.html b/patchwork/templates/patchwork/filters.html new file mode 100644 index 0000000..10ca587 --- /dev/null +++ b/patchwork/templates/patchwork/filters.html @@ -0,0 +1,183 @@ +{% load static %} + + + +
+
+ Filters: + {% if filters.applied_filters %} + {% for filter in filters.applied_filters %} + {{ filter.name }} = {{ filter.condition }} + {% if not filter.forced %} + remove filter + {% endif %} + {% if not forloop.last %}   |   {% endif %} + {% endfor %} + {% else %} + none + add filter + {% endif %} +
+ +
+ + diff --git a/patchwork/templates/patchwork/help/about.html b/patchwork/templates/patchwork/help/about.html new file mode 100644 index 0000000..7befa6b --- /dev/null +++ b/patchwork/templates/patchwork/help/about.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% block title %}About{% endblock %} +{% block heading %} - About Patchwork{% endblock %} + +{% block body %} + +

Patchwork is free software, and is available from the +patchwork website.

+ +

Patchwork is built on the django +web framework.

+ +

Icons from the Sweetie icon set. + +{% endblock %} + diff --git a/patchwork/templates/patchwork/help/index.html b/patchwork/templates/patchwork/help/index.html new file mode 100644 index 0000000..5cb6467 --- /dev/null +++ b/patchwork/templates/patchwork/help/index.html @@ -0,0 +1,2 @@ +{% extends "base.html" %} + diff --git a/patchwork/templates/patchwork/help/pwclient.html b/patchwork/templates/patchwork/help/pwclient.html new file mode 100644 index 0000000..7101ec1 --- /dev/null +++ b/patchwork/templates/patchwork/help/pwclient.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block title %}Command-line client{% endblock %} +{% block heading %} - Command-line client{% endblock %} + +{% block body %} + +

pwclient is the command-line client for patchwork. Currently, +it provides access to some read-only features of patchwork, such as downloading +and applying patches.

+ +

To use pwclient, you will need:

+
    +
  • The pwclient + program (11kB, python script)
  • +
  • (optional) a .pwclientrc file in your home directory.
  • +
+ +

You can create your own .pwclientrc file. Each +patchwork project +provides a sample linked from the 'project info' page.

+ +{% endblock %} diff --git a/patchwork/templates/patchwork/list.html b/patchwork/templates/patchwork/list.html new file mode 100644 index 0000000..654fe8c --- /dev/null +++ b/patchwork/templates/patchwork/list.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% load person %} +{% load static %} + +{% block title %}{{project.name}}{% endblock %} +{% block heading %}{{project.name}}{% endblock %} + +{% block body %} + +

Incoming patches

+ +{% if errors %} +

The following error{{ errors|length|pluralize:" was,s were" }} encountered +while updating patches:

+
    +{% for error in errors %} +
  • {{ error }}
  • +{% endfor %} +
+{% endif %} + +{% include "patchwork/patch-list.html" %} + +{% endblock %} diff --git a/patchwork/templates/patchwork/login.html b/patchwork/templates/patchwork/login.html new file mode 100644 index 0000000..2dfc2a7 --- /dev/null +++ b/patchwork/templates/patchwork/login.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block title %}Login{% endblock %} +{% block heading %}Login{% endblock %} + + +{% block body %} +
+{% csrf_token %} + + + + + {% if error %} + + + + {% endif %} + {{ form }} + + + +
login
{{ error }}
+ +
+
+{% endblock %} diff --git a/patchwork/templates/patchwork/logout.html b/patchwork/templates/patchwork/logout.html new file mode 100644 index 0000000..f030aee --- /dev/null +++ b/patchwork/templates/patchwork/logout.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block title %}Logout{% endblock %} +{% block heading %}Logout{% endblock %} + +{% block body %} +

Logged out

+{% endblock %} diff --git a/patchwork/templates/patchwork/mail-form.html b/patchwork/templates/patchwork/mail-form.html new file mode 100644 index 0000000..d71b2fb --- /dev/null +++ b/patchwork/templates/patchwork/mail-form.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} + +{% block title %}mail settings{% endblock %} +{% block heading %}mail settings{% endblock %} + +{% block body %} + +

You can configure patchwork to send you mail on certain events, +or block automated mail altogether. Enter your email address to +view or change your email settings.

+ +
+{% csrf_token %} + +{% if form.errors %} + + + +{% endif %} + + + + + + + +
+ There was an error accessing your mail settings: +
{{ form.email.label_tag }} + {{form.email}} + {{form.email.errors}} +
+ +
+
+ + +{% endblock %} diff --git a/patchwork/templates/patchwork/mail-settings.html b/patchwork/templates/patchwork/mail-settings.html new file mode 100644 index 0000000..440af08 --- /dev/null +++ b/patchwork/templates/patchwork/mail-settings.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} + +{% block title %}mail settings{% endblock %} +{% block heading %}mail settings{% endblock %} + +{% block body %} +

Settings for {{email}}:

+ + + + +{% if is_optout %} + + + +{% else %} + + +{% endif %} + +
Opt-out listPatchwork may not send automated notifications to + this address. +
+ {% csrf_token %} + + +
+
Patchwork may send automated notifications to + this address. +
+ {% csrf_token %} + + +
+
+ +{% endblock %} diff --git a/patchwork/templates/patchwork/optin-request.html b/patchwork/templates/patchwork/optin-request.html new file mode 100644 index 0000000..3dfb1bd --- /dev/null +++ b/patchwork/templates/patchwork/optin-request.html @@ -0,0 +1,50 @@ +{% extends "base.html" %} + +{% block title %}opt-in{% endblock %} +{% block heading %}opt-in{% endblock %} + +{% block body %} +{% if email_sent %} +

Opt-in confirmation email sent

+

An opt-in confirmation mail has been sent to +{{confirmation.email}}, containing a link. Please click on +that link to confirm your opt-in.

+{% else %} +{% if error %} +

{{error}}

+{% endif %} + +{% if form %} +

This form allows you to opt-in to automated email from patchwork. Use +this if you have previously opted-out of patchwork mail, but now want to +received notifications from patchwork.

+When you submit it, an email will be sent to your address with a link to click +to finalise the opt-in. Patchwork does this to prevent someone opting you in +without your consent.

+
+{% csrf_token %} +{{form.email.errors}} +
+{{form.email.label_tag}}: {{form.email}} +
+ +
+{% endif %} + +{% if error and admins %} +

If you are having trouble opting in, please email +{% for admin in admins %} +{% if admins|length > 1 and forloop.last %} or {% endif %} +{{admin.0}} <{{admin.1}}>{% if admins|length > 2 and not forloop.last %}, {% endif %} +{% endfor %} +{% endif %} + +{% endif %} + +{% if user.is_authenticated %} +

Return to your user +profile.

+{% endif %} + +{% endblock %} diff --git a/patchwork/templates/patchwork/optin-request.mail b/patchwork/templates/patchwork/optin-request.mail new file mode 100644 index 0000000..d97c78b --- /dev/null +++ b/patchwork/templates/patchwork/optin-request.mail @@ -0,0 +1,12 @@ +Hi, + +This email is to confirm that you would like to opt-in to automated +email from the patchwork system at {{site.domain}}. + +To complete the opt-in process, visit: + + http://{{site.domain}}{% url 'patchwork.views.confirm' key=confirmation.key %} + +If you didn't request this opt-in, you don't need to do anything. + +Happy patchworking. diff --git a/patchwork/templates/patchwork/optin.html b/patchwork/templates/patchwork/optin.html new file mode 100644 index 0000000..01aaa0e --- /dev/null +++ b/patchwork/templates/patchwork/optin.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block title %}opt-in{% endblock %} +{% block heading %}opt-in{% endblock %} + +{% block body %} + +

Opt-in complete. You have sucessfully opted back in to +automated email from this patchwork system, using the address +{{email}}.

+

If you later decide that you no longer want to receive automated mail from +patchwork, just visit http://{{site.domain}}{% url 'patchwork.views.mail.settings' %}, or +visit the main patchwork page and navigate from there.

+{% if user.is_authenticated %} +

Return to your user +profile.

+{% endif %} +{% endblock %} diff --git a/patchwork/templates/patchwork/optout-request.html b/patchwork/templates/patchwork/optout-request.html new file mode 100644 index 0000000..092dbbb --- /dev/null +++ b/patchwork/templates/patchwork/optout-request.html @@ -0,0 +1,51 @@ +{% extends "base.html" %} + +{% block title %}opt-out{% endblock %} +{% block heading %}opt-out{% endblock %} + +{% block body %} +{% if email_sent %} +

Opt-out confirmation email sent

+

An opt-out confirmation mail has been sent to +{{confirmation.email}}, containing a link. Please click on +that link to confirm your opt-out.

+{% else %} +{% if error %} +

{{error}}

+{% endif %} + +{% if form %} +

This form allows you to opt-out of automated email from patchwork.

+

If you opt-out of email, Patchwork may still email you if you do certain +actions yourself (such as create a new patchwork account), but will not send +you unsolicited email.

+When you submit it, one email will be sent to your address with a link to click +to finalise the opt-out. Patchwork does this to prevent someone opting you out +without your consent.

+
+{% csrf_token %} +{{form.email.errors}} +
+{{form.email.label_tag}}: {{form.email}} +
+ +
+{% endif %} + +{% if error and admins %} +

If you are having trouble opting out, please email +{% for admin in admins %} +{% if admins|length > 1 and forloop.last %} or {% endif %} +{{admin.0}} <{{admin.1}}>{% if admins|length > 2 and not forloop.last %}, {% endif %} +{% endfor %} +{% endif %} + +{% endif %} + +{% if user.is_authenticated %} +

Return to your user +profile.

+{% endif %} + +{% endblock %} diff --git a/patchwork/templates/patchwork/optout-request.mail b/patchwork/templates/patchwork/optout-request.mail new file mode 100644 index 0000000..67203ca --- /dev/null +++ b/patchwork/templates/patchwork/optout-request.mail @@ -0,0 +1,12 @@ +Hi, + +This email is to confirm that you would like to opt-out from all email +from the patchwork system at {{site.domain}}. + +To complete the opt-out process, visit: + + http://{{site.domain}}{% url 'patchwork.views.confirm' key=confirmation.key %} + +If you didn't request this opt-out, you don't need to do anything. + +Happy patchworking. diff --git a/patchwork/templates/patchwork/optout.html b/patchwork/templates/patchwork/optout.html new file mode 100644 index 0000000..b140bf4 --- /dev/null +++ b/patchwork/templates/patchwork/optout.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} + +{% block title %}opt-out{% endblock %} +{% block heading %}opt-out{% endblock %} + +{% block body %} + +

Opt-out complete. You have successfully opted-out of +automated notifications from this patchwork system, from the address +{{email}}

+

Please note that you may still receive email from other patchwork setups at +different sites, as they are run independently. You may need to opt-out of +those separately.

+

If you later decide to receive mail from patchwork, just visit +http://{{site.domain}}{% url 'patchwork.views.mail.settings' %}, or +visit the main patchwork page and navigate from there.

+{% if user.is_authenticated %} +

Return to your user +profile.

+{% endif %} +{% endblock %} diff --git a/patchwork/templates/patchwork/pagination.html b/patchwork/templates/patchwork/pagination.html new file mode 100644 index 0000000..3e95126 --- /dev/null +++ b/patchwork/templates/patchwork/pagination.html @@ -0,0 +1,45 @@ +{% load listurl %} + +{% ifnotequal page.paginator.num_pages 1 %} +
+{% if page.has_previous %} + + « Previous +{% else %} + « Previous +{% endif %} + +{% if page.paginator.trailing_set %} + {% for p in page.paginator.trailing_set %} + {{ p }} + {% endfor %} + ... +{% endif %} + +{% for p in page.paginator.adjacent_set %} + {% ifequal p page.number %} + {{ p }} + {% else %} + {{ p }} + {% endifequal %} +{% endfor %} + +{% if page.paginator.leading_set %} + ... + {% for p in page.paginator.leading_set %} + {{ p }} + {% endfor %} +{% endif %} + +{% if page.has_next %} + + Next » + +{% else %} + Next » +{% endif %} +
+{% endifnotequal %} diff --git a/patchwork/templates/patchwork/patch-change-notification-subject.text b/patchwork/templates/patchwork/patch-change-notification-subject.text new file mode 100644 index 0000000..c9d96d4 --- /dev/null +++ b/patchwork/templates/patchwork/patch-change-notification-subject.text @@ -0,0 +1 @@ +[{{ projects|join:"," }}] Patch notification: {{notifications|length}} patch{{notifications|length|pluralize:"es"}} updated diff --git a/patchwork/templates/patchwork/patch-change-notification.mail b/patchwork/templates/patchwork/patch-change-notification.mail new file mode 100644 index 0000000..4246704 --- /dev/null +++ b/patchwork/templates/patchwork/patch-change-notification.mail @@ -0,0 +1,20 @@ +Hello, + +The following patch{{notifications|length|pluralize:"es"}} (submitted by you) {{notifications|length|pluralize:"has,have"}} been updated in patchwork: +{% for notification in notifications %} + * {{notification.patch.project.linkname}}: {{notification.patch.name|safe}} + - http://{{site.domain}}{{notification.patch.get_absolute_url}} + - for: {{notification.patch.project.name}} + was: {{notification.orig_state}} + now: {{notification.patch.state}} +{% endfor %} +This email is a notification only - you do not need to respond. + +Happy patchworking. + +-- + +This is an automated mail sent by the patchwork system at +{{site.domain}}. To stop receiving these notifications, edit +your mail settings at: + http://{{site.domain}}{% url 'patchwork.views.mail.settings' %} diff --git a/patchwork/templates/patchwork/patch-list.html b/patchwork/templates/patchwork/patch-list.html new file mode 100644 index 0000000..675f67f --- /dev/null +++ b/patchwork/templates/patchwork/patch-list.html @@ -0,0 +1,268 @@ +{% load person %} +{% load listurl %} +{% load static %} + +{% include "patchwork/pagination.html" %} + + + + + + {% if order.editable %} + + {% endif %} + +
+ {% include "patchwork/filters.html" %} + +
+ {% csrf_token %} + + + + + +
+
+ +{% if page.paginator.long_page and user.is_authenticated %} +
+ +
+{% endif %} + +
+{% csrf_token %} + + + + + + {% if user.is_authenticated %} + + {% endif %} + + + + + + + + + + + + + + +{% if page.paginator.count %} + + {% for patch in page.object_list %} + + {% if user.is_authenticated %} + + {% endif %} + + + + + + + {% endfor %} + +
+ + + {% ifequal order.name "name" %} + Patch + {% else %} + {% if not order.editable %} + Patch + {% else %} + Patch + {% endif %} + {% endifequal %} + + {% ifequal order.name "date" %} + Date + {% else %} + {% if not order.editable %} + Date + {% else %} + Date + {% endif %} + {% endifequal %} + + {% ifequal order.name "submitter" %} + Submitter + {% else %} + {% if not order.editable %} + Submitter + {% else %} + Submitter + {% endif %} + {% endifequal %} + + {% ifequal order.name "delegate" %} + Delegate + {% else %} + {% if not order.editable %} + Delegate + {% else %} + Delegate + {% endif %} + {% endifequal %} + + {% ifequal order.name "state" %} + State + {% else %} + {% if not order.editable %} + State + {% else %} + State + {% endif %} + {% endifequal %} +
+ + {{ patch.name|default:"[no subject]" }}{{ patch.date|date:"Y-m-d" }}{{ patch.submitter|personify:project }}{{ patch.delegate.username }}{{ patch.state }}
+ +{% include "patchwork/pagination.html" %} + +
+ +{% if patchform %} +
+

Properties

+ + + + + + + + + + + + + + + +
Change state: + {{ patchform.state }} + {{ patchform.state.errors }} +
Delegate to: + + {{ patchform.delegate }} + {{ patchform.delegate.errors }} +
Archive: + + {{ patchform.archived }} + {{ patchform.archived.errors }} +
+ +
+
+ +{% endif %} + +{% if user.is_authenticated %} +
+

Bundling

+ + + + + + {% if bundles %} + + + + + {% endif %} + {% if bundle %} + + + + + {% endif %} +
Create bundle: + + +
Add to bundle: + + +
Remove from bundle: + + +
+
+{% endif %} + + +
+
+
+ +{% else %} + + No patches to display + +{% endif %} + + +
+ diff --git a/patchwork/templates/patchwork/patch.html b/patchwork/templates/patchwork/patch.html new file mode 100644 index 0000000..f18ee3b --- /dev/null +++ b/patchwork/templates/patchwork/patch.html @@ -0,0 +1,199 @@ +{% extends "base.html" %} + +{% load syntax %} +{% load person %} +{% load patch %} + +{% block title %}{{patch.name}}{% endblock %} +{% block heading %}{{patch.name}}{%endblock%} + +{% block body %} + + + + + + + + + + + + + + + + + + + + + + + + + + +{% if patch.commit_ref %} + + + + +{% endif %} +{% if patch.delegate %} + + + + +{% endif %} + + + + +
Submitter{{ patch.submitter|personify:project }}
Date{{ patch.date }}
Message ID{{ patch.msgid }}
Download + mbox +{% if patch.content %}| + patch +{% endif %} +
Permalink{{ patch.get_absolute_url }} +
State{{ patch.state.name }}{% if patch.archived %}, archived{% endif %}
Commit{{ patch.commit_ref }}
Delegated to:{{ patch.delegate.profile.name }}
Headersshow + +
+ +
+ +{% if patchform %} +
+

Patch Properties

+
+ {% csrf_token %} + + + + + + + + + + + + + + + + + +
Change state: + {{ patchform.state }} + {{ patchform.state.errors }} +
Delegate to: + {{ patchform.delegate }} + {{ patchform.delegate.errors }} +
Archived: + {{ patchform.archived }} + {{ patchform.archived.errors }} +
+ +
+
+
+{% endif %} + +{% if createbundleform %} +
+

Bundling

+ + + + + +{% if bundles %} + + + + +{% endif %} +
Create bundle: + {% if createbundleform.non_field_errors %} +
{{createbundleform.non_field_errors}}
+ {% endif %} +
+ {% csrf_token %} + + {% if createbundleform.name.errors %} +
{{createbundleform.name.errors}}
+ {% endif %} + {{ createbundleform.name }} + +
+
Add to bundle: +
+ {% csrf_token %} + + + +
+
+ +
+{% endif %} + +
+
+
+ +{% if patch.pull_url %} +

Pull-request

+{{ patch.pull_url }} +{% endif %} + +

Comments

+{% for comment in patch.comments %} +
+
{{ comment.submitter|personify:project }} - {{comment.date}}
+
+{{ comment|commentsyntax }}
+
+
+{% endfor %} + +{% if patch.content %} +

Patch

+
+
+{{ patch|patchsyntax }}
+
+
+{% endif %} + + +{% endblock %} diff --git a/patchwork/templates/patchwork/profile.html b/patchwork/templates/patchwork/profile.html new file mode 100644 index 0000000..116d6d6 --- /dev/null +++ b/patchwork/templates/patchwork/profile.html @@ -0,0 +1,144 @@ +{% extends "base.html" %} + +{% block title %}User Profile: {{ user.username }}{% endblock %} +{% block heading %}User Profile: {{ user.username }}{% endblock %} + + +{% block body %} + +

+{% if user.profile.maintainer_projects.count %} +Maintainer of +{% for project in user.profile.maintainer_projects.all %} +{{ project.linkname }}{% if not forloop.last %},{% endif %}{% endfor %}. +{% endif %} + +{% if user.profile.contributor_projects.count %} +Contributor to +{% for project in user.profile.contributor_projects.all %} +{{ project.linkname }}{% if not forloop.last %},{% endif %}{% endfor %}. +{% endif %} +

+ +
+
+

Todo

+{% if user.profile.n_todo_patches %} +

Your todo + list contains {{ user.profile.n_todo_patches }} + patch{{ user.profile.n_todo_patches|pluralize:"es" }}.

+{% else %} +

Your todo list contains patches that have been delegated to you. You + have no items in your todo list at present.

+{% endif %} +
+ +
+

Linked email addresses

+

The following email addresses are associated with this patchwork account. +Adding alternative addresses allows patchwork to group contributions that +you have made under different addresses.

+

The "notify?" column allows you to opt-in or -out of automated +patchwork notification emails. Setting it to "no" will disable automated +notifications for that address.

+

Adding a new email address will send a confirmation email to that +address.

+ + + + + + +{% for email in linked_emails %} + + + + + +{% endfor %} + + + +
emailactionnotify?
{{ email.email }} + {% ifnotequal user.email email.email %} +
+ {% csrf_token %} + +
+ {% endifnotequal %} +
+ {% if email.is_optout %} +
+ No, + {% csrf_token %} + + +
+ {% else %} +
+ Yes, + {% csrf_token %} + + +
+ {% endif %} +
+
+ {% csrf_token %} + {{ linkform.email }} + +
+
+
+
+ +
+ +
+

Bundles

+ +{% if bundles %} +

You have the following bundle{{ bundle|length|pluralize }}:

+ +

Visit the bundles + page to manage your bundles.

+{% else %} +

You have no bundles.

+{% endif %} +
+ + +
+

Settings

+ +
+ {% csrf_token %} + +{{ profileform }} + + + +
+ + +
+
+
+ +
+

Authentication

+Change password +
+ +
+ +

+ +{% endblock %} diff --git a/patchwork/templates/patchwork/project.html b/patchwork/templates/patchwork/project.html new file mode 100644 index 0000000..be8cadc --- /dev/null +++ b/patchwork/templates/patchwork/project.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} + +{% block title %}{{ project.name }}{% endblock %} +{% block heading %}{{ project.name }}{% endblock %} + +{% block body %} + + + + + + + + + + + + + + + + + +{% if project.web_url %} + + + + +{% endif %} +{% if project.webscm_url %} + + + + +{% endif %} +{% if project.scm_url %} + + + + +{% endif %} +
Name{{project.name}} +
List address{{project.listemail}}
Maintainer{{maintainers|length|pluralize}} + {% for maintainer in maintainers %} + {{ maintainer.profile.name }} + <{{maintainer.email}}> +
+ {% endfor %} +
Patch count{{n_patches}} (+ {{n_archived_patches}} archived)
Website{{project.web_url}}
Source Code Web Interface{{project.webscm_url}}
Source Code Manager URL{{project.scm_url}}
+ +{% if settings.ENABLE_XMLRPC %} +

Sample patchwork +client configuration for this project: .pwclientrc.

+{% endif %} + +{% endblock %} diff --git a/patchwork/templates/patchwork/projects.html b/patchwork/templates/patchwork/projects.html new file mode 100644 index 0000000..8c727ad --- /dev/null +++ b/patchwork/templates/patchwork/projects.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block title %}Project List{% endblock %} +{% block heading %}Project List{% endblock %} + +{% block body %} + +{% if projects %} +
+ {% for p in projects %} +
+

+ {{p.linkname}} +

+
{{p.name}}
+{% if p.web_url %} + +{% endif %} +
+ {% endfor %} +
+{% else %} +

Patchwork doesn't have any projects to display!

+{% endif %} + +{% endblock %} diff --git a/patchwork/templates/patchwork/pwclient b/patchwork/templates/patchwork/pwclient new file mode 120000 index 0000000..5ce255f --- /dev/null +++ b/patchwork/templates/patchwork/pwclient @@ -0,0 +1 @@ +../../bin/pwclient \ No newline at end of file diff --git a/patchwork/templates/patchwork/pwclientrc b/patchwork/templates/patchwork/pwclientrc new file mode 100644 index 0000000..d331003 --- /dev/null +++ b/patchwork/templates/patchwork/pwclientrc @@ -0,0 +1,15 @@ +# Sample .pwclientrc file for the {{ project.linkname }} project, +# running on {{ site.domain }}. +# +# Just append this file to your existing ~/.pwclientrc +# If you do not already have a ~/.pwclientrc, then copy this file to +# ~/.pwclientrc, and uncomment the following two lines: +# [options] +# default={{ project.linkname }} + +[{{ project.linkname }}] +url= {{scheme}}://{{site.domain}}{% url 'patchwork.views.xmlrpc.xmlrpc' %} +{% if user.is_authenticated %} +username: {{ user.username }} +password: +{% endif %} diff --git a/patchwork/templates/patchwork/register.mail b/patchwork/templates/patchwork/register.mail new file mode 100644 index 0000000..9079203 --- /dev/null +++ b/patchwork/templates/patchwork/register.mail @@ -0,0 +1,11 @@ +Hi, + +This email is to confirm your account on the patchwork patch-tracking +system. You can activate your account by visiting the url: + + http://{{site.domain}}{% url 'registration_activateactivation_key'=request.key %} + +If you didn't request a user account on patchwork, then you can ignore +this mail. + +Happy patchworking. diff --git a/patchwork/templates/patchwork/registration-confirm.html b/patchwork/templates/patchwork/registration-confirm.html new file mode 100644 index 0000000..6111401 --- /dev/null +++ b/patchwork/templates/patchwork/registration-confirm.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block title %}Registration{% endblock %} +{% block heading %}Registration{% endblock %} + +{% block body %} +

Registraton confirmed!

+ +

Your patchwork registration is complete. Head over to your profile to start using +patchwork's extra features.

+ +{% endblock %} diff --git a/patchwork/templates/patchwork/registration_form.html b/patchwork/templates/patchwork/registration_form.html new file mode 100644 index 0000000..3a314b8 --- /dev/null +++ b/patchwork/templates/patchwork/registration_form.html @@ -0,0 +1,121 @@ +{% extends "base.html" %} + +{% block title %}Registration{% endblock %} +{% block heading %}Registration{% endblock %} + + +{% block body %} + +{% if confirmation and not error %} +

Registration successful!

+

A confirmation email has been sent to {{ confirmation.email }}. You'll + need to visit the link provided in that email to confirm your + registration.

+

+{% else %} +

By creating a patchwork account, you can:

+

    +
  • create "bundles" of patches
  • +
  • update the state of your own patches
  • +
+
+{% csrf_token %} + + + + + {% if error %} + + + + {% endif %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
register
{{ error }}
{{ form.first_name.label_tag }} +{% if form.first_name.errors %} + {{ form.first_name.errors }} +{% endif %} + {{ form.first_name }} +{% if form.first_name.help_text %} +
{{ form.first_name.help_text }}
+{% endif %} +
{{ form.last_name.label_tag }} +{% if form.last_name.errors %} + {{ form.last_name.errors }} +{% endif %} + {{ form.last_name }} +{% if form.last_name.help_text %} +
{{ form.last_name.help_text }}
+{% endif %} +
+ Your name is used to identify you on the site +
{{ form.email.label_tag }} +{% if form.email.errors %} + {{ form.email.errors }} +{% endif %} + {{ form.email }} +{% if form.email.help_text %} +
{{ form.email.help_text }}
+{% endif %} +
+ Patchwork will send a confirmation email to this address +
{{ form.username.label_tag }} +{% if form.username.errors %} + {{ form.username.errors }} +{% endif %} + {{ form.username }} +{% if form.username.help_text %} +
{{ form.username.help_text }}
+{% endif %} +
{{ form.password.label_tag }} +{% if form.password.errors %} + {{ form.password.errors }} +{% endif %} + {{ form.password }} +{% if form.password.help_text %} +
{{ form.password.help_text }}
+{% endif %} +
+ +
+
+{% endif %} + +{% endblock %} diff --git a/patchwork/templates/patchwork/todo-list.html b/patchwork/templates/patchwork/todo-list.html new file mode 100644 index 0000000..b301901 --- /dev/null +++ b/patchwork/templates/patchwork/todo-list.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% load person %} + +{% block title %}{{ user }}'s todo list{% endblock %} +{% block heading %}{{user}}'s todo list for {{ project.linkname }}{% endblock %} + +{% block body %} + +

A Patchwork Todo-list contains patches that are assigned to you, and +are in an "action required" state +({% for state in action_required_states %}{% if forloop.last and not forloop.first %} or {% endif %}{{ state }}{% if not forloop.last and not forloop.first %}, {%endif %}{% endfor %}), and are not archived. +

+ +{% include "patchwork/patch-list.html" %} + +{% endblock %} diff --git a/patchwork/templates/patchwork/todo-lists.html b/patchwork/templates/patchwork/todo-lists.html new file mode 100644 index 0000000..e268160 --- /dev/null +++ b/patchwork/templates/patchwork/todo-lists.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} + +{% block title %}{{ user }}'s todo lists{% endblock %} +{% block heading %}{{ user }}'s todo lists{% endblock %} + +{% block body %} + +{% if todo_lists %} +

You have multiple todo lists. Each todo list contains patches for a single + project.

+ + + + + +{% for todo_list in todo_lists %} + + + + +{% endfor %} +
projectpatches
{{ todo_list.project.name }}{{ todo_list.n_patches }}
+ +{% else %} + No todo lists +{% endif %} +{% endblock %} diff --git a/patchwork/templates/patchwork/user-link-confirm.html b/patchwork/templates/patchwork/user-link-confirm.html new file mode 100644 index 0000000..449bfeb --- /dev/null +++ b/patchwork/templates/patchwork/user-link-confirm.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block title %}{{ user.username }}{% endblock %} +{% block heading %}link accounts for {{ user.username }}{% endblock %} + + +{% block body %} + +{% if errors %} +

{{ errors }}

+{% else %} +

You have sucessfully linked the email address {{ person.email }} to + your patchwork account

+ +{% endif %} +

Back to your + profile.

+ +{% endblock %} diff --git a/patchwork/templates/patchwork/user-link.html b/patchwork/templates/patchwork/user-link.html new file mode 100644 index 0000000..e436c3a --- /dev/null +++ b/patchwork/templates/patchwork/user-link.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} + +{% block title %}{{ user.username }}{% endblock %} +{% block heading %}link accounts for {{ user.username }}{% endblock %} + + +{% block body %} + +{% if confirmation and not error %} +

A confirmation email has been sent to {{ confirmation.email }}. Click +on the link provided in the email to confirm that this address belongs to +you.

+ +{% else %} + + {% if form.errors %} +

There was an error submitting your link request.

+ {{ form.non_field_errors }} + {% endif %} + {% if error %} +
  • {{error}}
+ {% endif %} + +
+ {% csrf_token %} + {{linkform.email.errors}} + Link an email address: {{ linkform.email }} +
+ +{% endif %} + +{% endblock %} diff --git a/patchwork/templates/patchwork/user-link.mail b/patchwork/templates/patchwork/user-link.mail new file mode 100644 index 0000000..8db6726 --- /dev/null +++ b/patchwork/templates/patchwork/user-link.mail @@ -0,0 +1,12 @@ +Hi, + +This email is to confirm that you own the email address: + + {{ confirmation.email }} + +So that you can add it to your patchwork profile. You can confirm this +email address by visiting the url: + + http://{{site.domain}}{% url 'patchwork.views.confirm' key=confirmation.key %} + +Happy patchworking. diff --git a/patchwork/templatetags/__init__.py b/patchwork/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/patchwork/templatetags/filter.py b/patchwork/templatetags/filter.py new file mode 100644 index 0000000..7a5d9df --- /dev/null +++ b/patchwork/templatetags/filter.py @@ -0,0 +1,36 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from django import template +from django.utils.html import escape + +import re + + +register = template.Library() + +@register.filter +def personify(person): + if person.name: + linktext = escape(person.name) + else: + linktext = escape(person.email) + + return '%s' % (escape(person.email), linktext) + diff --git a/patchwork/templatetags/listurl.py b/patchwork/templatetags/listurl.py new file mode 100644 index 0000000..5fe03e4 --- /dev/null +++ b/patchwork/templatetags/listurl.py @@ -0,0 +1,136 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from django import template +from django.utils.html import escape +from django.utils.safestring import mark_safe +from django.utils.encoding import smart_str +from patchwork.filters import filterclasses +from django.conf import settings +from django.core.urlresolvers import reverse, NoReverseMatch +import re + +register = template.Library() + +# params to preserve across views +list_params = [ c.param for c in filterclasses ] + ['order', 'page'] + +class ListURLNode(template.defaulttags.URLNode): + def __init__(self, kwargs): + super(ListURLNode, self).__init__(None, [], {}, False) + self.params = {} + for (k, v) in kwargs.iteritems(): + if k in list_params: + self.params[k] = v + + def render(self, context): + view_name = template.Variable('list_view.view').resolve(context) + kwargs = template.Variable('list_view.view_params') \ + .resolve(context) + + str = None + try: + str = reverse(view_name, args=[], kwargs=kwargs) + except NoReverseMatch: + try: + project_name = settings.SETTINGS_MODULE.split('.')[0] + str = reverse(project_name + '.' + view_name, + args=[], kwargs=kwargs) + except NoReverseMatch: + raise + + if str is None: + return '' + + params = [] + try: + qs_var = template.Variable('list_view.params') + params = dict(qs_var.resolve(context)) + except Exception: + pass + + for (k, v) in self.params.iteritems(): + params[smart_str(k,'ascii')] = v.resolve(context) + + if not params: + return str + + return str + '?' + '&'.join(['%s=%s' % (k, escape(v)) \ + for (k, v) in params.iteritems()]) + +@register.tag +def listurl(parser, token): + bits = token.contents.split(' ', 1) + if len(bits) < 1: + raise TemplateSyntaxError("'%s' takes at least one argument" + " (path to a view)" % bits[0]) + kwargs = {} + if len(bits) > 1: + for arg in bits[1].split(','): + if '=' in arg: + k, v = arg.split('=', 1) + k = k.strip() + kwargs[k] = parser.compile_filter(v) + else: + raise TemplateSyntaxError("'%s' requires name=value params" \ + % bits[0]) + return ListURLNode(kwargs) + +class ListFieldsNode(template.Node): + def __init__(self, params): + self.params = params + + def render(self, context): + self.view_name = template.Variable('list_view.view').resolve(context) + try: + qs_var = template.Variable('list_view.params') + params = dict(qs_var.resolve(context)) + except Exception: + pass + + params.update(self.params) + + if not params: + return '' + + str = '' + for (k, v) in params.iteritems(): + str += '' % \ + (k, escape(v)) + + return mark_safe(str) + +@register.tag +def listfields(parser, token): + bits = token.contents.split(' ', 1) + if len(bits) < 1: + raise TemplateSyntaxError("'%s' takes at least one argument" + " (path to a view)" % bits[0]) + params = {} + if len(bits) > 2: + for arg in bits[2].split(','): + if '=' in arg: + k, v = arg.split('=', 1) + k = k.strip() + params[k] = parser.compile_filter(v) + else: + raise TemplateSyntaxError("'%s' requires name=value params" \ + % bits[0]) + return ListFieldsNode(bits[1], params) + diff --git a/patchwork/templatetags/order.py b/patchwork/templatetags/order.py new file mode 100644 index 0000000..e392f03 --- /dev/null +++ b/patchwork/templatetags/order.py @@ -0,0 +1,66 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +from django import template +import re + +register = template.Library() + +@register.tag(name = 'ifpatcheditable') +def do_patch_is_editable(parser, token): + try: + tag_name, name, cur_order = token.split_contents() + except ValueError: + raise template.TemplateSyntaxError("%r tag requires two arguments" \ + % token.contents.split()[0]) + + end_tag = 'endifpatcheditable' + nodelist_true = parser.parse([end_tag, 'else']) + + token = parser.next_token() + if token.contents == 'else': + nodelist_false = parser.parse([end_tag]) + parser.delete_first_token() + else: + nodelist_false = template.NodeList() + + return EditablePatchNode(patch_var, nodelist_true, nodelist_false) + +class EditablePatchNode(template.Node): + def __init__(self, patch_var, nodelist_true, nodelist_false): + self.nodelist_true = nodelist_true + self.nodelist_false = nodelist_false + self.patch_var = template.Variable(patch_var) + self.user_var = template.Variable('user') + + def render(self, context): + try: + patch = self.patch_var.resolve(context) + user = self.user_var.resolve(context) + except template.VariableDoesNotExist: + return '' + + if not user.is_authenticated(): + return self.nodelist_false.render(context) + + if not patch.is_editable(user): + return self.nodelist_false.render(context) + + return self.nodelist_true.render(context) diff --git a/patchwork/templatetags/patch.py b/patchwork/templatetags/patch.py new file mode 100644 index 0000000..bec0cab --- /dev/null +++ b/patchwork/templatetags/patch.py @@ -0,0 +1,65 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from django import template +import re + +register = template.Library() + +@register.tag(name = 'ifpatcheditable') +def do_patch_is_editable(parser, token): + try: + tag_name, patch_var = token.split_contents() + except ValueError: + raise template.TemplateSyntaxError("%r tag requires one argument" \ + % token.contents.split()[0]) + + end_tag = 'endifpatcheditable' + nodelist_true = parser.parse([end_tag, 'else']) + + token = parser.next_token() + if token.contents == 'else': + nodelist_false = parser.parse([end_tag]) + parser.delete_first_token() + else: + nodelist_false = template.NodeList() + + return EditablePatchNode(patch_var, nodelist_true, nodelist_false) + +class EditablePatchNode(template.Node): + def __init__(self, patch_var, nodelist_true, nodelist_false): + self.nodelist_true = nodelist_true + self.nodelist_false = nodelist_false + self.patch_var = template.Variable(patch_var) + self.user_var = template.Variable('user') + + def render(self, context): + try: + patch = self.patch_var.resolve(context) + user = self.user_var.resolve(context) + except template.VariableDoesNotExist: + return '' + + if not user.is_authenticated(): + return self.nodelist_false.render(context) + + if not patch.is_editable(user): + return self.nodelist_false.render(context) + + return self.nodelist_true.render(context) diff --git a/patchwork/templatetags/person.py b/patchwork/templatetags/person.py new file mode 100644 index 0000000..c337c74 --- /dev/null +++ b/patchwork/templatetags/person.py @@ -0,0 +1,43 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from django import template +from django.utils.html import escape +from django.utils.safestring import mark_safe +from django.core.urlresolvers import reverse +from patchwork.filters import SubmitterFilter +import re + +register = template.Library() + +@register.filter +def personify(person, project): + + if person.name: + linktext = escape(person.name) + else: + linktext = escape(person.email) + + url = reverse('patchwork.views.patch.list', kwargs = {'project_id' : project.linkname}) + str = '%s' % \ + (url, SubmitterFilter.param, escape(person.id), linktext) + + return mark_safe(str) + + diff --git a/patchwork/templatetags/pwurl.py b/patchwork/templatetags/pwurl.py new file mode 100644 index 0000000..98bc1ca --- /dev/null +++ b/patchwork/templatetags/pwurl.py @@ -0,0 +1,76 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from django import template +from django.utils.html import escape +from django.utils.safestring import mark_safe +from patchwork.filters import filterclasses +import re + +register = template.Library() + +# params to preserve across views +list_params = [ c.param for c in filterclasses ] + ['order', 'page'] + +class ListURLNode(template.defaulttags.URLNode): + def __init__(self, *args, **kwargs): + super(ListURLNode, self).__init__(*args, **kwargs) + self.params = {} + for (k, v) in kwargs: + if k in list_params: + self.params[k] = v + + def render(self, context): + self.view_name = template.Variable('list_view.view') + str = super(ListURLNode, self).render(context) + if str == '': + return str + params = [] + try: + qs_var = template.Variable('list_view.params') + params = dict(qs_var.resolve(context)) + except Exception: + pass + + params.update(self.params) + + if not params: + return str + + return str + '?' + '&'.join(['%s=%s' % (k, escape(v)) \ + for (k, v) in params.iteritems()]) + +@register.tag +def listurl(parser, token): + bits = token.contents.split(' ', 1) + if len(bits) < 1: + raise TemplateSyntaxError("'%s' takes at least one argument" + " (path to a view)" % bits[0]) + args = [''] + kwargs = {} + if len(bits) > 1: + for arg in bits[2].split(','): + if '=' in arg: + k, v = arg.split('=', 1) + k = k.strip() + kwargs[k] = parser.compile_filter(v) + else: + args.append(parser.compile_filter(arg)) + return PatchworkURLNode(bits[1], args, kwargs) + diff --git a/patchwork/templatetags/syntax.py b/patchwork/templatetags/syntax.py new file mode 100644 index 0000000..abdbb4d --- /dev/null +++ b/patchwork/templatetags/syntax.py @@ -0,0 +1,75 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from django import template +from django.utils.html import escape +from django.utils.safestring import mark_safe +import re + +register = template.Library() + +def _compile(t): + (r, str) = t + return (re.compile(r, re.M | re.I), str) + +_patch_span_res = map(_compile, [ + ('^(Index:?|diff|\-\-\-|\+\+\+|\*\*\*) .*$', 'p_header'), + ('^\+.*$', 'p_add'), + ('^-.*$', 'p_del'), + ('^!.*$', 'p_mod'), + ]) + +_patch_chunk_re = \ + re.compile('^(@@ \-\d+(?:,\d+)? \+\d+(?:,\d+)? @@)(.*)$', re.M | re.I) + +_comment_span_res = map(_compile, [ + ('^\s*Signed-off-by: .*$', 'signed-off-by'), + ('^\s*Acked-by: .*$', 'acked-by'), + ('^\s*Nacked-by: .*$', 'nacked-by'), + ('^\s*Tested-by: .*$', 'tested-by'), + ('^\s*Reviewed-by: .*$', 'reviewed-by'), + ('^\s*From: .*$', 'from'), + ('^\s*>.*$', 'quote'), + ]) + +_span = '%s' + +@register.filter +def patchsyntax(patch): + content = escape(patch.content) + + for (r,cls) in _patch_span_res: + content = r.sub(lambda x: _span % (cls, x.group(0)), content) + + content = _patch_chunk_re.sub( \ + lambda x: \ + _span % ('p_chunk', x.group(1)) + ' ' + \ + _span % ('p_context', x.group(2)), \ + content) + + return mark_safe(content) + +@register.filter +def commentsyntax(comment): + content = escape(comment.content) + + for (r,cls) in _comment_span_res: + content = r.sub(lambda x: _span % (cls, x.group(0)), content) + + return mark_safe(content) diff --git a/patchwork/tests/__init__.py b/patchwork/tests/__init__.py new file mode 100644 index 0000000..85200bd --- /dev/null +++ b/patchwork/tests/__init__.py @@ -0,0 +1,34 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from patchwork.tests.test_patchparser import * +from patchwork.tests.test_encodings import * +from patchwork.tests.test_bundles import * +from patchwork.tests.test_mboxviews import * +from patchwork.tests.test_updates import * +from patchwork.tests.test_filters import * +from patchwork.tests.test_confirm import * +from patchwork.tests.test_registration import * +from patchwork.tests.test_user import * +from patchwork.tests.test_mail_settings import * +from patchwork.tests.test_notifications import * +from patchwork.tests.test_list import * +from patchwork.tests.test_person import * +from patchwork.tests.test_expiry import * +from patchwork.tests.test_xmlrpc import * diff --git a/patchwork/tests/mail/0001-git-pull-request.mbox b/patchwork/tests/mail/0001-git-pull-request.mbox new file mode 100644 index 0000000..0dbedbe --- /dev/null +++ b/patchwork/tests/mail/0001-git-pull-request.mbox @@ -0,0 +1,348 @@ +From benh@kernel.crashing.org Fri Oct 22 11:51:02 2010 +Return-Path: +X-Spam-Checker-Version: SpamAssassin 3.3.1 (2010-03-16) on bilbo.ozlabs.org +X-Spam-Level: +X-Spam-Status: No, score=0.0 required=3.0 tests=none autolearn=disabled + version=3.3.1 +X-Original-To: jk@ozlabs.org +Delivered-To: jk@ozlabs.org +Received: from bilbo.ozlabs.org (localhost [127.0.0.1]) + by ozlabs.org (Postfix) with ESMTP id ED4B3100937 + for ; Fri, 22 Oct 2010 14:51:54 +1100 (EST) +Received: by ozlabs.org (Postfix) + id BF799B70CB; Fri, 22 Oct 2010 14:51:50 +1100 (EST) +Delivered-To: linuxppc-dev@ozlabs.org +Received: from gate.crashing.org (gate.crashing.org [63.228.1.57]) + (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits)) + (Client did not present a certificate) + by ozlabs.org (Postfix) with ESMTPS id 94629B7043 + for ; Fri, 22 Oct 2010 14:51:49 +1100 (EST) +Received: from [IPv6:::1] (localhost.localdomain [127.0.0.1]) + by gate.crashing.org (8.14.1/8.13.8) with ESMTP id o9M3p3SP018234; + Thu, 21 Oct 2010 22:51:04 -0500 +Subject: [git pull] Please pull powerpc.git next branch +From: Benjamin Herrenschmidt +To: Linus Torvalds +Date: Fri, 22 Oct 2010 14:51:02 +1100 +Message-ID: <1287719462.2198.37.camel@pasglop> +Mime-Version: 1.0 +X-Mailer: Evolution 2.30.3 +Cc: linuxppc-dev list , + Andrew Morton , + Linux Kernel list +X-BeenThere: linuxppc-dev@lists.ozlabs.org +X-Mailman-Version: 2.1.13 +Precedence: list +List-Id: Linux on PowerPC Developers Mail List +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-Subscribe: , + +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: 7bit +Sender: linuxppc-dev-bounces+jk=ozlabs.org@lists.ozlabs.org +Errors-To: linuxppc-dev-bounces+jk=ozlabs.org@lists.ozlabs.org +X-UID: 11446 +X-Length: 16781 +Status: R +X-Status: N +X-KMail-EncryptionState: +X-KMail-SignatureState: +X-KMail-MDN-Sent: + +Hi Linus ! + +Here's powerpc's batch for this merge window. Mostly bits and pieces, +such as Anton doing some performance tuning left and right, and the +usual churn. One hilight is the support for the new Freescale e5500 core +(64-bit BookE). Another one is that we now wire up the whole lot of +socket calls as direct syscalls in addition to the old style indirect +method. + +Cheers, +Ben. + +The following changes since commit e10117d36ef758da0690c95ecffc09d5dd7da479: + Linus Torvalds (1): + Merge branch 'upstream-linus' of git://git.kernel.org/.../jgarzik/libata-dev + +are available in the git repository at: + + git://git.kernel.org/pub/scm/linux/kernel/git/benh/powerpc.git next + +Andreas Schwab (1): + powerpc: Remove fpscr use from [kvm_]cvt_{fd,df} + +Anton Blanchard (5): + powerpc: Optimise 64bit csum_partial + powerpc: Optimise 64bit csum_partial_copy_generic and add csum_and_copy_from_user + powerpc: Add 64bit csum_and_copy_to_user + powerpc: Feature nop out reservation clear when stcx checks address + powerpc: Check end of stack canary at oops time + +Arnd Bergmann (1): + powerpc/spufs: Use llseek in all file operations + +Benjamin Herrenschmidt (4): + powerpc/dma: Add optional platform override of dma_set_mask() + powerpc/dart_iommu: Support for 64-bit iommu bypass window on PCIe + Merge remote branch 'kumar/merge' into next + Merge remote branch 'jwb/next' into next + +Denis Kirjanov (1): + powerpc: Use is_32bit_task() helper to test 32-bit binary + +Harninder Rai (1): + powerpc/85xx: add cache-sram support + +Ian Munsie (1): + powerpc: Wire up direct socket system calls + +Ilya Yanok (1): + powerpc/mpc83xx: Support for MPC8308 P1M board + +Joe Perches (2): + powerpc: Use static const char arrays + powerpc: Remove pr_ uses of KERN_ + +Josh Boyer (1): + powerpc/44x: Update ppc44x_defconfig + +Julia Lawall (7): + powerpc/via-pmu-led.c: Add of_node_put to avoid memory leak + powerpc/maple: Add of_node_put to avoid memory leak + powerpc/powermac/pfunc_core.c: Add of_node_put to avoid memory leak + powerpc/cell: Add of_node_put to avoid memory leak + powerpc/chrp/nvram.c: Add of_node_put to avoid memory leak + powerpc/irq.c: Add of_node_put to avoid memory leak + i2c/i2c-pasemi.c: Fix unsigned return type + +Kumar Gala (11): + powerpc/ppc64e: Fix link problem when building ppc64e_defconfig + powerpc/fsl-pci: Fix MSI support on 83xx platforms + powerpc/mpc8xxx_gpio: Add support for 'qoriq-gpio' controllers + powerpc/fsl-booke: Add PCI device ids for P2040/P3041/P5010/P5020 QoirQ chips + powerpc/fsl-booke: Add p3041 DS board support + powerpc: Fix compile error with paca code on ppc64e + powerpc/fsl-booke: Add support for FSL 64-bit e5500 core + powerpc/fsl-booke: Add support for FSL Arch v1.0 MMU in setup_page_sizes + powerpc/fsl-booke64: Use TLB CAMs to cover linear mapping on FSL 64-bit chips + powerpc/fsl-booke: Add p5020 DS board support + powerpc/fsl-booke: Add e55xx (64-bit) smp defconfig + +Matthew McClintock (7): + powerpc/mm: Assume first cpu is boot_cpuid not 0 + powerpc/kexec: make masking/disabling interrupts generic + powerpc/85xx: Remove call to mpic_teardown_this_cpu in kexec + powerpc/85xx: Minor fixups for kexec on 85xx + powerpc/85xx: flush dcache before resetting cores + powerpc/fsl_soc: Search all global-utilities nodes for rstccr + powerpc/fsl_booke: Add support to boot from core other than 0 + +Michael Neuling (1): + powerpc: Move arch_sd_sibling_asym_packing() to smp.c + +Nathan Fontenot (3): + powerpc/pseries: Export device tree updating routines + powerpc/pseries: Export rtas_ibm_suspend_me() + powerpc/pseries: Partition migration in the kernel + +Nishanth Aravamudan (8): + powerpc/pci: Fix return type of BUID_{HI,LO} macros + powerpc/dma: Fix dma_iommu_dma_supported compare + powerpc/dma: Fix check for direct DMA support + powerpc/vio: Use put_device() on device_register failure + powerpc/viobus: Free TCE table on device release + powerpc/pseries: Use kmemdup + powerpc/pci: Cleanup device dma setup code + powerpc/pseries/xics: Use cpu_possible_mask rather than cpu_all_mask + +Paul Gortmaker (1): + powerpc: Fix invalid page flags in create TLB CAM path for PTE_64BIT + +Paul Mackerras (5): + powerpc: Abstract indexing of lppaca structs + powerpc: Dynamically allocate most lppaca structs + powerpc: Account time using timebase rather than PURR + powerpc/pseries: Re-enable dispatch trace log userspace interface + powerpc/perf: Fix sampling enable for PPC970 + +Scott Wood (1): + oprofile/fsl emb: Don't set MSR[PMM] until after clearing the interrupt. + +Sean MacLennan (2): + powerpc: Fix incorrect .stabs entry for copy_32.S + powerpc: mtmsrd not defined + +Shaohui Xie (1): + fsl_rio: Add comments for sRIO registers. + +Stephen Rothwell (1): + powerpc: define a compat_sys_recv cond_syscall + +Timur Tabi (5): + powerpc: export ppc_proc_freq and ppc_tb_freq as GPL symbols + powerpc/watchdog: Allow the Book-E driver to be compiled as a module + powerpc/p1022: Add probing for individual DMA channels + powerpc/85xx: add ngPIXIS FPGA device tree node to the P1022DS board + powerpc/watchdog: Make default timeout for Book-E watchdog a Kconfig option + +Tirumala Marri (1): + powerpc/44x: Add support for the AMCC APM821xx SoC + +matt mooney (1): + powerpc/Makefiles: Change to new flag variables + + arch/powerpc/boot/addnote.c | 4 +- + arch/powerpc/boot/dts/bluestone.dts | 254 +++++++++++++ + arch/powerpc/boot/dts/mpc8308_p1m.dts | 332 ++++++++++++++++ + arch/powerpc/boot/dts/p1022ds.dts | 11 + + arch/powerpc/configs/44x/bluestone_defconfig | 68 ++++ + arch/powerpc/configs/e55xx_smp_defconfig | 84 ++++ + arch/powerpc/configs/ppc44x_defconfig | 9 +- + arch/powerpc/configs/ppc64e_defconfig | 4 +- + arch/powerpc/include/asm/checksum.h | 10 + + arch/powerpc/include/asm/compat.h | 4 +- + arch/powerpc/include/asm/cputable.h | 14 +- + arch/powerpc/include/asm/dma-mapping.h | 14 +- + arch/powerpc/include/asm/elf.h | 2 +- + arch/powerpc/include/asm/exception-64s.h | 3 +- + arch/powerpc/include/asm/fsl_85xx_cache_sram.h | 48 +++ + arch/powerpc/include/asm/kexec.h | 1 + + arch/powerpc/include/asm/kvm_fpu.h | 4 +- + arch/powerpc/include/asm/lppaca.h | 29 ++ + arch/powerpc/include/asm/machdep.h | 3 + + arch/powerpc/include/asm/mmu-book3e.h | 15 + + arch/powerpc/include/asm/paca.h | 10 +- + arch/powerpc/include/asm/page_64.h | 4 +- + arch/powerpc/include/asm/ppc-pci.h | 4 +- + arch/powerpc/include/asm/ppc_asm.h | 50 ++- + arch/powerpc/include/asm/processor.h | 4 +- + arch/powerpc/include/asm/pte-common.h | 7 + + arch/powerpc/include/asm/rtas.h | 1 + + arch/powerpc/include/asm/systbl.h | 19 + + arch/powerpc/include/asm/system.h | 4 +- + arch/powerpc/include/asm/time.h | 5 - + arch/powerpc/include/asm/unistd.h | 21 +- + arch/powerpc/kernel/Makefile | 4 +- + arch/powerpc/kernel/align.c | 4 +- + arch/powerpc/kernel/asm-offsets.c | 12 +- + arch/powerpc/kernel/cpu_setup_44x.S | 1 + + arch/powerpc/kernel/cpu_setup_fsl_booke.S | 15 + + arch/powerpc/kernel/cputable.c | 43 ++- + arch/powerpc/kernel/crash.c | 13 +- + arch/powerpc/kernel/dma-iommu.c | 21 +- + arch/powerpc/kernel/dma.c | 20 +- + arch/powerpc/kernel/entry_64.S | 40 ++ + arch/powerpc/kernel/fpu.S | 10 - + arch/powerpc/kernel/head_fsl_booke.S | 10 +- + arch/powerpc/kernel/irq.c | 6 +- + arch/powerpc/kernel/lparcfg.c | 14 +- + arch/powerpc/kernel/machine_kexec.c | 24 ++ + arch/powerpc/kernel/machine_kexec_32.c | 4 + + arch/powerpc/kernel/paca.c | 70 ++++- + arch/powerpc/kernel/pci-common.c | 4 +- + arch/powerpc/kernel/ppc970-pmu.c | 2 + + arch/powerpc/kernel/process.c | 12 - + arch/powerpc/kernel/ptrace.c | 2 +- + arch/powerpc/kernel/rtas.c | 4 +- + arch/powerpc/kernel/setup_32.c | 2 +- + arch/powerpc/kernel/smp.c | 14 +- + arch/powerpc/kernel/time.c | 275 +++++++------- + arch/powerpc/kernel/traps.c | 5 + + arch/powerpc/kernel/vdso.c | 6 +- + arch/powerpc/kernel/vdso32/Makefile | 6 +- + arch/powerpc/kernel/vdso64/Makefile | 6 +- + arch/powerpc/kernel/vio.c | 10 +- + arch/powerpc/kvm/Makefile | 2 +- + arch/powerpc/kvm/book3s_paired_singles.c | 44 +-- + arch/powerpc/kvm/emulate.c | 4 +- + arch/powerpc/kvm/fpu.S | 8 - + arch/powerpc/lib/Makefile | 7 +- + arch/powerpc/lib/checksum_64.S | 482 +++++++++++++++++------- + arch/powerpc/lib/checksum_wrappers_64.c | 102 +++++ + arch/powerpc/lib/copy_32.S | 2 +- + arch/powerpc/lib/ldstfp.S | 36 +- + arch/powerpc/lib/locks.c | 4 +- + arch/powerpc/lib/sstep.c | 8 + + arch/powerpc/math-emu/Makefile | 2 +- + arch/powerpc/mm/Makefile | 6 +- + arch/powerpc/mm/fault.c | 6 + + arch/powerpc/mm/fsl_booke_mmu.c | 15 +- + arch/powerpc/mm/mmu_context_nohash.c | 6 +- + arch/powerpc/mm/mmu_decl.h | 5 +- + arch/powerpc/mm/tlb_nohash.c | 56 +++- + arch/powerpc/mm/tlb_nohash_low.S | 2 +- + arch/powerpc/oprofile/Makefile | 4 +- + arch/powerpc/oprofile/backtrace.c | 2 +- + arch/powerpc/oprofile/op_model_fsl_emb.c | 15 +- + arch/powerpc/platforms/44x/Kconfig | 16 + + arch/powerpc/platforms/44x/ppc44x_simple.c | 1 + + arch/powerpc/platforms/83xx/Kconfig | 4 +- + arch/powerpc/platforms/83xx/mpc830x_rdb.c | 3 +- + arch/powerpc/platforms/85xx/Kconfig | 28 ++- + arch/powerpc/platforms/85xx/Makefile | 2 + + arch/powerpc/platforms/85xx/p1022_ds.c | 2 + + arch/powerpc/platforms/85xx/p3041_ds.c | 64 ++++ + arch/powerpc/platforms/85xx/p5020_ds.c | 69 ++++ + arch/powerpc/platforms/85xx/smp.c | 83 ++++- + arch/powerpc/platforms/Kconfig.cputype | 8 +- + arch/powerpc/platforms/cell/ras.c | 4 +- + arch/powerpc/platforms/cell/spider-pic.c | 4 +- + arch/powerpc/platforms/cell/spufs/file.c | 18 + + arch/powerpc/platforms/chrp/nvram.c | 4 +- + arch/powerpc/platforms/iseries/Makefile | 2 +- + arch/powerpc/platforms/iseries/dt.c | 4 +- + arch/powerpc/platforms/iseries/smp.c | 2 +- + arch/powerpc/platforms/maple/setup.c | 1 + + arch/powerpc/platforms/powermac/pfunc_core.c | 9 +- + arch/powerpc/platforms/pseries/Makefile | 13 +- + arch/powerpc/platforms/pseries/dlpar.c | 7 +- + arch/powerpc/platforms/pseries/dtl.c | 224 +++++++++--- + arch/powerpc/platforms/pseries/lpar.c | 25 ++- + arch/powerpc/platforms/pseries/mobility.c | 362 ++++++++++++++++++ + arch/powerpc/platforms/pseries/pseries.h | 9 + + arch/powerpc/platforms/pseries/setup.c | 52 +++ + arch/powerpc/platforms/pseries/xics.c | 2 +- + arch/powerpc/sysdev/Makefile | 5 +- + arch/powerpc/sysdev/dart_iommu.c | 74 ++++- + arch/powerpc/sysdev/fsl_85xx_cache_ctlr.h | 101 +++++ + arch/powerpc/sysdev/fsl_85xx_cache_sram.c | 159 ++++++++ + arch/powerpc/sysdev/fsl_85xx_l2ctlr.c | 231 +++++++++++ + arch/powerpc/sysdev/fsl_msi.c | 9 +- + arch/powerpc/sysdev/fsl_pci.c | 60 +++- + arch/powerpc/sysdev/fsl_pci.h | 1 + + arch/powerpc/sysdev/fsl_rio.c | 65 ++-- + arch/powerpc/sysdev/fsl_soc.c | 20 +- + arch/powerpc/sysdev/mpc8xxx_gpio.c | 3 + + arch/powerpc/sysdev/pmi.c | 2 +- + arch/powerpc/xmon/Makefile | 4 +- + drivers/i2c/busses/i2c-pasemi.c | 2 +- + drivers/macintosh/via-pmu-led.c | 4 +- + drivers/watchdog/Kconfig | 22 +- + drivers/watchdog/booke_wdt.c | 47 ++- + include/linux/pci_ids.h | 8 + + kernel/sys_ni.c | 1 + + 130 files changed, 3676 insertions(+), 683 deletions(-) + create mode 100644 arch/powerpc/boot/dts/bluestone.dts + create mode 100644 arch/powerpc/boot/dts/mpc8308_p1m.dts + create mode 100644 arch/powerpc/configs/44x/bluestone_defconfig + create mode 100644 arch/powerpc/configs/e55xx_smp_defconfig + create mode 100644 arch/powerpc/include/asm/fsl_85xx_cache_sram.h + create mode 100644 arch/powerpc/lib/checksum_wrappers_64.c + create mode 100644 arch/powerpc/platforms/85xx/p3041_ds.c + create mode 100644 arch/powerpc/platforms/85xx/p5020_ds.c + create mode 100644 arch/powerpc/platforms/pseries/mobility.c + create mode 100644 arch/powerpc/sysdev/fsl_85xx_cache_ctlr.h + create mode 100644 arch/powerpc/sysdev/fsl_85xx_cache_sram.c + create mode 100644 arch/powerpc/sysdev/fsl_85xx_l2ctlr.c + + +_______________________________________________ +Linuxppc-dev mailing list +Linuxppc-dev@lists.ozlabs.org +https://lists.ozlabs.org/listinfo/linuxppc-dev diff --git a/patchwork/tests/mail/0002-git-pull-request-wrapped.mbox b/patchwork/tests/mail/0002-git-pull-request-wrapped.mbox new file mode 100644 index 0000000..d3ccee1 --- /dev/null +++ b/patchwork/tests/mail/0002-git-pull-request-wrapped.mbox @@ -0,0 +1,349 @@ +From benh@kernel.crashing.org Fri Oct 22 11:51:02 2010 +Return-Path: +X-Spam-Checker-Version: SpamAssassin 3.3.1 (2010-03-16) on bilbo.ozlabs.org +X-Spam-Level: +X-Spam-Status: No, score=0.0 required=3.0 tests=none autolearn=disabled + version=3.3.1 +X-Original-To: jk@ozlabs.org +Delivered-To: jk@ozlabs.org +Received: from bilbo.ozlabs.org (localhost [127.0.0.1]) + by ozlabs.org (Postfix) with ESMTP id ED4B3100937 + for ; Fri, 22 Oct 2010 14:51:54 +1100 (EST) +Received: by ozlabs.org (Postfix) + id BF799B70CB; Fri, 22 Oct 2010 14:51:50 +1100 (EST) +Delivered-To: linuxppc-dev@ozlabs.org +Received: from gate.crashing.org (gate.crashing.org [63.228.1.57]) + (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits)) + (Client did not present a certificate) + by ozlabs.org (Postfix) with ESMTPS id 94629B7043 + for ; Fri, 22 Oct 2010 14:51:49 +1100 (EST) +Received: from [IPv6:::1] (localhost.localdomain [127.0.0.1]) + by gate.crashing.org (8.14.1/8.13.8) with ESMTP id o9M3p3SP018234; + Thu, 21 Oct 2010 22:51:04 -0500 +Subject: [git pull] Please pull powerpc.git next branch +From: Benjamin Herrenschmidt +To: Linus Torvalds +Date: Fri, 22 Oct 2010 14:51:02 +1100 +Message-ID: <1287719462.2198.37.camel@pasglop> +Mime-Version: 1.0 +X-Mailer: Evolution 2.30.3 +Cc: linuxppc-dev list , + Andrew Morton , + Linux Kernel list +X-BeenThere: linuxppc-dev@lists.ozlabs.org +X-Mailman-Version: 2.1.13 +Precedence: list +List-Id: Linux on PowerPC Developers Mail List +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-Subscribe: , + +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: 7bit +Sender: linuxppc-dev-bounces+jk=ozlabs.org@lists.ozlabs.org +Errors-To: linuxppc-dev-bounces+jk=ozlabs.org@lists.ozlabs.org +X-UID: 11446 +X-Length: 16781 +Status: R +X-Status: N +X-KMail-EncryptionState: +X-KMail-SignatureState: +X-KMail-MDN-Sent: + +Hi Linus ! + +Here's powerpc's batch for this merge window. Mostly bits and pieces, +such as Anton doing some performance tuning left and right, and the +usual churn. One hilight is the support for the new Freescale e5500 core +(64-bit BookE). Another one is that we now wire up the whole lot of +socket calls as direct syscalls in addition to the old style indirect +method. + +Cheers, +Ben. + +The following changes since commit +e10117d36ef758da0690c95ecffc09d5dd7da479: + Linus Torvalds (1): + Merge branch 'upstream-linus' of git://git.kernel.org/.../jgarzik/libata-dev + +are available in the git repository at: + + git://git.kernel.org/pub/scm/linux/kernel/git/benh/powerpc.git next + +Andreas Schwab (1): + powerpc: Remove fpscr use from [kvm_]cvt_{fd,df} + +Anton Blanchard (5): + powerpc: Optimise 64bit csum_partial + powerpc: Optimise 64bit csum_partial_copy_generic and add csum_and_copy_from_user + powerpc: Add 64bit csum_and_copy_to_user + powerpc: Feature nop out reservation clear when stcx checks address + powerpc: Check end of stack canary at oops time + +Arnd Bergmann (1): + powerpc/spufs: Use llseek in all file operations + +Benjamin Herrenschmidt (4): + powerpc/dma: Add optional platform override of dma_set_mask() + powerpc/dart_iommu: Support for 64-bit iommu bypass window on PCIe + Merge remote branch 'kumar/merge' into next + Merge remote branch 'jwb/next' into next + +Denis Kirjanov (1): + powerpc: Use is_32bit_task() helper to test 32-bit binary + +Harninder Rai (1): + powerpc/85xx: add cache-sram support + +Ian Munsie (1): + powerpc: Wire up direct socket system calls + +Ilya Yanok (1): + powerpc/mpc83xx: Support for MPC8308 P1M board + +Joe Perches (2): + powerpc: Use static const char arrays + powerpc: Remove pr_ uses of KERN_ + +Josh Boyer (1): + powerpc/44x: Update ppc44x_defconfig + +Julia Lawall (7): + powerpc/via-pmu-led.c: Add of_node_put to avoid memory leak + powerpc/maple: Add of_node_put to avoid memory leak + powerpc/powermac/pfunc_core.c: Add of_node_put to avoid memory leak + powerpc/cell: Add of_node_put to avoid memory leak + powerpc/chrp/nvram.c: Add of_node_put to avoid memory leak + powerpc/irq.c: Add of_node_put to avoid memory leak + i2c/i2c-pasemi.c: Fix unsigned return type + +Kumar Gala (11): + powerpc/ppc64e: Fix link problem when building ppc64e_defconfig + powerpc/fsl-pci: Fix MSI support on 83xx platforms + powerpc/mpc8xxx_gpio: Add support for 'qoriq-gpio' controllers + powerpc/fsl-booke: Add PCI device ids for P2040/P3041/P5010/P5020 QoirQ chips + powerpc/fsl-booke: Add p3041 DS board support + powerpc: Fix compile error with paca code on ppc64e + powerpc/fsl-booke: Add support for FSL 64-bit e5500 core + powerpc/fsl-booke: Add support for FSL Arch v1.0 MMU in setup_page_sizes + powerpc/fsl-booke64: Use TLB CAMs to cover linear mapping on FSL 64-bit chips + powerpc/fsl-booke: Add p5020 DS board support + powerpc/fsl-booke: Add e55xx (64-bit) smp defconfig + +Matthew McClintock (7): + powerpc/mm: Assume first cpu is boot_cpuid not 0 + powerpc/kexec: make masking/disabling interrupts generic + powerpc/85xx: Remove call to mpic_teardown_this_cpu in kexec + powerpc/85xx: Minor fixups for kexec on 85xx + powerpc/85xx: flush dcache before resetting cores + powerpc/fsl_soc: Search all global-utilities nodes for rstccr + powerpc/fsl_booke: Add support to boot from core other than 0 + +Michael Neuling (1): + powerpc: Move arch_sd_sibling_asym_packing() to smp.c + +Nathan Fontenot (3): + powerpc/pseries: Export device tree updating routines + powerpc/pseries: Export rtas_ibm_suspend_me() + powerpc/pseries: Partition migration in the kernel + +Nishanth Aravamudan (8): + powerpc/pci: Fix return type of BUID_{HI,LO} macros + powerpc/dma: Fix dma_iommu_dma_supported compare + powerpc/dma: Fix check for direct DMA support + powerpc/vio: Use put_device() on device_register failure + powerpc/viobus: Free TCE table on device release + powerpc/pseries: Use kmemdup + powerpc/pci: Cleanup device dma setup code + powerpc/pseries/xics: Use cpu_possible_mask rather than cpu_all_mask + +Paul Gortmaker (1): + powerpc: Fix invalid page flags in create TLB CAM path for PTE_64BIT + +Paul Mackerras (5): + powerpc: Abstract indexing of lppaca structs + powerpc: Dynamically allocate most lppaca structs + powerpc: Account time using timebase rather than PURR + powerpc/pseries: Re-enable dispatch trace log userspace interface + powerpc/perf: Fix sampling enable for PPC970 + +Scott Wood (1): + oprofile/fsl emb: Don't set MSR[PMM] until after clearing the interrupt. + +Sean MacLennan (2): + powerpc: Fix incorrect .stabs entry for copy_32.S + powerpc: mtmsrd not defined + +Shaohui Xie (1): + fsl_rio: Add comments for sRIO registers. + +Stephen Rothwell (1): + powerpc: define a compat_sys_recv cond_syscall + +Timur Tabi (5): + powerpc: export ppc_proc_freq and ppc_tb_freq as GPL symbols + powerpc/watchdog: Allow the Book-E driver to be compiled as a module + powerpc/p1022: Add probing for individual DMA channels + powerpc/85xx: add ngPIXIS FPGA device tree node to the P1022DS board + powerpc/watchdog: Make default timeout for Book-E watchdog a Kconfig option + +Tirumala Marri (1): + powerpc/44x: Add support for the AMCC APM821xx SoC + +matt mooney (1): + powerpc/Makefiles: Change to new flag variables + + arch/powerpc/boot/addnote.c | 4 +- + arch/powerpc/boot/dts/bluestone.dts | 254 +++++++++++++ + arch/powerpc/boot/dts/mpc8308_p1m.dts | 332 ++++++++++++++++ + arch/powerpc/boot/dts/p1022ds.dts | 11 + + arch/powerpc/configs/44x/bluestone_defconfig | 68 ++++ + arch/powerpc/configs/e55xx_smp_defconfig | 84 ++++ + arch/powerpc/configs/ppc44x_defconfig | 9 +- + arch/powerpc/configs/ppc64e_defconfig | 4 +- + arch/powerpc/include/asm/checksum.h | 10 + + arch/powerpc/include/asm/compat.h | 4 +- + arch/powerpc/include/asm/cputable.h | 14 +- + arch/powerpc/include/asm/dma-mapping.h | 14 +- + arch/powerpc/include/asm/elf.h | 2 +- + arch/powerpc/include/asm/exception-64s.h | 3 +- + arch/powerpc/include/asm/fsl_85xx_cache_sram.h | 48 +++ + arch/powerpc/include/asm/kexec.h | 1 + + arch/powerpc/include/asm/kvm_fpu.h | 4 +- + arch/powerpc/include/asm/lppaca.h | 29 ++ + arch/powerpc/include/asm/machdep.h | 3 + + arch/powerpc/include/asm/mmu-book3e.h | 15 + + arch/powerpc/include/asm/paca.h | 10 +- + arch/powerpc/include/asm/page_64.h | 4 +- + arch/powerpc/include/asm/ppc-pci.h | 4 +- + arch/powerpc/include/asm/ppc_asm.h | 50 ++- + arch/powerpc/include/asm/processor.h | 4 +- + arch/powerpc/include/asm/pte-common.h | 7 + + arch/powerpc/include/asm/rtas.h | 1 + + arch/powerpc/include/asm/systbl.h | 19 + + arch/powerpc/include/asm/system.h | 4 +- + arch/powerpc/include/asm/time.h | 5 - + arch/powerpc/include/asm/unistd.h | 21 +- + arch/powerpc/kernel/Makefile | 4 +- + arch/powerpc/kernel/align.c | 4 +- + arch/powerpc/kernel/asm-offsets.c | 12 +- + arch/powerpc/kernel/cpu_setup_44x.S | 1 + + arch/powerpc/kernel/cpu_setup_fsl_booke.S | 15 + + arch/powerpc/kernel/cputable.c | 43 ++- + arch/powerpc/kernel/crash.c | 13 +- + arch/powerpc/kernel/dma-iommu.c | 21 +- + arch/powerpc/kernel/dma.c | 20 +- + arch/powerpc/kernel/entry_64.S | 40 ++ + arch/powerpc/kernel/fpu.S | 10 - + arch/powerpc/kernel/head_fsl_booke.S | 10 +- + arch/powerpc/kernel/irq.c | 6 +- + arch/powerpc/kernel/lparcfg.c | 14 +- + arch/powerpc/kernel/machine_kexec.c | 24 ++ + arch/powerpc/kernel/machine_kexec_32.c | 4 + + arch/powerpc/kernel/paca.c | 70 ++++- + arch/powerpc/kernel/pci-common.c | 4 +- + arch/powerpc/kernel/ppc970-pmu.c | 2 + + arch/powerpc/kernel/process.c | 12 - + arch/powerpc/kernel/ptrace.c | 2 +- + arch/powerpc/kernel/rtas.c | 4 +- + arch/powerpc/kernel/setup_32.c | 2 +- + arch/powerpc/kernel/smp.c | 14 +- + arch/powerpc/kernel/time.c | 275 +++++++------- + arch/powerpc/kernel/traps.c | 5 + + arch/powerpc/kernel/vdso.c | 6 +- + arch/powerpc/kernel/vdso32/Makefile | 6 +- + arch/powerpc/kernel/vdso64/Makefile | 6 +- + arch/powerpc/kernel/vio.c | 10 +- + arch/powerpc/kvm/Makefile | 2 +- + arch/powerpc/kvm/book3s_paired_singles.c | 44 +-- + arch/powerpc/kvm/emulate.c | 4 +- + arch/powerpc/kvm/fpu.S | 8 - + arch/powerpc/lib/Makefile | 7 +- + arch/powerpc/lib/checksum_64.S | 482 +++++++++++++++++------- + arch/powerpc/lib/checksum_wrappers_64.c | 102 +++++ + arch/powerpc/lib/copy_32.S | 2 +- + arch/powerpc/lib/ldstfp.S | 36 +- + arch/powerpc/lib/locks.c | 4 +- + arch/powerpc/lib/sstep.c | 8 + + arch/powerpc/math-emu/Makefile | 2 +- + arch/powerpc/mm/Makefile | 6 +- + arch/powerpc/mm/fault.c | 6 + + arch/powerpc/mm/fsl_booke_mmu.c | 15 +- + arch/powerpc/mm/mmu_context_nohash.c | 6 +- + arch/powerpc/mm/mmu_decl.h | 5 +- + arch/powerpc/mm/tlb_nohash.c | 56 +++- + arch/powerpc/mm/tlb_nohash_low.S | 2 +- + arch/powerpc/oprofile/Makefile | 4 +- + arch/powerpc/oprofile/backtrace.c | 2 +- + arch/powerpc/oprofile/op_model_fsl_emb.c | 15 +- + arch/powerpc/platforms/44x/Kconfig | 16 + + arch/powerpc/platforms/44x/ppc44x_simple.c | 1 + + arch/powerpc/platforms/83xx/Kconfig | 4 +- + arch/powerpc/platforms/83xx/mpc830x_rdb.c | 3 +- + arch/powerpc/platforms/85xx/Kconfig | 28 ++- + arch/powerpc/platforms/85xx/Makefile | 2 + + arch/powerpc/platforms/85xx/p1022_ds.c | 2 + + arch/powerpc/platforms/85xx/p3041_ds.c | 64 ++++ + arch/powerpc/platforms/85xx/p5020_ds.c | 69 ++++ + arch/powerpc/platforms/85xx/smp.c | 83 ++++- + arch/powerpc/platforms/Kconfig.cputype | 8 +- + arch/powerpc/platforms/cell/ras.c | 4 +- + arch/powerpc/platforms/cell/spider-pic.c | 4 +- + arch/powerpc/platforms/cell/spufs/file.c | 18 + + arch/powerpc/platforms/chrp/nvram.c | 4 +- + arch/powerpc/platforms/iseries/Makefile | 2 +- + arch/powerpc/platforms/iseries/dt.c | 4 +- + arch/powerpc/platforms/iseries/smp.c | 2 +- + arch/powerpc/platforms/maple/setup.c | 1 + + arch/powerpc/platforms/powermac/pfunc_core.c | 9 +- + arch/powerpc/platforms/pseries/Makefile | 13 +- + arch/powerpc/platforms/pseries/dlpar.c | 7 +- + arch/powerpc/platforms/pseries/dtl.c | 224 +++++++++--- + arch/powerpc/platforms/pseries/lpar.c | 25 ++- + arch/powerpc/platforms/pseries/mobility.c | 362 ++++++++++++++++++ + arch/powerpc/platforms/pseries/pseries.h | 9 + + arch/powerpc/platforms/pseries/setup.c | 52 +++ + arch/powerpc/platforms/pseries/xics.c | 2 +- + arch/powerpc/sysdev/Makefile | 5 +- + arch/powerpc/sysdev/dart_iommu.c | 74 ++++- + arch/powerpc/sysdev/fsl_85xx_cache_ctlr.h | 101 +++++ + arch/powerpc/sysdev/fsl_85xx_cache_sram.c | 159 ++++++++ + arch/powerpc/sysdev/fsl_85xx_l2ctlr.c | 231 +++++++++++ + arch/powerpc/sysdev/fsl_msi.c | 9 +- + arch/powerpc/sysdev/fsl_pci.c | 60 +++- + arch/powerpc/sysdev/fsl_pci.h | 1 + + arch/powerpc/sysdev/fsl_rio.c | 65 ++-- + arch/powerpc/sysdev/fsl_soc.c | 20 +- + arch/powerpc/sysdev/mpc8xxx_gpio.c | 3 + + arch/powerpc/sysdev/pmi.c | 2 +- + arch/powerpc/xmon/Makefile | 4 +- + drivers/i2c/busses/i2c-pasemi.c | 2 +- + drivers/macintosh/via-pmu-led.c | 4 +- + drivers/watchdog/Kconfig | 22 +- + drivers/watchdog/booke_wdt.c | 47 ++- + include/linux/pci_ids.h | 8 + + kernel/sys_ni.c | 1 + + 130 files changed, 3676 insertions(+), 683 deletions(-) + create mode 100644 arch/powerpc/boot/dts/bluestone.dts + create mode 100644 arch/powerpc/boot/dts/mpc8308_p1m.dts + create mode 100644 arch/powerpc/configs/44x/bluestone_defconfig + create mode 100644 arch/powerpc/configs/e55xx_smp_defconfig + create mode 100644 arch/powerpc/include/asm/fsl_85xx_cache_sram.h + create mode 100644 arch/powerpc/lib/checksum_wrappers_64.c + create mode 100644 arch/powerpc/platforms/85xx/p3041_ds.c + create mode 100644 arch/powerpc/platforms/85xx/p5020_ds.c + create mode 100644 arch/powerpc/platforms/pseries/mobility.c + create mode 100644 arch/powerpc/sysdev/fsl_85xx_cache_ctlr.h + create mode 100644 arch/powerpc/sysdev/fsl_85xx_cache_sram.c + create mode 100644 arch/powerpc/sysdev/fsl_85xx_l2ctlr.c + + +_______________________________________________ +Linuxppc-dev mailing list +Linuxppc-dev@lists.ozlabs.org +https://lists.ozlabs.org/listinfo/linuxppc-dev diff --git a/patchwork/tests/mail/0003-git-pull-request-with-diff.mbox b/patchwork/tests/mail/0003-git-pull-request-with-diff.mbox new file mode 100644 index 0000000..b4d578c --- /dev/null +++ b/patchwork/tests/mail/0003-git-pull-request-with-diff.mbox @@ -0,0 +1,141 @@ +From benh@kernel.crashing.org Fri Oct 22 11:51:02 2010 +Return-Path: +X-Spam-Checker-Version: SpamAssassin 3.3.1 (2010-03-16) on bilbo.ozlabs.org +X-Spam-Level: +X-Spam-Status: No, score=0.0 required=3.0 tests=none autolearn=disabled + version=3.3.1 +X-Original-To: jk@ozlabs.org +Delivered-To: jk@ozlabs.org +Received: from bilbo.ozlabs.org (localhost [127.0.0.1]) + by ozlabs.org (Postfix) with ESMTP id ED4B3100937 + for ; Fri, 22 Oct 2010 14:51:54 +1100 (EST) +Received: by ozlabs.org (Postfix) + id BF799B70CB; Fri, 22 Oct 2010 14:51:50 +1100 (EST) +Delivered-To: linuxppc-dev@ozlabs.org +Received: from gate.crashing.org (gate.crashing.org [63.228.1.57]) + (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits)) + (Client did not present a certificate) + by ozlabs.org (Postfix) with ESMTPS id 94629B7043 + for ; Fri, 22 Oct 2010 14:51:49 +1100 (EST) +Received: from [IPv6:::1] (localhost.localdomain [127.0.0.1]) + by gate.crashing.org (8.14.1/8.13.8) with ESMTP id o9M3p3SP018234; + Thu, 21 Oct 2010 22:51:04 -0500 +Subject: [git pull] Please pull powerpc.git next branch +From: Benjamin Herrenschmidt +To: Linus Torvalds +Date: Fri, 22 Oct 2010 14:51:02 +1100 +Message-ID: <1287719462.2198.37.camel@pasglop> +Mime-Version: 1.0 +X-Mailer: Evolution 2.30.3 +Cc: linuxppc-dev list , + Andrew Morton , + Linux Kernel list +X-BeenThere: linuxppc-dev@lists.ozlabs.org +X-Mailman-Version: 2.1.13 +Precedence: list +List-Id: Linux on PowerPC Developers Mail List +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-Subscribe: , + +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: 7bit +Sender: linuxppc-dev-bounces+jk=ozlabs.org@lists.ozlabs.org +Errors-To: linuxppc-dev-bounces+jk=ozlabs.org@lists.ozlabs.org +X-UID: 11446 +X-Length: 16781 +Status: R +X-Status: N +X-KMail-EncryptionState: +X-KMail-SignatureState: +X-KMail-MDN-Sent: + +The following changes since commit e10117d36ef758da0690c95ecffc09d5dd7da479: + Linus Torvalds (1): + Merge branch 'upstream-linus' of git://git.kernel.org/.../jgarzik/libata-dev + +are available in the git repository at: + + git://git.kernel.org/pub/scm/linux/kernel/git/tip/linux-2.6-tip.git x86-fixes-for-linus + +------------------> +H. Peter Anvin (1): + x86-32: Make sure the stack is set up before we use it +Matthieu CASTET (1): + x86, nx: Don't force pages RW when setting NX bits + +Suresh Siddha (1): + x86, mtrr: Avoid MTRR reprogramming on BP during boot on UP platforms + + + arch/x86/include/asm/smp.h | 5 +---- + arch/x86/kernel/acpi/sleep.c | 2 +- + arch/x86/kernel/cpu/mtrr/main.c | 10 +++++++++- + arch/x86/kernel/head_32.S | 30 +++++++++++++----------------- + arch/x86/kernel/smpboot.c | 4 ++-- + arch/x86/mm/pageattr.c | 8 -------- + 6 files changed, 26 insertions(+), 33 deletions(-) +diff --git a/arch/x86/include/asm/smp.h b/arch/x86/include/asm/smp.h +index 4c2f63c..1f46951 100644 +--- a/arch/x86/include/asm/smp.h ++++ b/arch/x86/include/asm/smp.h +@@ -40,10 +40,7 @@ DECLARE_EARLY_PER_CPU(u16, x86_cpu_to_apicid); + DECLARE_EARLY_PER_CPU(u16, x86_bios_cpu_apicid); + + /* Static state in head.S used to set up a CPU */ +-extern struct { +- void *sp; +- unsigned short ss; +-} stack_start; ++extern unsigned long stack_start; /* Initial stack pointer address */ + + struct smp_ops { + void (*smp_prepare_boot_cpu)(void); +diff --git a/arch/x86/kernel/acpi/sleep.c b/arch/x86/kernel/acpi/sleep.c +index 69fd72a..4d9ebba 100644 +--- a/arch/x86/kernel/acpi/sleep.c ++++ b/arch/x86/kernel/acpi/sleep.c +@@ -100,7 +100,7 @@ int acpi_save_state_mem(void) + #else /* CONFIG_64BIT */ + header->trampoline_segment = setup_trampoline() >> 4; + #ifdef CONFIG_SMP +- stack_start.sp = temp_stack + sizeof(temp_stack); ++ stack_start = (unsigned long)temp_stack + sizeof(temp_stack); + early_gdt_descr.address = + (unsigned long)get_cpu_gdt_table(smp_processor_id()); + initial_gs = per_cpu_offset(smp_processor_id()); +diff --git a/arch/x86/kernel/cpu/mtrr/main.c b/arch/x86/kernel/cpu/mtrr/main.c +index 01c0f3e..bebabec 100644 +--- a/arch/x86/kernel/cpu/mtrr/main.c ++++ b/arch/x86/kernel/cpu/mtrr/main.c +@@ -793,13 +793,21 @@ void set_mtrr_aps_delayed_init(void) + } + + /* +- * MTRR initialization for all AP's ++ * Delayed MTRR initialization for all AP's + */ + void mtrr_aps_init(void) + { + if (!use_intel()) + return; + ++ /* ++ * Check if someone has requested the delay of AP MTRR initialization, ++ * by doing set_mtrr_aps_delayed_init(), prior to this point. If not, ++ * then we are done. ++ */ ++ if (!mtrr_aps_delayed_init) ++ return; ++ + set_mtrr(~0U, 0, 0, 0); + mtrr_aps_delayed_init = false; + } +_______________________________________________ +Linuxppc-dev mailing list +Linuxppc-dev@lists.ozlabs.org +https://lists.ozlabs.org/listinfo/linuxppc-dev diff --git a/patchwork/tests/mail/0004-git-pull-request-git+ssh.mbox b/patchwork/tests/mail/0004-git-pull-request-git+ssh.mbox new file mode 100644 index 0000000..da96465 --- /dev/null +++ b/patchwork/tests/mail/0004-git-pull-request-git+ssh.mbox @@ -0,0 +1,348 @@ +From benh@kernel.crashing.org Fri Oct 22 11:51:02 2010 +Return-Path: +X-Spam-Checker-Version: SpamAssassin 3.3.1 (2010-03-16) on bilbo.ozlabs.org +X-Spam-Level: +X-Spam-Status: No, score=0.0 required=3.0 tests=none autolearn=disabled + version=3.3.1 +X-Original-To: jk@ozlabs.org +Delivered-To: jk@ozlabs.org +Received: from bilbo.ozlabs.org (localhost [127.0.0.1]) + by ozlabs.org (Postfix) with ESMTP id ED4B3100937 + for ; Fri, 22 Oct 2010 14:51:54 +1100 (EST) +Received: by ozlabs.org (Postfix) + id BF799B70CB; Fri, 22 Oct 2010 14:51:50 +1100 (EST) +Delivered-To: linuxppc-dev@ozlabs.org +Received: from gate.crashing.org (gate.crashing.org [63.228.1.57]) + (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits)) + (Client did not present a certificate) + by ozlabs.org (Postfix) with ESMTPS id 94629B7043 + for ; Fri, 22 Oct 2010 14:51:49 +1100 (EST) +Received: from [IPv6:::1] (localhost.localdomain [127.0.0.1]) + by gate.crashing.org (8.14.1/8.13.8) with ESMTP id o9M3p3SP018234; + Thu, 21 Oct 2010 22:51:04 -0500 +Subject: [git pull] Please pull powerpc.git next branch +From: Benjamin Herrenschmidt +To: Linus Torvalds +Date: Fri, 22 Oct 2010 14:51:02 +1100 +Message-ID: <1287719462.2198.37.camel@pasglop> +Mime-Version: 1.0 +X-Mailer: Evolution 2.30.3 +Cc: linuxppc-dev list , + Andrew Morton , + Linux Kernel list +X-BeenThere: linuxppc-dev@lists.ozlabs.org +X-Mailman-Version: 2.1.13 +Precedence: list +List-Id: Linux on PowerPC Developers Mail List +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-Subscribe: , + +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: 7bit +Sender: linuxppc-dev-bounces+jk=ozlabs.org@lists.ozlabs.org +Errors-To: linuxppc-dev-bounces+jk=ozlabs.org@lists.ozlabs.org +X-UID: 11446 +X-Length: 16781 +Status: R +X-Status: N +X-KMail-EncryptionState: +X-KMail-SignatureState: +X-KMail-MDN-Sent: + +Hi Linus ! + +Here's powerpc's batch for this merge window. Mostly bits and pieces, +such as Anton doing some performance tuning left and right, and the +usual churn. One hilight is the support for the new Freescale e5500 core +(64-bit BookE). Another one is that we now wire up the whole lot of +socket calls as direct syscalls in addition to the old style indirect +method. + +Cheers, +Ben. + +The following changes since commit e10117d36ef758da0690c95ecffc09d5dd7da479: + Linus Torvalds (1): + Merge branch 'upstream-linus' of git://git.kernel.org/.../jgarzik/libata-dev + +are available in the git repository at: + + git+ssh://git.kernel.org/pub/scm/linux/kernel/git/benh/powerpc.git next + +Andreas Schwab (1): + powerpc: Remove fpscr use from [kvm_]cvt_{fd,df} + +Anton Blanchard (5): + powerpc: Optimise 64bit csum_partial + powerpc: Optimise 64bit csum_partial_copy_generic and add csum_and_copy_from_user + powerpc: Add 64bit csum_and_copy_to_user + powerpc: Feature nop out reservation clear when stcx checks address + powerpc: Check end of stack canary at oops time + +Arnd Bergmann (1): + powerpc/spufs: Use llseek in all file operations + +Benjamin Herrenschmidt (4): + powerpc/dma: Add optional platform override of dma_set_mask() + powerpc/dart_iommu: Support for 64-bit iommu bypass window on PCIe + Merge remote branch 'kumar/merge' into next + Merge remote branch 'jwb/next' into next + +Denis Kirjanov (1): + powerpc: Use is_32bit_task() helper to test 32-bit binary + +Harninder Rai (1): + powerpc/85xx: add cache-sram support + +Ian Munsie (1): + powerpc: Wire up direct socket system calls + +Ilya Yanok (1): + powerpc/mpc83xx: Support for MPC8308 P1M board + +Joe Perches (2): + powerpc: Use static const char arrays + powerpc: Remove pr_ uses of KERN_ + +Josh Boyer (1): + powerpc/44x: Update ppc44x_defconfig + +Julia Lawall (7): + powerpc/via-pmu-led.c: Add of_node_put to avoid memory leak + powerpc/maple: Add of_node_put to avoid memory leak + powerpc/powermac/pfunc_core.c: Add of_node_put to avoid memory leak + powerpc/cell: Add of_node_put to avoid memory leak + powerpc/chrp/nvram.c: Add of_node_put to avoid memory leak + powerpc/irq.c: Add of_node_put to avoid memory leak + i2c/i2c-pasemi.c: Fix unsigned return type + +Kumar Gala (11): + powerpc/ppc64e: Fix link problem when building ppc64e_defconfig + powerpc/fsl-pci: Fix MSI support on 83xx platforms + powerpc/mpc8xxx_gpio: Add support for 'qoriq-gpio' controllers + powerpc/fsl-booke: Add PCI device ids for P2040/P3041/P5010/P5020 QoirQ chips + powerpc/fsl-booke: Add p3041 DS board support + powerpc: Fix compile error with paca code on ppc64e + powerpc/fsl-booke: Add support for FSL 64-bit e5500 core + powerpc/fsl-booke: Add support for FSL Arch v1.0 MMU in setup_page_sizes + powerpc/fsl-booke64: Use TLB CAMs to cover linear mapping on FSL 64-bit chips + powerpc/fsl-booke: Add p5020 DS board support + powerpc/fsl-booke: Add e55xx (64-bit) smp defconfig + +Matthew McClintock (7): + powerpc/mm: Assume first cpu is boot_cpuid not 0 + powerpc/kexec: make masking/disabling interrupts generic + powerpc/85xx: Remove call to mpic_teardown_this_cpu in kexec + powerpc/85xx: Minor fixups for kexec on 85xx + powerpc/85xx: flush dcache before resetting cores + powerpc/fsl_soc: Search all global-utilities nodes for rstccr + powerpc/fsl_booke: Add support to boot from core other than 0 + +Michael Neuling (1): + powerpc: Move arch_sd_sibling_asym_packing() to smp.c + +Nathan Fontenot (3): + powerpc/pseries: Export device tree updating routines + powerpc/pseries: Export rtas_ibm_suspend_me() + powerpc/pseries: Partition migration in the kernel + +Nishanth Aravamudan (8): + powerpc/pci: Fix return type of BUID_{HI,LO} macros + powerpc/dma: Fix dma_iommu_dma_supported compare + powerpc/dma: Fix check for direct DMA support + powerpc/vio: Use put_device() on device_register failure + powerpc/viobus: Free TCE table on device release + powerpc/pseries: Use kmemdup + powerpc/pci: Cleanup device dma setup code + powerpc/pseries/xics: Use cpu_possible_mask rather than cpu_all_mask + +Paul Gortmaker (1): + powerpc: Fix invalid page flags in create TLB CAM path for PTE_64BIT + +Paul Mackerras (5): + powerpc: Abstract indexing of lppaca structs + powerpc: Dynamically allocate most lppaca structs + powerpc: Account time using timebase rather than PURR + powerpc/pseries: Re-enable dispatch trace log userspace interface + powerpc/perf: Fix sampling enable for PPC970 + +Scott Wood (1): + oprofile/fsl emb: Don't set MSR[PMM] until after clearing the interrupt. + +Sean MacLennan (2): + powerpc: Fix incorrect .stabs entry for copy_32.S + powerpc: mtmsrd not defined + +Shaohui Xie (1): + fsl_rio: Add comments for sRIO registers. + +Stephen Rothwell (1): + powerpc: define a compat_sys_recv cond_syscall + +Timur Tabi (5): + powerpc: export ppc_proc_freq and ppc_tb_freq as GPL symbols + powerpc/watchdog: Allow the Book-E driver to be compiled as a module + powerpc/p1022: Add probing for individual DMA channels + powerpc/85xx: add ngPIXIS FPGA device tree node to the P1022DS board + powerpc/watchdog: Make default timeout for Book-E watchdog a Kconfig option + +Tirumala Marri (1): + powerpc/44x: Add support for the AMCC APM821xx SoC + +matt mooney (1): + powerpc/Makefiles: Change to new flag variables + + arch/powerpc/boot/addnote.c | 4 +- + arch/powerpc/boot/dts/bluestone.dts | 254 +++++++++++++ + arch/powerpc/boot/dts/mpc8308_p1m.dts | 332 ++++++++++++++++ + arch/powerpc/boot/dts/p1022ds.dts | 11 + + arch/powerpc/configs/44x/bluestone_defconfig | 68 ++++ + arch/powerpc/configs/e55xx_smp_defconfig | 84 ++++ + arch/powerpc/configs/ppc44x_defconfig | 9 +- + arch/powerpc/configs/ppc64e_defconfig | 4 +- + arch/powerpc/include/asm/checksum.h | 10 + + arch/powerpc/include/asm/compat.h | 4 +- + arch/powerpc/include/asm/cputable.h | 14 +- + arch/powerpc/include/asm/dma-mapping.h | 14 +- + arch/powerpc/include/asm/elf.h | 2 +- + arch/powerpc/include/asm/exception-64s.h | 3 +- + arch/powerpc/include/asm/fsl_85xx_cache_sram.h | 48 +++ + arch/powerpc/include/asm/kexec.h | 1 + + arch/powerpc/include/asm/kvm_fpu.h | 4 +- + arch/powerpc/include/asm/lppaca.h | 29 ++ + arch/powerpc/include/asm/machdep.h | 3 + + arch/powerpc/include/asm/mmu-book3e.h | 15 + + arch/powerpc/include/asm/paca.h | 10 +- + arch/powerpc/include/asm/page_64.h | 4 +- + arch/powerpc/include/asm/ppc-pci.h | 4 +- + arch/powerpc/include/asm/ppc_asm.h | 50 ++- + arch/powerpc/include/asm/processor.h | 4 +- + arch/powerpc/include/asm/pte-common.h | 7 + + arch/powerpc/include/asm/rtas.h | 1 + + arch/powerpc/include/asm/systbl.h | 19 + + arch/powerpc/include/asm/system.h | 4 +- + arch/powerpc/include/asm/time.h | 5 - + arch/powerpc/include/asm/unistd.h | 21 +- + arch/powerpc/kernel/Makefile | 4 +- + arch/powerpc/kernel/align.c | 4 +- + arch/powerpc/kernel/asm-offsets.c | 12 +- + arch/powerpc/kernel/cpu_setup_44x.S | 1 + + arch/powerpc/kernel/cpu_setup_fsl_booke.S | 15 + + arch/powerpc/kernel/cputable.c | 43 ++- + arch/powerpc/kernel/crash.c | 13 +- + arch/powerpc/kernel/dma-iommu.c | 21 +- + arch/powerpc/kernel/dma.c | 20 +- + arch/powerpc/kernel/entry_64.S | 40 ++ + arch/powerpc/kernel/fpu.S | 10 - + arch/powerpc/kernel/head_fsl_booke.S | 10 +- + arch/powerpc/kernel/irq.c | 6 +- + arch/powerpc/kernel/lparcfg.c | 14 +- + arch/powerpc/kernel/machine_kexec.c | 24 ++ + arch/powerpc/kernel/machine_kexec_32.c | 4 + + arch/powerpc/kernel/paca.c | 70 ++++- + arch/powerpc/kernel/pci-common.c | 4 +- + arch/powerpc/kernel/ppc970-pmu.c | 2 + + arch/powerpc/kernel/process.c | 12 - + arch/powerpc/kernel/ptrace.c | 2 +- + arch/powerpc/kernel/rtas.c | 4 +- + arch/powerpc/kernel/setup_32.c | 2 +- + arch/powerpc/kernel/smp.c | 14 +- + arch/powerpc/kernel/time.c | 275 +++++++------- + arch/powerpc/kernel/traps.c | 5 + + arch/powerpc/kernel/vdso.c | 6 +- + arch/powerpc/kernel/vdso32/Makefile | 6 +- + arch/powerpc/kernel/vdso64/Makefile | 6 +- + arch/powerpc/kernel/vio.c | 10 +- + arch/powerpc/kvm/Makefile | 2 +- + arch/powerpc/kvm/book3s_paired_singles.c | 44 +-- + arch/powerpc/kvm/emulate.c | 4 +- + arch/powerpc/kvm/fpu.S | 8 - + arch/powerpc/lib/Makefile | 7 +- + arch/powerpc/lib/checksum_64.S | 482 +++++++++++++++++------- + arch/powerpc/lib/checksum_wrappers_64.c | 102 +++++ + arch/powerpc/lib/copy_32.S | 2 +- + arch/powerpc/lib/ldstfp.S | 36 +- + arch/powerpc/lib/locks.c | 4 +- + arch/powerpc/lib/sstep.c | 8 + + arch/powerpc/math-emu/Makefile | 2 +- + arch/powerpc/mm/Makefile | 6 +- + arch/powerpc/mm/fault.c | 6 + + arch/powerpc/mm/fsl_booke_mmu.c | 15 +- + arch/powerpc/mm/mmu_context_nohash.c | 6 +- + arch/powerpc/mm/mmu_decl.h | 5 +- + arch/powerpc/mm/tlb_nohash.c | 56 +++- + arch/powerpc/mm/tlb_nohash_low.S | 2 +- + arch/powerpc/oprofile/Makefile | 4 +- + arch/powerpc/oprofile/backtrace.c | 2 +- + arch/powerpc/oprofile/op_model_fsl_emb.c | 15 +- + arch/powerpc/platforms/44x/Kconfig | 16 + + arch/powerpc/platforms/44x/ppc44x_simple.c | 1 + + arch/powerpc/platforms/83xx/Kconfig | 4 +- + arch/powerpc/platforms/83xx/mpc830x_rdb.c | 3 +- + arch/powerpc/platforms/85xx/Kconfig | 28 ++- + arch/powerpc/platforms/85xx/Makefile | 2 + + arch/powerpc/platforms/85xx/p1022_ds.c | 2 + + arch/powerpc/platforms/85xx/p3041_ds.c | 64 ++++ + arch/powerpc/platforms/85xx/p5020_ds.c | 69 ++++ + arch/powerpc/platforms/85xx/smp.c | 83 ++++- + arch/powerpc/platforms/Kconfig.cputype | 8 +- + arch/powerpc/platforms/cell/ras.c | 4 +- + arch/powerpc/platforms/cell/spider-pic.c | 4 +- + arch/powerpc/platforms/cell/spufs/file.c | 18 + + arch/powerpc/platforms/chrp/nvram.c | 4 +- + arch/powerpc/platforms/iseries/Makefile | 2 +- + arch/powerpc/platforms/iseries/dt.c | 4 +- + arch/powerpc/platforms/iseries/smp.c | 2 +- + arch/powerpc/platforms/maple/setup.c | 1 + + arch/powerpc/platforms/powermac/pfunc_core.c | 9 +- + arch/powerpc/platforms/pseries/Makefile | 13 +- + arch/powerpc/platforms/pseries/dlpar.c | 7 +- + arch/powerpc/platforms/pseries/dtl.c | 224 +++++++++--- + arch/powerpc/platforms/pseries/lpar.c | 25 ++- + arch/powerpc/platforms/pseries/mobility.c | 362 ++++++++++++++++++ + arch/powerpc/platforms/pseries/pseries.h | 9 + + arch/powerpc/platforms/pseries/setup.c | 52 +++ + arch/powerpc/platforms/pseries/xics.c | 2 +- + arch/powerpc/sysdev/Makefile | 5 +- + arch/powerpc/sysdev/dart_iommu.c | 74 ++++- + arch/powerpc/sysdev/fsl_85xx_cache_ctlr.h | 101 +++++ + arch/powerpc/sysdev/fsl_85xx_cache_sram.c | 159 ++++++++ + arch/powerpc/sysdev/fsl_85xx_l2ctlr.c | 231 +++++++++++ + arch/powerpc/sysdev/fsl_msi.c | 9 +- + arch/powerpc/sysdev/fsl_pci.c | 60 +++- + arch/powerpc/sysdev/fsl_pci.h | 1 + + arch/powerpc/sysdev/fsl_rio.c | 65 ++-- + arch/powerpc/sysdev/fsl_soc.c | 20 +- + arch/powerpc/sysdev/mpc8xxx_gpio.c | 3 + + arch/powerpc/sysdev/pmi.c | 2 +- + arch/powerpc/xmon/Makefile | 4 +- + drivers/i2c/busses/i2c-pasemi.c | 2 +- + drivers/macintosh/via-pmu-led.c | 4 +- + drivers/watchdog/Kconfig | 22 +- + drivers/watchdog/booke_wdt.c | 47 ++- + include/linux/pci_ids.h | 8 + + kernel/sys_ni.c | 1 + + 130 files changed, 3676 insertions(+), 683 deletions(-) + create mode 100644 arch/powerpc/boot/dts/bluestone.dts + create mode 100644 arch/powerpc/boot/dts/mpc8308_p1m.dts + create mode 100644 arch/powerpc/configs/44x/bluestone_defconfig + create mode 100644 arch/powerpc/configs/e55xx_smp_defconfig + create mode 100644 arch/powerpc/include/asm/fsl_85xx_cache_sram.h + create mode 100644 arch/powerpc/lib/checksum_wrappers_64.c + create mode 100644 arch/powerpc/platforms/85xx/p3041_ds.c + create mode 100644 arch/powerpc/platforms/85xx/p5020_ds.c + create mode 100644 arch/powerpc/platforms/pseries/mobility.c + create mode 100644 arch/powerpc/sysdev/fsl_85xx_cache_ctlr.h + create mode 100644 arch/powerpc/sysdev/fsl_85xx_cache_sram.c + create mode 100644 arch/powerpc/sysdev/fsl_85xx_l2ctlr.c + + +_______________________________________________ +Linuxppc-dev mailing list +Linuxppc-dev@lists.ozlabs.org +https://lists.ozlabs.org/listinfo/linuxppc-dev diff --git a/patchwork/tests/mail/0005-git-pull-request-ssh.mbox b/patchwork/tests/mail/0005-git-pull-request-ssh.mbox new file mode 100644 index 0000000..7f4c93e --- /dev/null +++ b/patchwork/tests/mail/0005-git-pull-request-ssh.mbox @@ -0,0 +1,348 @@ +From benh@kernel.crashing.org Fri Oct 22 11:51:02 2010 +Return-Path: +X-Spam-Checker-Version: SpamAssassin 3.3.1 (2010-03-16) on bilbo.ozlabs.org +X-Spam-Level: +X-Spam-Status: No, score=0.0 required=3.0 tests=none autolearn=disabled + version=3.3.1 +X-Original-To: jk@ozlabs.org +Delivered-To: jk@ozlabs.org +Received: from bilbo.ozlabs.org (localhost [127.0.0.1]) + by ozlabs.org (Postfix) with ESMTP id ED4B3100937 + for ; Fri, 22 Oct 2010 14:51:54 +1100 (EST) +Received: by ozlabs.org (Postfix) + id BF799B70CB; Fri, 22 Oct 2010 14:51:50 +1100 (EST) +Delivered-To: linuxppc-dev@ozlabs.org +Received: from gate.crashing.org (gate.crashing.org [63.228.1.57]) + (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits)) + (Client did not present a certificate) + by ozlabs.org (Postfix) with ESMTPS id 94629B7043 + for ; Fri, 22 Oct 2010 14:51:49 +1100 (EST) +Received: from [IPv6:::1] (localhost.localdomain [127.0.0.1]) + by gate.crashing.org (8.14.1/8.13.8) with ESMTP id o9M3p3SP018234; + Thu, 21 Oct 2010 22:51:04 -0500 +Subject: [git pull] Please pull powerpc.git next branch +From: Benjamin Herrenschmidt +To: Linus Torvalds +Date: Fri, 22 Oct 2010 14:51:02 +1100 +Message-ID: <1287719462.2198.37.camel@pasglop> +Mime-Version: 1.0 +X-Mailer: Evolution 2.30.3 +Cc: linuxppc-dev list , + Andrew Morton , + Linux Kernel list +X-BeenThere: linuxppc-dev@lists.ozlabs.org +X-Mailman-Version: 2.1.13 +Precedence: list +List-Id: Linux on PowerPC Developers Mail List +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-Subscribe: , + +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: 7bit +Sender: linuxppc-dev-bounces+jk=ozlabs.org@lists.ozlabs.org +Errors-To: linuxppc-dev-bounces+jk=ozlabs.org@lists.ozlabs.org +X-UID: 11446 +X-Length: 16781 +Status: R +X-Status: N +X-KMail-EncryptionState: +X-KMail-SignatureState: +X-KMail-MDN-Sent: + +Hi Linus ! + +Here's powerpc's batch for this merge window. Mostly bits and pieces, +such as Anton doing some performance tuning left and right, and the +usual churn. One hilight is the support for the new Freescale e5500 core +(64-bit BookE). Another one is that we now wire up the whole lot of +socket calls as direct syscalls in addition to the old style indirect +method. + +Cheers, +Ben. + +The following changes since commit e10117d36ef758da0690c95ecffc09d5dd7da479: + Linus Torvalds (1): + Merge branch 'upstream-linus' of git://git.kernel.org/.../jgarzik/libata-dev + +are available in the git repository at: + + ssh://git.kernel.org/pub/scm/linux/kernel/git/benh/powerpc.git next + +Andreas Schwab (1): + powerpc: Remove fpscr use from [kvm_]cvt_{fd,df} + +Anton Blanchard (5): + powerpc: Optimise 64bit csum_partial + powerpc: Optimise 64bit csum_partial_copy_generic and add csum_and_copy_from_user + powerpc: Add 64bit csum_and_copy_to_user + powerpc: Feature nop out reservation clear when stcx checks address + powerpc: Check end of stack canary at oops time + +Arnd Bergmann (1): + powerpc/spufs: Use llseek in all file operations + +Benjamin Herrenschmidt (4): + powerpc/dma: Add optional platform override of dma_set_mask() + powerpc/dart_iommu: Support for 64-bit iommu bypass window on PCIe + Merge remote branch 'kumar/merge' into next + Merge remote branch 'jwb/next' into next + +Denis Kirjanov (1): + powerpc: Use is_32bit_task() helper to test 32-bit binary + +Harninder Rai (1): + powerpc/85xx: add cache-sram support + +Ian Munsie (1): + powerpc: Wire up direct socket system calls + +Ilya Yanok (1): + powerpc/mpc83xx: Support for MPC8308 P1M board + +Joe Perches (2): + powerpc: Use static const char arrays + powerpc: Remove pr_ uses of KERN_ + +Josh Boyer (1): + powerpc/44x: Update ppc44x_defconfig + +Julia Lawall (7): + powerpc/via-pmu-led.c: Add of_node_put to avoid memory leak + powerpc/maple: Add of_node_put to avoid memory leak + powerpc/powermac/pfunc_core.c: Add of_node_put to avoid memory leak + powerpc/cell: Add of_node_put to avoid memory leak + powerpc/chrp/nvram.c: Add of_node_put to avoid memory leak + powerpc/irq.c: Add of_node_put to avoid memory leak + i2c/i2c-pasemi.c: Fix unsigned return type + +Kumar Gala (11): + powerpc/ppc64e: Fix link problem when building ppc64e_defconfig + powerpc/fsl-pci: Fix MSI support on 83xx platforms + powerpc/mpc8xxx_gpio: Add support for 'qoriq-gpio' controllers + powerpc/fsl-booke: Add PCI device ids for P2040/P3041/P5010/P5020 QoirQ chips + powerpc/fsl-booke: Add p3041 DS board support + powerpc: Fix compile error with paca code on ppc64e + powerpc/fsl-booke: Add support for FSL 64-bit e5500 core + powerpc/fsl-booke: Add support for FSL Arch v1.0 MMU in setup_page_sizes + powerpc/fsl-booke64: Use TLB CAMs to cover linear mapping on FSL 64-bit chips + powerpc/fsl-booke: Add p5020 DS board support + powerpc/fsl-booke: Add e55xx (64-bit) smp defconfig + +Matthew McClintock (7): + powerpc/mm: Assume first cpu is boot_cpuid not 0 + powerpc/kexec: make masking/disabling interrupts generic + powerpc/85xx: Remove call to mpic_teardown_this_cpu in kexec + powerpc/85xx: Minor fixups for kexec on 85xx + powerpc/85xx: flush dcache before resetting cores + powerpc/fsl_soc: Search all global-utilities nodes for rstccr + powerpc/fsl_booke: Add support to boot from core other than 0 + +Michael Neuling (1): + powerpc: Move arch_sd_sibling_asym_packing() to smp.c + +Nathan Fontenot (3): + powerpc/pseries: Export device tree updating routines + powerpc/pseries: Export rtas_ibm_suspend_me() + powerpc/pseries: Partition migration in the kernel + +Nishanth Aravamudan (8): + powerpc/pci: Fix return type of BUID_{HI,LO} macros + powerpc/dma: Fix dma_iommu_dma_supported compare + powerpc/dma: Fix check for direct DMA support + powerpc/vio: Use put_device() on device_register failure + powerpc/viobus: Free TCE table on device release + powerpc/pseries: Use kmemdup + powerpc/pci: Cleanup device dma setup code + powerpc/pseries/xics: Use cpu_possible_mask rather than cpu_all_mask + +Paul Gortmaker (1): + powerpc: Fix invalid page flags in create TLB CAM path for PTE_64BIT + +Paul Mackerras (5): + powerpc: Abstract indexing of lppaca structs + powerpc: Dynamically allocate most lppaca structs + powerpc: Account time using timebase rather than PURR + powerpc/pseries: Re-enable dispatch trace log userspace interface + powerpc/perf: Fix sampling enable for PPC970 + +Scott Wood (1): + oprofile/fsl emb: Don't set MSR[PMM] until after clearing the interrupt. + +Sean MacLennan (2): + powerpc: Fix incorrect .stabs entry for copy_32.S + powerpc: mtmsrd not defined + +Shaohui Xie (1): + fsl_rio: Add comments for sRIO registers. + +Stephen Rothwell (1): + powerpc: define a compat_sys_recv cond_syscall + +Timur Tabi (5): + powerpc: export ppc_proc_freq and ppc_tb_freq as GPL symbols + powerpc/watchdog: Allow the Book-E driver to be compiled as a module + powerpc/p1022: Add probing for individual DMA channels + powerpc/85xx: add ngPIXIS FPGA device tree node to the P1022DS board + powerpc/watchdog: Make default timeout for Book-E watchdog a Kconfig option + +Tirumala Marri (1): + powerpc/44x: Add support for the AMCC APM821xx SoC + +matt mooney (1): + powerpc/Makefiles: Change to new flag variables + + arch/powerpc/boot/addnote.c | 4 +- + arch/powerpc/boot/dts/bluestone.dts | 254 +++++++++++++ + arch/powerpc/boot/dts/mpc8308_p1m.dts | 332 ++++++++++++++++ + arch/powerpc/boot/dts/p1022ds.dts | 11 + + arch/powerpc/configs/44x/bluestone_defconfig | 68 ++++ + arch/powerpc/configs/e55xx_smp_defconfig | 84 ++++ + arch/powerpc/configs/ppc44x_defconfig | 9 +- + arch/powerpc/configs/ppc64e_defconfig | 4 +- + arch/powerpc/include/asm/checksum.h | 10 + + arch/powerpc/include/asm/compat.h | 4 +- + arch/powerpc/include/asm/cputable.h | 14 +- + arch/powerpc/include/asm/dma-mapping.h | 14 +- + arch/powerpc/include/asm/elf.h | 2 +- + arch/powerpc/include/asm/exception-64s.h | 3 +- + arch/powerpc/include/asm/fsl_85xx_cache_sram.h | 48 +++ + arch/powerpc/include/asm/kexec.h | 1 + + arch/powerpc/include/asm/kvm_fpu.h | 4 +- + arch/powerpc/include/asm/lppaca.h | 29 ++ + arch/powerpc/include/asm/machdep.h | 3 + + arch/powerpc/include/asm/mmu-book3e.h | 15 + + arch/powerpc/include/asm/paca.h | 10 +- + arch/powerpc/include/asm/page_64.h | 4 +- + arch/powerpc/include/asm/ppc-pci.h | 4 +- + arch/powerpc/include/asm/ppc_asm.h | 50 ++- + arch/powerpc/include/asm/processor.h | 4 +- + arch/powerpc/include/asm/pte-common.h | 7 + + arch/powerpc/include/asm/rtas.h | 1 + + arch/powerpc/include/asm/systbl.h | 19 + + arch/powerpc/include/asm/system.h | 4 +- + arch/powerpc/include/asm/time.h | 5 - + arch/powerpc/include/asm/unistd.h | 21 +- + arch/powerpc/kernel/Makefile | 4 +- + arch/powerpc/kernel/align.c | 4 +- + arch/powerpc/kernel/asm-offsets.c | 12 +- + arch/powerpc/kernel/cpu_setup_44x.S | 1 + + arch/powerpc/kernel/cpu_setup_fsl_booke.S | 15 + + arch/powerpc/kernel/cputable.c | 43 ++- + arch/powerpc/kernel/crash.c | 13 +- + arch/powerpc/kernel/dma-iommu.c | 21 +- + arch/powerpc/kernel/dma.c | 20 +- + arch/powerpc/kernel/entry_64.S | 40 ++ + arch/powerpc/kernel/fpu.S | 10 - + arch/powerpc/kernel/head_fsl_booke.S | 10 +- + arch/powerpc/kernel/irq.c | 6 +- + arch/powerpc/kernel/lparcfg.c | 14 +- + arch/powerpc/kernel/machine_kexec.c | 24 ++ + arch/powerpc/kernel/machine_kexec_32.c | 4 + + arch/powerpc/kernel/paca.c | 70 ++++- + arch/powerpc/kernel/pci-common.c | 4 +- + arch/powerpc/kernel/ppc970-pmu.c | 2 + + arch/powerpc/kernel/process.c | 12 - + arch/powerpc/kernel/ptrace.c | 2 +- + arch/powerpc/kernel/rtas.c | 4 +- + arch/powerpc/kernel/setup_32.c | 2 +- + arch/powerpc/kernel/smp.c | 14 +- + arch/powerpc/kernel/time.c | 275 +++++++------- + arch/powerpc/kernel/traps.c | 5 + + arch/powerpc/kernel/vdso.c | 6 +- + arch/powerpc/kernel/vdso32/Makefile | 6 +- + arch/powerpc/kernel/vdso64/Makefile | 6 +- + arch/powerpc/kernel/vio.c | 10 +- + arch/powerpc/kvm/Makefile | 2 +- + arch/powerpc/kvm/book3s_paired_singles.c | 44 +-- + arch/powerpc/kvm/emulate.c | 4 +- + arch/powerpc/kvm/fpu.S | 8 - + arch/powerpc/lib/Makefile | 7 +- + arch/powerpc/lib/checksum_64.S | 482 +++++++++++++++++------- + arch/powerpc/lib/checksum_wrappers_64.c | 102 +++++ + arch/powerpc/lib/copy_32.S | 2 +- + arch/powerpc/lib/ldstfp.S | 36 +- + arch/powerpc/lib/locks.c | 4 +- + arch/powerpc/lib/sstep.c | 8 + + arch/powerpc/math-emu/Makefile | 2 +- + arch/powerpc/mm/Makefile | 6 +- + arch/powerpc/mm/fault.c | 6 + + arch/powerpc/mm/fsl_booke_mmu.c | 15 +- + arch/powerpc/mm/mmu_context_nohash.c | 6 +- + arch/powerpc/mm/mmu_decl.h | 5 +- + arch/powerpc/mm/tlb_nohash.c | 56 +++- + arch/powerpc/mm/tlb_nohash_low.S | 2 +- + arch/powerpc/oprofile/Makefile | 4 +- + arch/powerpc/oprofile/backtrace.c | 2 +- + arch/powerpc/oprofile/op_model_fsl_emb.c | 15 +- + arch/powerpc/platforms/44x/Kconfig | 16 + + arch/powerpc/platforms/44x/ppc44x_simple.c | 1 + + arch/powerpc/platforms/83xx/Kconfig | 4 +- + arch/powerpc/platforms/83xx/mpc830x_rdb.c | 3 +- + arch/powerpc/platforms/85xx/Kconfig | 28 ++- + arch/powerpc/platforms/85xx/Makefile | 2 + + arch/powerpc/platforms/85xx/p1022_ds.c | 2 + + arch/powerpc/platforms/85xx/p3041_ds.c | 64 ++++ + arch/powerpc/platforms/85xx/p5020_ds.c | 69 ++++ + arch/powerpc/platforms/85xx/smp.c | 83 ++++- + arch/powerpc/platforms/Kconfig.cputype | 8 +- + arch/powerpc/platforms/cell/ras.c | 4 +- + arch/powerpc/platforms/cell/spider-pic.c | 4 +- + arch/powerpc/platforms/cell/spufs/file.c | 18 + + arch/powerpc/platforms/chrp/nvram.c | 4 +- + arch/powerpc/platforms/iseries/Makefile | 2 +- + arch/powerpc/platforms/iseries/dt.c | 4 +- + arch/powerpc/platforms/iseries/smp.c | 2 +- + arch/powerpc/platforms/maple/setup.c | 1 + + arch/powerpc/platforms/powermac/pfunc_core.c | 9 +- + arch/powerpc/platforms/pseries/Makefile | 13 +- + arch/powerpc/platforms/pseries/dlpar.c | 7 +- + arch/powerpc/platforms/pseries/dtl.c | 224 +++++++++--- + arch/powerpc/platforms/pseries/lpar.c | 25 ++- + arch/powerpc/platforms/pseries/mobility.c | 362 ++++++++++++++++++ + arch/powerpc/platforms/pseries/pseries.h | 9 + + arch/powerpc/platforms/pseries/setup.c | 52 +++ + arch/powerpc/platforms/pseries/xics.c | 2 +- + arch/powerpc/sysdev/Makefile | 5 +- + arch/powerpc/sysdev/dart_iommu.c | 74 ++++- + arch/powerpc/sysdev/fsl_85xx_cache_ctlr.h | 101 +++++ + arch/powerpc/sysdev/fsl_85xx_cache_sram.c | 159 ++++++++ + arch/powerpc/sysdev/fsl_85xx_l2ctlr.c | 231 +++++++++++ + arch/powerpc/sysdev/fsl_msi.c | 9 +- + arch/powerpc/sysdev/fsl_pci.c | 60 +++- + arch/powerpc/sysdev/fsl_pci.h | 1 + + arch/powerpc/sysdev/fsl_rio.c | 65 ++-- + arch/powerpc/sysdev/fsl_soc.c | 20 +- + arch/powerpc/sysdev/mpc8xxx_gpio.c | 3 + + arch/powerpc/sysdev/pmi.c | 2 +- + arch/powerpc/xmon/Makefile | 4 +- + drivers/i2c/busses/i2c-pasemi.c | 2 +- + drivers/macintosh/via-pmu-led.c | 4 +- + drivers/watchdog/Kconfig | 22 +- + drivers/watchdog/booke_wdt.c | 47 ++- + include/linux/pci_ids.h | 8 + + kernel/sys_ni.c | 1 + + 130 files changed, 3676 insertions(+), 683 deletions(-) + create mode 100644 arch/powerpc/boot/dts/bluestone.dts + create mode 100644 arch/powerpc/boot/dts/mpc8308_p1m.dts + create mode 100644 arch/powerpc/configs/44x/bluestone_defconfig + create mode 100644 arch/powerpc/configs/e55xx_smp_defconfig + create mode 100644 arch/powerpc/include/asm/fsl_85xx_cache_sram.h + create mode 100644 arch/powerpc/lib/checksum_wrappers_64.c + create mode 100644 arch/powerpc/platforms/85xx/p3041_ds.c + create mode 100644 arch/powerpc/platforms/85xx/p5020_ds.c + create mode 100644 arch/powerpc/platforms/pseries/mobility.c + create mode 100644 arch/powerpc/sysdev/fsl_85xx_cache_ctlr.h + create mode 100644 arch/powerpc/sysdev/fsl_85xx_cache_sram.c + create mode 100644 arch/powerpc/sysdev/fsl_85xx_l2ctlr.c + + +_______________________________________________ +Linuxppc-dev mailing list +Linuxppc-dev@lists.ozlabs.org +https://lists.ozlabs.org/listinfo/linuxppc-dev diff --git a/patchwork/tests/mail/0006-git-pull-request-http.mbox b/patchwork/tests/mail/0006-git-pull-request-http.mbox new file mode 100644 index 0000000..e4f9007 --- /dev/null +++ b/patchwork/tests/mail/0006-git-pull-request-http.mbox @@ -0,0 +1,348 @@ +From benh@kernel.crashing.org Fri Oct 22 11:51:02 2010 +Return-Path: +X-Spam-Checker-Version: SpamAssassin 3.3.1 (2010-03-16) on bilbo.ozlabs.org +X-Spam-Level: +X-Spam-Status: No, score=0.0 required=3.0 tests=none autolearn=disabled + version=3.3.1 +X-Original-To: jk@ozlabs.org +Delivered-To: jk@ozlabs.org +Received: from bilbo.ozlabs.org (localhost [127.0.0.1]) + by ozlabs.org (Postfix) with ESMTP id ED4B3100937 + for ; Fri, 22 Oct 2010 14:51:54 +1100 (EST) +Received: by ozlabs.org (Postfix) + id BF799B70CB; Fri, 22 Oct 2010 14:51:50 +1100 (EST) +Delivered-To: linuxppc-dev@ozlabs.org +Received: from gate.crashing.org (gate.crashing.org [63.228.1.57]) + (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits)) + (Client did not present a certificate) + by ozlabs.org (Postfix) with ESMTPS id 94629B7043 + for ; Fri, 22 Oct 2010 14:51:49 +1100 (EST) +Received: from [IPv6:::1] (localhost.localdomain [127.0.0.1]) + by gate.crashing.org (8.14.1/8.13.8) with ESMTP id o9M3p3SP018234; + Thu, 21 Oct 2010 22:51:04 -0500 +Subject: [git pull] Please pull powerpc.git next branch +From: Benjamin Herrenschmidt +To: Linus Torvalds +Date: Fri, 22 Oct 2010 14:51:02 +1100 +Message-ID: <1287719462.2198.37.camel@pasglop> +Mime-Version: 1.0 +X-Mailer: Evolution 2.30.3 +Cc: linuxppc-dev list , + Andrew Morton , + Linux Kernel list +X-BeenThere: linuxppc-dev@lists.ozlabs.org +X-Mailman-Version: 2.1.13 +Precedence: list +List-Id: Linux on PowerPC Developers Mail List +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-Subscribe: , + +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: 7bit +Sender: linuxppc-dev-bounces+jk=ozlabs.org@lists.ozlabs.org +Errors-To: linuxppc-dev-bounces+jk=ozlabs.org@lists.ozlabs.org +X-UID: 11446 +X-Length: 16781 +Status: R +X-Status: N +X-KMail-EncryptionState: +X-KMail-SignatureState: +X-KMail-MDN-Sent: + +Hi Linus ! + +Here's powerpc's batch for this merge window. Mostly bits and pieces, +such as Anton doing some performance tuning left and right, and the +usual churn. One hilight is the support for the new Freescale e5500 core +(64-bit BookE). Another one is that we now wire up the whole lot of +socket calls as direct syscalls in addition to the old style indirect +method. + +Cheers, +Ben. + +The following changes since commit e10117d36ef758da0690c95ecffc09d5dd7da479: + Linus Torvalds (1): + Merge branch 'upstream-linus' of git://git.kernel.org/.../jgarzik/libata-dev + +are available in the git repository at: + + http://git.kernel.org/pub/scm/linux/kernel/git/benh/powerpc.git next + +Andreas Schwab (1): + powerpc: Remove fpscr use from [kvm_]cvt_{fd,df} + +Anton Blanchard (5): + powerpc: Optimise 64bit csum_partial + powerpc: Optimise 64bit csum_partial_copy_generic and add csum_and_copy_from_user + powerpc: Add 64bit csum_and_copy_to_user + powerpc: Feature nop out reservation clear when stcx checks address + powerpc: Check end of stack canary at oops time + +Arnd Bergmann (1): + powerpc/spufs: Use llseek in all file operations + +Benjamin Herrenschmidt (4): + powerpc/dma: Add optional platform override of dma_set_mask() + powerpc/dart_iommu: Support for 64-bit iommu bypass window on PCIe + Merge remote branch 'kumar/merge' into next + Merge remote branch 'jwb/next' into next + +Denis Kirjanov (1): + powerpc: Use is_32bit_task() helper to test 32-bit binary + +Harninder Rai (1): + powerpc/85xx: add cache-sram support + +Ian Munsie (1): + powerpc: Wire up direct socket system calls + +Ilya Yanok (1): + powerpc/mpc83xx: Support for MPC8308 P1M board + +Joe Perches (2): + powerpc: Use static const char arrays + powerpc: Remove pr_ uses of KERN_ + +Josh Boyer (1): + powerpc/44x: Update ppc44x_defconfig + +Julia Lawall (7): + powerpc/via-pmu-led.c: Add of_node_put to avoid memory leak + powerpc/maple: Add of_node_put to avoid memory leak + powerpc/powermac/pfunc_core.c: Add of_node_put to avoid memory leak + powerpc/cell: Add of_node_put to avoid memory leak + powerpc/chrp/nvram.c: Add of_node_put to avoid memory leak + powerpc/irq.c: Add of_node_put to avoid memory leak + i2c/i2c-pasemi.c: Fix unsigned return type + +Kumar Gala (11): + powerpc/ppc64e: Fix link problem when building ppc64e_defconfig + powerpc/fsl-pci: Fix MSI support on 83xx platforms + powerpc/mpc8xxx_gpio: Add support for 'qoriq-gpio' controllers + powerpc/fsl-booke: Add PCI device ids for P2040/P3041/P5010/P5020 QoirQ chips + powerpc/fsl-booke: Add p3041 DS board support + powerpc: Fix compile error with paca code on ppc64e + powerpc/fsl-booke: Add support for FSL 64-bit e5500 core + powerpc/fsl-booke: Add support for FSL Arch v1.0 MMU in setup_page_sizes + powerpc/fsl-booke64: Use TLB CAMs to cover linear mapping on FSL 64-bit chips + powerpc/fsl-booke: Add p5020 DS board support + powerpc/fsl-booke: Add e55xx (64-bit) smp defconfig + +Matthew McClintock (7): + powerpc/mm: Assume first cpu is boot_cpuid not 0 + powerpc/kexec: make masking/disabling interrupts generic + powerpc/85xx: Remove call to mpic_teardown_this_cpu in kexec + powerpc/85xx: Minor fixups for kexec on 85xx + powerpc/85xx: flush dcache before resetting cores + powerpc/fsl_soc: Search all global-utilities nodes for rstccr + powerpc/fsl_booke: Add support to boot from core other than 0 + +Michael Neuling (1): + powerpc: Move arch_sd_sibling_asym_packing() to smp.c + +Nathan Fontenot (3): + powerpc/pseries: Export device tree updating routines + powerpc/pseries: Export rtas_ibm_suspend_me() + powerpc/pseries: Partition migration in the kernel + +Nishanth Aravamudan (8): + powerpc/pci: Fix return type of BUID_{HI,LO} macros + powerpc/dma: Fix dma_iommu_dma_supported compare + powerpc/dma: Fix check for direct DMA support + powerpc/vio: Use put_device() on device_register failure + powerpc/viobus: Free TCE table on device release + powerpc/pseries: Use kmemdup + powerpc/pci: Cleanup device dma setup code + powerpc/pseries/xics: Use cpu_possible_mask rather than cpu_all_mask + +Paul Gortmaker (1): + powerpc: Fix invalid page flags in create TLB CAM path for PTE_64BIT + +Paul Mackerras (5): + powerpc: Abstract indexing of lppaca structs + powerpc: Dynamically allocate most lppaca structs + powerpc: Account time using timebase rather than PURR + powerpc/pseries: Re-enable dispatch trace log userspace interface + powerpc/perf: Fix sampling enable for PPC970 + +Scott Wood (1): + oprofile/fsl emb: Don't set MSR[PMM] until after clearing the interrupt. + +Sean MacLennan (2): + powerpc: Fix incorrect .stabs entry for copy_32.S + powerpc: mtmsrd not defined + +Shaohui Xie (1): + fsl_rio: Add comments for sRIO registers. + +Stephen Rothwell (1): + powerpc: define a compat_sys_recv cond_syscall + +Timur Tabi (5): + powerpc: export ppc_proc_freq and ppc_tb_freq as GPL symbols + powerpc/watchdog: Allow the Book-E driver to be compiled as a module + powerpc/p1022: Add probing for individual DMA channels + powerpc/85xx: add ngPIXIS FPGA device tree node to the P1022DS board + powerpc/watchdog: Make default timeout for Book-E watchdog a Kconfig option + +Tirumala Marri (1): + powerpc/44x: Add support for the AMCC APM821xx SoC + +matt mooney (1): + powerpc/Makefiles: Change to new flag variables + + arch/powerpc/boot/addnote.c | 4 +- + arch/powerpc/boot/dts/bluestone.dts | 254 +++++++++++++ + arch/powerpc/boot/dts/mpc8308_p1m.dts | 332 ++++++++++++++++ + arch/powerpc/boot/dts/p1022ds.dts | 11 + + arch/powerpc/configs/44x/bluestone_defconfig | 68 ++++ + arch/powerpc/configs/e55xx_smp_defconfig | 84 ++++ + arch/powerpc/configs/ppc44x_defconfig | 9 +- + arch/powerpc/configs/ppc64e_defconfig | 4 +- + arch/powerpc/include/asm/checksum.h | 10 + + arch/powerpc/include/asm/compat.h | 4 +- + arch/powerpc/include/asm/cputable.h | 14 +- + arch/powerpc/include/asm/dma-mapping.h | 14 +- + arch/powerpc/include/asm/elf.h | 2 +- + arch/powerpc/include/asm/exception-64s.h | 3 +- + arch/powerpc/include/asm/fsl_85xx_cache_sram.h | 48 +++ + arch/powerpc/include/asm/kexec.h | 1 + + arch/powerpc/include/asm/kvm_fpu.h | 4 +- + arch/powerpc/include/asm/lppaca.h | 29 ++ + arch/powerpc/include/asm/machdep.h | 3 + + arch/powerpc/include/asm/mmu-book3e.h | 15 + + arch/powerpc/include/asm/paca.h | 10 +- + arch/powerpc/include/asm/page_64.h | 4 +- + arch/powerpc/include/asm/ppc-pci.h | 4 +- + arch/powerpc/include/asm/ppc_asm.h | 50 ++- + arch/powerpc/include/asm/processor.h | 4 +- + arch/powerpc/include/asm/pte-common.h | 7 + + arch/powerpc/include/asm/rtas.h | 1 + + arch/powerpc/include/asm/systbl.h | 19 + + arch/powerpc/include/asm/system.h | 4 +- + arch/powerpc/include/asm/time.h | 5 - + arch/powerpc/include/asm/unistd.h | 21 +- + arch/powerpc/kernel/Makefile | 4 +- + arch/powerpc/kernel/align.c | 4 +- + arch/powerpc/kernel/asm-offsets.c | 12 +- + arch/powerpc/kernel/cpu_setup_44x.S | 1 + + arch/powerpc/kernel/cpu_setup_fsl_booke.S | 15 + + arch/powerpc/kernel/cputable.c | 43 ++- + arch/powerpc/kernel/crash.c | 13 +- + arch/powerpc/kernel/dma-iommu.c | 21 +- + arch/powerpc/kernel/dma.c | 20 +- + arch/powerpc/kernel/entry_64.S | 40 ++ + arch/powerpc/kernel/fpu.S | 10 - + arch/powerpc/kernel/head_fsl_booke.S | 10 +- + arch/powerpc/kernel/irq.c | 6 +- + arch/powerpc/kernel/lparcfg.c | 14 +- + arch/powerpc/kernel/machine_kexec.c | 24 ++ + arch/powerpc/kernel/machine_kexec_32.c | 4 + + arch/powerpc/kernel/paca.c | 70 ++++- + arch/powerpc/kernel/pci-common.c | 4 +- + arch/powerpc/kernel/ppc970-pmu.c | 2 + + arch/powerpc/kernel/process.c | 12 - + arch/powerpc/kernel/ptrace.c | 2 +- + arch/powerpc/kernel/rtas.c | 4 +- + arch/powerpc/kernel/setup_32.c | 2 +- + arch/powerpc/kernel/smp.c | 14 +- + arch/powerpc/kernel/time.c | 275 +++++++------- + arch/powerpc/kernel/traps.c | 5 + + arch/powerpc/kernel/vdso.c | 6 +- + arch/powerpc/kernel/vdso32/Makefile | 6 +- + arch/powerpc/kernel/vdso64/Makefile | 6 +- + arch/powerpc/kernel/vio.c | 10 +- + arch/powerpc/kvm/Makefile | 2 +- + arch/powerpc/kvm/book3s_paired_singles.c | 44 +-- + arch/powerpc/kvm/emulate.c | 4 +- + arch/powerpc/kvm/fpu.S | 8 - + arch/powerpc/lib/Makefile | 7 +- + arch/powerpc/lib/checksum_64.S | 482 +++++++++++++++++------- + arch/powerpc/lib/checksum_wrappers_64.c | 102 +++++ + arch/powerpc/lib/copy_32.S | 2 +- + arch/powerpc/lib/ldstfp.S | 36 +- + arch/powerpc/lib/locks.c | 4 +- + arch/powerpc/lib/sstep.c | 8 + + arch/powerpc/math-emu/Makefile | 2 +- + arch/powerpc/mm/Makefile | 6 +- + arch/powerpc/mm/fault.c | 6 + + arch/powerpc/mm/fsl_booke_mmu.c | 15 +- + arch/powerpc/mm/mmu_context_nohash.c | 6 +- + arch/powerpc/mm/mmu_decl.h | 5 +- + arch/powerpc/mm/tlb_nohash.c | 56 +++- + arch/powerpc/mm/tlb_nohash_low.S | 2 +- + arch/powerpc/oprofile/Makefile | 4 +- + arch/powerpc/oprofile/backtrace.c | 2 +- + arch/powerpc/oprofile/op_model_fsl_emb.c | 15 +- + arch/powerpc/platforms/44x/Kconfig | 16 + + arch/powerpc/platforms/44x/ppc44x_simple.c | 1 + + arch/powerpc/platforms/83xx/Kconfig | 4 +- + arch/powerpc/platforms/83xx/mpc830x_rdb.c | 3 +- + arch/powerpc/platforms/85xx/Kconfig | 28 ++- + arch/powerpc/platforms/85xx/Makefile | 2 + + arch/powerpc/platforms/85xx/p1022_ds.c | 2 + + arch/powerpc/platforms/85xx/p3041_ds.c | 64 ++++ + arch/powerpc/platforms/85xx/p5020_ds.c | 69 ++++ + arch/powerpc/platforms/85xx/smp.c | 83 ++++- + arch/powerpc/platforms/Kconfig.cputype | 8 +- + arch/powerpc/platforms/cell/ras.c | 4 +- + arch/powerpc/platforms/cell/spider-pic.c | 4 +- + arch/powerpc/platforms/cell/spufs/file.c | 18 + + arch/powerpc/platforms/chrp/nvram.c | 4 +- + arch/powerpc/platforms/iseries/Makefile | 2 +- + arch/powerpc/platforms/iseries/dt.c | 4 +- + arch/powerpc/platforms/iseries/smp.c | 2 +- + arch/powerpc/platforms/maple/setup.c | 1 + + arch/powerpc/platforms/powermac/pfunc_core.c | 9 +- + arch/powerpc/platforms/pseries/Makefile | 13 +- + arch/powerpc/platforms/pseries/dlpar.c | 7 +- + arch/powerpc/platforms/pseries/dtl.c | 224 +++++++++--- + arch/powerpc/platforms/pseries/lpar.c | 25 ++- + arch/powerpc/platforms/pseries/mobility.c | 362 ++++++++++++++++++ + arch/powerpc/platforms/pseries/pseries.h | 9 + + arch/powerpc/platforms/pseries/setup.c | 52 +++ + arch/powerpc/platforms/pseries/xics.c | 2 +- + arch/powerpc/sysdev/Makefile | 5 +- + arch/powerpc/sysdev/dart_iommu.c | 74 ++++- + arch/powerpc/sysdev/fsl_85xx_cache_ctlr.h | 101 +++++ + arch/powerpc/sysdev/fsl_85xx_cache_sram.c | 159 ++++++++ + arch/powerpc/sysdev/fsl_85xx_l2ctlr.c | 231 +++++++++++ + arch/powerpc/sysdev/fsl_msi.c | 9 +- + arch/powerpc/sysdev/fsl_pci.c | 60 +++- + arch/powerpc/sysdev/fsl_pci.h | 1 + + arch/powerpc/sysdev/fsl_rio.c | 65 ++-- + arch/powerpc/sysdev/fsl_soc.c | 20 +- + arch/powerpc/sysdev/mpc8xxx_gpio.c | 3 + + arch/powerpc/sysdev/pmi.c | 2 +- + arch/powerpc/xmon/Makefile | 4 +- + drivers/i2c/busses/i2c-pasemi.c | 2 +- + drivers/macintosh/via-pmu-led.c | 4 +- + drivers/watchdog/Kconfig | 22 +- + drivers/watchdog/booke_wdt.c | 47 ++- + include/linux/pci_ids.h | 8 + + kernel/sys_ni.c | 1 + + 130 files changed, 3676 insertions(+), 683 deletions(-) + create mode 100644 arch/powerpc/boot/dts/bluestone.dts + create mode 100644 arch/powerpc/boot/dts/mpc8308_p1m.dts + create mode 100644 arch/powerpc/configs/44x/bluestone_defconfig + create mode 100644 arch/powerpc/configs/e55xx_smp_defconfig + create mode 100644 arch/powerpc/include/asm/fsl_85xx_cache_sram.h + create mode 100644 arch/powerpc/lib/checksum_wrappers_64.c + create mode 100644 arch/powerpc/platforms/85xx/p3041_ds.c + create mode 100644 arch/powerpc/platforms/85xx/p5020_ds.c + create mode 100644 arch/powerpc/platforms/pseries/mobility.c + create mode 100644 arch/powerpc/sysdev/fsl_85xx_cache_ctlr.h + create mode 100644 arch/powerpc/sysdev/fsl_85xx_cache_sram.c + create mode 100644 arch/powerpc/sysdev/fsl_85xx_l2ctlr.c + + +_______________________________________________ +Linuxppc-dev mailing list +Linuxppc-dev@lists.ozlabs.org +https://lists.ozlabs.org/listinfo/linuxppc-dev diff --git a/patchwork/tests/mail/0007-cvs-format-diff.mbox b/patchwork/tests/mail/0007-cvs-format-diff.mbox new file mode 100644 index 0000000..99735fa --- /dev/null +++ b/patchwork/tests/mail/0007-cvs-format-diff.mbox @@ -0,0 +1,134 @@ +Received: with ECARTIS (v1.0.0; list linux-mips); Tue, 06 Dec 2011 01:49:42 +0100 (CET) +Received: from mail3.caviumnetworks.com ([12.108.191.235]:14337 "EHLO + mail3.caviumnetworks.com" rhost-flags-OK-OK-OK-OK) + by eddie.linux-mips.org with ESMTP id S1903632Ab1LFAth (ORCPT + ); Tue, 6 Dec 2011 01:49:37 +0100 +Received: from caexch01.caveonetworks.com (Not Verified[192.168.16.9]) by mail3.caviumnetworks.com with MailMarshal (v6,7,2,8378) + id ; Mon, 05 Dec 2011 16:51:04 -0800 +Received: from caexch01.caveonetworks.com ([192.168.16.9]) by caexch01.caveonetworks.com with Microsoft SMTPSVC(6.0.3790.4675); + Mon, 5 Dec 2011 16:49:36 -0800 +Received: from dd1.caveonetworks.com ([64.2.3.195]) by caexch01.caveonetworks.com over TLS secured channel with Microsoft SMTPSVC(6.0.3790.4675); + Mon, 5 Dec 2011 16:49:35 -0800 +Message-ID: <4EDD669F.30207@cavium.com> +Date: Mon, 05 Dec 2011 16:49:35 -0800 +From: David Daney +User-Agent: Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.15) Gecko/20101027 Fedora/3.0.10-1.fc12 Thunderbird/3.0.10 +MIME-Version: 1.0 +To: binutils +CC: linux-mips , + Manuel Lauss , + Debian MIPS +Subject: [Patch]: Fix ld pr11138 FAILures on mips*. +Content-Type: multipart/mixed; + boundary="------------080709040708040308010506" +X-OriginalArrivalTime: 06 Dec 2011 00:49:35.0825 (UTC) FILETIME=[ECF8DC10:01CCB3B0] +Return-Path: +X-Envelope-To: <"|/home/ecartis/ecartis -s linux-mips"> (uid 0) +X-Orcpt: rfc822;linux-mips@linux-mips.org +Original-Recipient: rfc822;linux-mips@linux-mips.org +X-archive-position: 32041 +X-ecartis-version: Ecartis v1.0.0 +Sender: linux-mips-bounce@linux-mips.org +Errors-to: linux-mips-bounce@linux-mips.org +X-original-sender: david.daney@cavium.com +Precedence: bulk +X-list: linux-mips + +This is a multi-part message in MIME format. +--------------080709040708040308010506 +Content-Type: text/plain; charset=ISO-8859-1; format=flowed +Content-Transfer-Encoding: 7bit + +The pr11138 testcase links an executable with a version script. On +mips64-linux the presence of a version script was causing the +MIPS_RLD_MAP dynamic tag to be populated with a NULL value. When such +an executable was run ld.so would try to dereference this and receive +SIGSEGV, thus killing the process. + +The root cause of this is that the mips linker synthesizes a special +symbol "__RLD_MAP", and then sets MIPS_RLD_MAP to point to it. When a +version script is present, this symbol gets versioned along with all the +rest, and when it is time to take its address, the symbol can no longer +be found as it has had version information appended to its name. + +Since "__RLD_MAP" is really part of the ABI, we want to exclude it from +symbol versioning. To this end, I introduced a new symbol flag +'no_sym_version' to tag this type of symbol. When the "__RLD_MAP" +symbol is created, we set this flag. + +In _bfd_elf_link_assign_sym_version, we then skip all symbols that have +'no_sym_version' set, and everything now works. + +This problem has also been reported in the wild when linking the firefox +executable. + +Tested on mips64-linux-gnu and x86_64-linux-gnu + +Ok to commit? + +2011-12-05 David Daney + + * elf-bfd.h (elf_link_hash_entry): Add no_sym_version field. + * elflink.c (_bfd_elf_link_assign_sym_version): Don't assign a + version if no_sym_version is set. + * elfxx-mips.c (_bfd_mips_elf_create_dynamic_sections): Set + no_sym_version for "__RLD_MAP". + +--------------080709040708040308010506 +Content-Type: text/plain; + name="dd-2.patch" +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment; + filename="dd-2.patch" + +Index: bfd/elf-bfd.h +=================================================================== +RCS file: /cvs/src/src/bfd/elf-bfd.h,v +retrieving revision 1.329 +diff -u -p -r1.329 elf-bfd.h +--- bfd/elf-bfd.h 17 Aug 2011 00:39:38 -0000 1.329 ++++ bfd/elf-bfd.h 5 Dec 2011 20:15:49 -0000 +@@ -198,6 +198,8 @@ struct elf_link_hash_entry + unsigned int pointer_equality_needed : 1; + /* Symbol is a unique global symbol. */ + unsigned int unique_global : 1; ++ /* Symbol should not be versioned. It is part of the ABI */ ++ unsigned int no_sym_version : 1; + + /* String table index in .dynstr if this is a dynamic symbol. */ + unsigned long dynstr_index; +Index: bfd/elflink.c +=================================================================== +RCS file: /cvs/src/src/bfd/elflink.c,v +retrieving revision 1.430 +diff -u -p -r1.430 elflink.c +--- bfd/elflink.c 15 Nov 2011 11:33:57 -0000 1.430 ++++ bfd/elflink.c 5 Dec 2011 20:15:50 -0000 +@@ -1946,6 +1946,9 @@ _bfd_elf_link_assign_sym_version (struct + if (!h->def_regular) + return TRUE; + ++ if (h->no_sym_version) ++ return TRUE; ++ + bed = get_elf_backend_data (info->output_bfd); + p = strchr (h->root.root.string, ELF_VER_CHR); + if (p != NULL && h->verinfo.vertree == NULL) +Index: bfd/elfxx-mips.c +=================================================================== +RCS file: /cvs/src/src/bfd/elfxx-mips.c,v +retrieving revision 1.296 +diff -u -p -r1.296 elfxx-mips.c +--- bfd/elfxx-mips.c 29 Nov 2011 20:28:54 -0000 1.296 ++++ bfd/elfxx-mips.c 5 Dec 2011 20:15:50 -0000 +@@ -7260,6 +7260,7 @@ _bfd_mips_elf_create_dynamic_sections (b + h = (struct elf_link_hash_entry *) bh; + h->non_elf = 0; + h->def_regular = 1; ++ h->no_sym_version = 1; + h->type = STT_OBJECT; + + if (! bfd_elf_link_record_dynamic_symbol (info, h)) + +--------------080709040708040308010506-- + diff --git a/patchwork/tests/mail/0008-git-rename.mbox b/patchwork/tests/mail/0008-git-rename.mbox new file mode 100644 index 0000000..8277049 --- /dev/null +++ b/patchwork/tests/mail/0008-git-rename.mbox @@ -0,0 +1,24 @@ +From: "Yann E. MORIN" +Subject: [Buildroot] [PATCH 01/11] package/rpi-userland: rename patches +Date: Tue, 8 Oct 2013 22:09:47 +0000 + +Rename patches to follow standard naming scheme. + +Signed-off-by: "Yann E. MORIN" +--- + ...d-pkgconfig-files.patch => rpi-userland-000-add-pkgconfig-files.patch} | 0 + ...erland-001-makefiles-cmake-vmcs.cmake-allow-to-override-VMCS_IN.patch} | 0 + 2 files changed, 0 insertions(+), 0 deletions(-) + rename package/rpi-userland/{rpi-userland-add-pkgconfig-files.patch => rpi-userland-000-add-pkgconfig-files.patch} (100%) + rename package/rpi-userland/{rpi-userland-makefiles-0001-cmake-vmcs.cmake-allow-to-override-VMCS_IN.patch => rpi-userland-001-makefiles-cmake-vmcs.cmake-allow-to-override-VMCS_IN.patch} (100%) + +diff --git a/package/rpi-userland/rpi-userland-add-pkgconfig-files.patch b/package/rpi-userland/rpi-userland-000-add-pkgconfig-files.patch +similarity index 100% +rename from package/rpi-userland/rpi-userland-add-pkgconfig-files.patch +rename to package/rpi-userland/rpi-userland-000-add-pkgconfig-files.patch +diff --git a/package/rpi-userland/rpi-userland-makefiles-0001-cmake-vmcs.cmake-allow-to-override-VMCS_IN.patch b/package/rpi-userland/rpi-userland-001-makefiles-cmake-vmcs.cmake-allow-to-override-VMCS_IN.patch +similarity index 100% +rename from package/rpi-userland/rpi-userland-makefiles-0001-cmake-vmcs.cmake-allow-to-override-VMCS_IN.patch +rename to package/rpi-userland/rpi-userland-001-makefiles-cmake-vmcs.cmake-allow-to-override-VMCS_IN.patch +-- +1.8.1.2 diff --git a/patchwork/tests/mail/0009-git-rename-with-diff.mbox b/patchwork/tests/mail/0009-git-rename-with-diff.mbox new file mode 100644 index 0000000..761cfc1 --- /dev/null +++ b/patchwork/tests/mail/0009-git-rename-with-diff.mbox @@ -0,0 +1,32 @@ +From: "Yann E. MORIN" +Subject: [Buildroot] [PATCH 01/11] package/rpi-userland: rename patches +Date: Tue, 8 Oct 2013 22:09:47 +0000 + +Rename patches to follow standard naming scheme. + +Signed-off-by: "Yann E. MORIN" +--- + ...d-pkgconfig-files.patch => rpi-userland-000-add-pkgconfig-files.patch} | 0 + ...erland-001-makefiles-cmake-vmcs.cmake-allow-to-override-VMCS_IN.patch} | 0 + 2 files changed, 0 insertions(+), 0 deletions(-) + rename package/rpi-userland/{rpi-userland-add-pkgconfig-files.patch => rpi-userland-000-add-pkgconfig-files.patch} (100%) + rename package/rpi-userland/{rpi-userland-makefiles-0001-cmake-vmcs.cmake-allow-to-override-VMCS_IN.patch => rpi-userland-001-makefiles-cmake-vmcs.cmake-allow-to-override-VMCS_IN.patch} (100%) + +diff --git a/package/rpi-userland/rpi-userland-add-pkgconfig-files.patch b/package/rpi-userland/rpi-userland-000-add-pkgconfig-files.patch +similarity index 100% +rename from package/rpi-userland/rpi-userland-add-pkgconfig-files.patch +rename to package/rpi-userland/rpi-userland-000-add-pkgconfig-files.patch +@@ -100,7 +100,7 @@ + a + a +-a ++b + c + c + c +diff --git a/package/rpi-userland/rpi-userland-makefiles-0001-cmake-vmcs.cmake-allow-to-override-VMCS_IN.patch b/package/rpi-userland/rpi-userland-001-makefiles-cmake-vmcs.cmake-allow-to-override-VMCS_IN.patch +similarity index 100% +rename from package/rpi-userland/rpi-userland-makefiles-0001-cmake-vmcs.cmake-allow-to-override-VMCS_IN.patch +rename to package/rpi-userland/rpi-userland-001-makefiles-cmake-vmcs.cmake-allow-to-override-VMCS_IN.patch +-- +1.8.1.2 diff --git a/patchwork/tests/mail/0010-invalid-charset.mbox b/patchwork/tests/mail/0010-invalid-charset.mbox new file mode 100644 index 0000000..10b369d --- /dev/null +++ b/patchwork/tests/mail/0010-invalid-charset.mbox @@ -0,0 +1,90 @@ +From libc-alpha-return-50517-siddhesh=redhat.com@sourceware.org Thu Jun 5 10:36:33 2014 +Received: (qmail 11948 invoked by alias); 4 Jun 2014 17:51:01 -0000 +Mailing-List: contact libc-alpha-help@sourceware.org; run by ezmlm +List-Id: +Sender: libc-alpha-owner@sourceware.org +Date: Wed, 4 Jun 2014 17:50:46 +0000 +From: "Joseph S. Myers" +To: +Subject: Fix pow overflow in non-default rounding modes (bug 16315) +Message-ID: +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="-1152306461-1522705971-1401904246=:3719" +Content-Length: 24171 + +---1152306461-1522705971-1401904246=:3719 +Content-Type: text/plain; charset="none" +Content-Transfer-Encoding: QUOTED-PRINTABLE + +This patch, relative to a tree with + applied, +fixes bug 16315, bad pow handling of overflow/underflow in non-default +rounding modes. Tests of pow are duly converted to ALL_RM_TEST to run +all tests in all rounding modes. + +There are two main issues here. First, various implementations +compute a negative result by negating a positive result, but this +yields inappropriate overflow / underflow values for directed +rounding, so either overflow / underflow results need recomputing in +the correct sign, or the relevant overflowing / underflowing operation +needs to be made to have a result of the correct sign. Second, the +dbl-64 implementation sets FE_TONEAREST internally; in the overflow / +underflow case, the result needs recomputing in the original rounding +mode. + +Tested x86_64 and x86 and ulps updated accordingly. + +(auto-libm-test-out diffs omitted below.) + +2014-06-04 Joseph Myers + +=09[BZ #16315] +=09* sysdeps/i386/fpu/e_pow.S (__ieee754_pow): Ensure possibly +=09overflowing or underflowing operations take place with sign of +=09result. +=09* sysdeps/i386/fpu/e_powf.S (__ieee754_powf): Likewise. +=09* sysdeps/i386/fpu/e_powl.S (__ieee754_powl): Likewise. +=09* sysdeps/ieee754/dbl-64/e_pow.c: Include . +=09(__ieee754_pow): Recompute overflowing and underflowing results in +=09original rounding mode. +=09* sysdeps/x86/fpu/powl_helper.c: Include . +=09(__powl_helper): Allow negative argument X and scale negated value +=09as needed. Avoid passing value outside [-1, 1] to f2xm1. +=09* sysdeps/x86_64/fpu/e_powl.S (__ieee754_powl): Ensure possibly +=09overflowing or underflowing operations take place with sign of +=09result. +=09* sysdeps/x86_64/fpu/multiarch/e_pow.c [HAVE_FMA4_SUPPORT]: +=09Include . +=09* math/auto-libm-test-in: Add more tests of pow. +=09* math/auto-libm-test-out: Regenerated. +=09* math/libm-test.inc (pow_test): Use ALL_RM_TEST. +=09(pow_tonearest_test_data): Remove. +=09(pow_test_tonearest): Likewise. +=09(pow_towardzero_test_data): Likewise. +=09(pow_test_towardzero): Likewise. +=09(pow_downward_test_data): Likewise. +=09(pow_test_downward): Likewise. +=09(pow_upward_test_data): Likewise. +=09(pow_test_upward): Likewise. +=09(main): Don't call removed functions. +=09* sysdeps/i386/fpu/libm-test-ulps: Update. +=09* sysdeps/x86_64/fpu/libm-test-ulps: Likewise. + +diff --git a/sysdeps/x86_64/fpu/multiarch/e_pow.c b/sysdeps/x86_64/fpu/mult= +iarch/e_pow.c +index a740b6c..433cce0 100644 +--- a/sysdeps/x86_64/fpu/multiarch/e_pow.c ++++ b/sysdeps/x86_64/fpu/multiarch/e_pow.c +@@ -1,5 +1,6 @@ + #ifdef HAVE_FMA4_SUPPORT + # include ++# include + # include +=20 + extern double __ieee754_pow_sse2 (double, double); + +--=20 +Joseph S. Myers +joseph@codesourcery.com +---1152306461-1522705971-1401904246=:3719-- diff --git a/patchwork/tests/mail/0011-no-newline-at-end-of-file.mbox b/patchwork/tests/mail/0011-no-newline-at-end-of-file.mbox new file mode 100644 index 0000000..3ed0597 --- /dev/null +++ b/patchwork/tests/mail/0011-no-newline-at-end-of-file.mbox @@ -0,0 +1,45 @@ +Subject: [PATCH v3 5/5] selftests, powerpc: Add test for VPHN +From: Greg Kurz +To: Michael Ellerman +Cc: Benjamin Herrenschmidt , + linuxppc-dev@lists.ozlabs.org +Date: Mon, 23 Feb 2015 16:14:44 +0100 +MIME-Version: 1.0 +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: 8bit + +The goal is to verify vphn_unpack_associativity() parses VPHN numbers +correctly. We feed it with a variety of input values and compare with +expected results. + +diff --git a/tools/testing/selftests/powerpc/Makefile b/tools/testing/selftests/powerpc/Makefile +index 1d5e7ad..476b8dd 100644 +--- a/tools/testing/selftests/powerpc/Makefile ++++ b/tools/testing/selftests/powerpc/Makefile +@@ -13,7 +13,7 @@ CFLAGS := -Wall -O2 -flto -Wall -Werror -DGIT_VERSION='"$(GIT_VERSION)"' -I$(CUR + + export CC CFLAGS + +-TARGETS = pmu copyloops mm tm primitives stringloops ++TARGETS = pmu copyloops mm tm primitives stringloops vphn + + endif + +diff --git a/tools/testing/selftests/powerpc/vphn/vphn.c b/tools/testing/selftests/powerpc/vphn/vphn.c +new file mode 120000 +index 0000000..186b906 +--- /dev/null ++++ b/tools/testing/selftests/powerpc/vphn/vphn.c +@@ -0,0 +1 @@ ++../../../../../arch/powerpc/mm/vphn.c +\ No newline at end of file +diff --git a/tools/testing/selftests/powerpc/vphn/vphn.h b/tools/testing/selftests/powerpc/vphn/vphn.h +new file mode 120000 +index 0000000..7131efe +--- /dev/null ++++ b/tools/testing/selftests/powerpc/vphn/vphn.h +@@ -0,0 +1 @@ ++../../../../../arch/powerpc/mm/vphn.h +\ No newline at end of file + + diff --git a/patchwork/tests/patches/0001-add-line.patch b/patchwork/tests/patches/0001-add-line.patch new file mode 100644 index 0000000..c6cb9f1 --- /dev/null +++ b/patchwork/tests/patches/0001-add-line.patch @@ -0,0 +1,7 @@ +diff --git a/meep.text b/meep.text +index 3d75d48..a57f4dd 100644 +--- a/meep.text ++++ b/meep.text +@@ -1,1 +1,2 @@ + meep ++meep diff --git a/patchwork/tests/patches/0002-utf-8.patch b/patchwork/tests/patches/0002-utf-8.patch new file mode 100644 index 0000000..71a2f24 --- /dev/null +++ b/patchwork/tests/patches/0002-utf-8.patch @@ -0,0 +1,7 @@ +diff --git a/meep.text b/meep.text +index 3d75d48..a57f4dd 100644 +--- a/meep.text ++++ b/meep.text +@@ -1,1 +1,2 @@ + meep ++meëp diff --git a/patchwork/tests/test_bundles.py b/patchwork/tests/test_bundles.py new file mode 100644 index 0000000..38f3a2c --- /dev/null +++ b/patchwork/tests/test_bundles.py @@ -0,0 +1,646 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2009 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import unittest +import datetime +from django.test import TestCase +from django.test.client import Client +from django.utils.http import urlencode +from django.conf import settings +from patchwork.models import Patch, Bundle, BundlePatch, Person +from patchwork.tests.utils import defaults, create_user, find_in_context + +def bundle_url(bundle): + return '/bundle/%s/%s/' % (bundle.owner.username, bundle.name) + +class BundleListTest(TestCase): + def setUp(self): + self.user = create_user() + self.client.login(username = self.user.username, + password = self.user.username) + + def testNoBundles(self): + response = self.client.get('/user/bundles/') + self.failUnlessEqual(response.status_code, 200) + self.failUnlessEqual( + len(find_in_context(response.context, 'bundles')), 0) + + def testSingleBundle(self): + defaults.project.save() + bundle = Bundle(owner = self.user, project = defaults.project) + bundle.save() + response = self.client.get('/user/bundles/') + self.failUnlessEqual(response.status_code, 200) + self.failUnlessEqual( + len(find_in_context(response.context, 'bundles')), 1) + + def tearDown(self): + self.user.delete() + +class BundleTestBase(TestCase): + def setUp(self, patch_count=3): + patch_names = ['testpatch%d' % (i) for i in range(1, patch_count+1)] + self.user = create_user() + self.client.login(username = self.user.username, + password = self.user.username) + defaults.project.save() + self.bundle = Bundle(owner = self.user, project = defaults.project, + name = 'testbundle') + self.bundle.save() + self.patches = [] + + for patch_name in patch_names: + patch = Patch(project = defaults.project, + msgid = patch_name, name = patch_name, + submitter = Person.objects.get(user = self.user), + content = '') + patch.save() + self.patches.append(patch) + + def tearDown(self): + for patch in self.patches: + patch.delete() + self.bundle.delete() + self.user.delete() + +class BundleViewTest(BundleTestBase): + + def testEmptyBundle(self): + response = self.client.get(bundle_url(self.bundle)) + self.failUnlessEqual(response.status_code, 200) + page = find_in_context(response.context, 'page') + self.failUnlessEqual(len(page.object_list), 0) + + def testNonEmptyBundle(self): + self.bundle.append_patch(self.patches[0]) + + response = self.client.get(bundle_url(self.bundle)) + self.failUnlessEqual(response.status_code, 200) + page = find_in_context(response.context, 'page') + self.failUnlessEqual(len(page.object_list), 1) + + def testBundleOrder(self): + for patch in self.patches: + self.bundle.append_patch(patch) + + response = self.client.get(bundle_url(self.bundle)) + + pos = 0 + for patch in self.patches: + next_pos = response.content.find(patch.name) + # ensure that this patch is after the previous + self.failUnless(next_pos > pos) + pos = next_pos + + # reorder and recheck + i = 0 + for patch in self.patches.__reversed__(): + bundlepatch = BundlePatch.objects.get(bundle = self.bundle, + patch = patch) + bundlepatch.order = i + bundlepatch.save() + i += 1 + + response = self.client.get(bundle_url(self.bundle)) + pos = len(response.content) + for patch in self.patches: + next_pos = response.content.find(patch.name) + # ensure that this patch is now *before* the previous + self.failUnless(next_pos < pos) + pos = next_pos + +class BundleUpdateTest(BundleTestBase): + + def setUp(self): + super(BundleUpdateTest, self).setUp() + self.newname = 'newbundlename' + + def checkPatchformErrors(self, response): + formname = 'patchform' + if not formname in response.context: + return + form = response.context[formname] + if not form: + return + self.assertEquals(form.errors, {}) + + def publicString(self, public): + if public: + return 'on' + return '' + + def testNoAction(self): + data = { + 'form': 'bundle', + 'name': self.newname, + 'public': self.publicString(not self.bundle.public) + } + response = self.client.post(bundle_url(self.bundle), data) + self.assertEqual(response.status_code, 200) + + bundle = Bundle.objects.get(pk = self.bundle.pk) + self.assertEqual(bundle.name, self.bundle.name) + self.assertEqual(bundle.public, self.bundle.public) + + def testUpdateName(self): + newname = 'newbundlename' + data = { + 'form': 'bundle', + 'action': 'update', + 'name': newname, + 'public': self.publicString(self.bundle.public) + } + response = self.client.post(bundle_url(self.bundle), data) + bundle = Bundle.objects.get(pk = self.bundle.pk) + self.assertRedirects(response, bundle_url(bundle)) + self.assertEqual(bundle.name, newname) + self.assertEqual(bundle.public, self.bundle.public) + + def testUpdatePublic(self): + newname = 'newbundlename' + data = { + 'form': 'bundle', + 'action': 'update', + 'name': self.bundle.name, + 'public': self.publicString(not self.bundle.public) + } + response = self.client.post(bundle_url(self.bundle), data) + self.assertEqual(response.status_code, 200) + bundle = Bundle.objects.get(pk = self.bundle.pk) + self.assertEqual(bundle.name, self.bundle.name) + self.assertEqual(bundle.public, not self.bundle.public) + + # check other forms for errors + self.checkPatchformErrors(response) + +class BundleMaintainerUpdateTest(BundleUpdateTest): + + def setUp(self): + super(BundleMaintainerUpdateTest, self).setUp() + profile = self.user.profile + profile.maintainer_projects.add(defaults.project) + profile.save() + +class BundlePublicViewTest(BundleTestBase): + + def setUp(self): + super(BundlePublicViewTest, self).setUp() + self.client.logout() + self.bundle.append_patch(self.patches[0]) + self.url = bundle_url(self.bundle) + + def testPublicBundle(self): + self.bundle.public = True + self.bundle.save() + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertContains(response, self.patches[0].name) + + def testPrivateBundle(self): + self.bundle.public = False + self.bundle.save() + response = self.client.get(self.url) + self.assertEqual(response.status_code, 404) + +class BundlePublicViewMboxTest(BundlePublicViewTest): + def setUp(self): + super(BundlePublicViewMboxTest, self).setUp() + self.url = bundle_url(self.bundle) + "mbox/" + +class BundlePublicModifyTest(BundleTestBase): + """Ensure that non-owners can't modify bundles""" + + def setUp(self): + super(BundlePublicModifyTest, self).setUp() + self.bundle.public = True + self.bundle.save() + self.other_user = create_user() + + def testBundleFormPresence(self): + """Check for presence of the modify form on the bundle""" + self.client.login(username = self.other_user.username, + password = self.other_user.username) + response = self.client.get(bundle_url(self.bundle)) + self.assertNotContains(response, 'name="form" value="bundle"') + self.assertNotContains(response, 'Change order') + + def testBundleFormSubmission(self): + oldname = 'oldbundlename' + newname = 'newbundlename' + data = { + 'form': 'bundle', + 'action': 'update', + 'name': newname, + } + self.bundle.name = oldname + self.bundle.save() + + # first, check that we can modify with the owner + self.client.login(username = self.user.username, + password = self.user.username) + response = self.client.post(bundle_url(self.bundle), data) + self.bundle = Bundle.objects.get(pk = self.bundle.pk) + self.assertEqual(self.bundle.name, newname) + + # reset bundle name + self.bundle.name = oldname + self.bundle.save() + + # log in with a different user, and check that we can no longer modify + self.client.login(username = self.other_user.username, + password = self.other_user.username) + response = self.client.post(bundle_url(self.bundle), data) + self.bundle = Bundle.objects.get(pk = self.bundle.pk) + self.assertNotEqual(self.bundle.name, newname) + +class BundleCreateFromListTest(BundleTestBase): + def testCreateEmptyBundle(self): + newbundlename = 'testbundle-new' + params = {'form': 'patchlistform', + 'bundle_name': newbundlename, + 'action': 'Create', + 'project': defaults.project.id} + + response = self.client.post( + '/project/%s/list/' % defaults.project.linkname, + params) + + self.assertContains(response, 'Bundle %s created' % newbundlename) + + def testCreateNonEmptyBundle(self): + newbundlename = 'testbundle-new' + patch = self.patches[0] + + params = {'form': 'patchlistform', + 'bundle_name': newbundlename, + 'action': 'Create', + 'project': defaults.project.id, + 'patch_id:%d' % patch.id: 'checked'} + + response = self.client.post( + '/project/%s/list/' % defaults.project.linkname, + params) + + self.assertContains(response, 'Bundle %s created' % newbundlename) + self.assertContains(response, 'added to bundle %s' % newbundlename, + count = 1) + + bundle = Bundle.objects.get(name = newbundlename) + self.failUnlessEqual(bundle.patches.count(), 1) + self.failUnlessEqual(bundle.patches.all()[0], patch) + + def testCreateNonEmptyBundleEmptyName(self): + newbundlename = 'testbundle-new' + patch = self.patches[0] + + n_bundles = Bundle.objects.count() + + params = {'form': 'patchlistform', + 'bundle_name': '', + 'action': 'Create', + 'project': defaults.project.id, + 'patch_id:%d' % patch.id: 'checked'} + + response = self.client.post( + '/project/%s/list/' % defaults.project.linkname, + params) + + self.assertContains(response, 'No bundle name was specified', + status_code = 200) + + # test that no new bundles are present + self.failUnlessEqual(n_bundles, Bundle.objects.count()) + + def testCreateDuplicateName(self): + newbundlename = 'testbundle-dup' + patch = self.patches[0] + + params = {'form': 'patchlistform', + 'bundle_name': newbundlename, + 'action': 'Create', + 'project': defaults.project.id, + 'patch_id:%d' % patch.id: 'checked'} + + response = self.client.post( + '/project/%s/list/' % defaults.project.linkname, + params) + + n_bundles = Bundle.objects.count() + self.assertContains(response, 'Bundle %s created' % newbundlename) + self.assertContains(response, 'added to bundle %s' % newbundlename, + count = 1) + + bundle = Bundle.objects.get(name = newbundlename) + self.failUnlessEqual(bundle.patches.count(), 1) + self.failUnlessEqual(bundle.patches.all()[0], patch) + + response = self.client.post( + '/project/%s/list/' % defaults.project.linkname, + params) + + self.assertNotContains(response, 'Bundle %s created' % newbundlename) + self.assertContains(response, 'You already have a bundle called') + self.assertEqual(Bundle.objects.count(), n_bundles) + self.assertEqual(bundle.patches.count(), 1) + +class BundleCreateFromPatchTest(BundleTestBase): + def testCreateNonEmptyBundle(self): + newbundlename = 'testbundle-new' + patch = self.patches[0] + + params = {'name': newbundlename, + 'action': 'createbundle'} + + response = self.client.post('/patch/%d/' % patch.id, params) + + self.assertContains(response, + 'Bundle %s created' % newbundlename) + + bundle = Bundle.objects.get(name = newbundlename) + self.failUnlessEqual(bundle.patches.count(), 1) + self.failUnlessEqual(bundle.patches.all()[0], patch) + + def testCreateWithExistingName(self): + newbundlename = self.bundle.name + patch = self.patches[0] + + params = {'name': newbundlename, + 'action': 'createbundle'} + + response = self.client.post('/patch/%d/' % patch.id, params) + + self.assertContains(response, + 'A bundle called %s already exists' % newbundlename) + + count = Bundle.objects.count() + self.failUnlessEqual(Bundle.objects.count(), 1) + +class BundleAddFromListTest(BundleTestBase): + def testAddToEmptyBundle(self): + patch = self.patches[0] + params = {'form': 'patchlistform', + 'action': 'Add', + 'project': defaults.project.id, + 'bundle_id': self.bundle.id, + 'patch_id:%d' % patch.id: 'checked'} + + response = self.client.post( + '/project/%s/list/' % defaults.project.linkname, + params) + + self.assertContains(response, 'added to bundle %s' % self.bundle.name, + count = 1) + + self.failUnlessEqual(self.bundle.patches.count(), 1) + self.failUnlessEqual(self.bundle.patches.all()[0], patch) + + def testAddToNonEmptyBundle(self): + self.bundle.append_patch(self.patches[0]) + patch = self.patches[1] + params = {'form': 'patchlistform', + 'action': 'Add', + 'project': defaults.project.id, + 'bundle_id': self.bundle.id, + 'patch_id:%d' % patch.id: 'checked'} + + response = self.client.post( + '/project/%s/list/' % defaults.project.linkname, + params) + + self.assertContains(response, 'added to bundle %s' % self.bundle.name, + count = 1) + + self.failUnlessEqual(self.bundle.patches.count(), 2) + self.failUnless(self.patches[0] in self.bundle.patches.all()) + self.failUnless(self.patches[1] in self.bundle.patches.all()) + + # check order + bps = [ BundlePatch.objects.get(bundle = self.bundle, + patch = self.patches[i]) \ + for i in [0, 1] ] + self.failUnless(bps[0].order < bps[1].order) + + def testAddDuplicate(self): + self.bundle.append_patch(self.patches[0]) + count = self.bundle.patches.count() + patch = self.patches[0] + + params = {'form': 'patchlistform', + 'action': 'Add', + 'project': defaults.project.id, + 'bundle_id': self.bundle.id, + 'patch_id:%d' % patch.id: 'checked'} + + response = self.client.post( + '/project/%s/list/' % defaults.project.linkname, + params) + + self.assertContains(response, 'Patch '%s' already in bundle' \ + % patch.name, count = 1, status_code = 200) + + self.assertEquals(count, self.bundle.patches.count()) + + def testAddNewAndDuplicate(self): + self.bundle.append_patch(self.patches[0]) + count = self.bundle.patches.count() + patch = self.patches[0] + + params = {'form': 'patchlistform', + 'action': 'Add', + 'project': defaults.project.id, + 'bundle_id': self.bundle.id, + 'patch_id:%d' % patch.id: 'checked', + 'patch_id:%d' % self.patches[1].id: 'checked'} + + response = self.client.post( + '/project/%s/list/' % defaults.project.linkname, + params) + + self.assertContains(response, 'Patch '%s' already in bundle' \ + % patch.name, count = 1, status_code = 200) + self.assertContains(response, 'Patch '%s' added to bundle' \ + % self.patches[1].name, count = 1, + status_code = 200) + self.assertEquals(count + 1, self.bundle.patches.count()) + +class BundleAddFromPatchTest(BundleTestBase): + def testAddToEmptyBundle(self): + patch = self.patches[0] + params = {'action': 'addtobundle', + 'bundle_id': self.bundle.id} + + response = self.client.post('/patch/%d/' % patch.id, params) + + self.assertContains(response, + 'added to bundle "%s"' % self.bundle.name, + count = 1) + + self.failUnlessEqual(self.bundle.patches.count(), 1) + self.failUnlessEqual(self.bundle.patches.all()[0], patch) + + def testAddToNonEmptyBundle(self): + self.bundle.append_patch(self.patches[0]) + patch = self.patches[1] + params = {'action': 'addtobundle', + 'bundle_id': self.bundle.id} + + response = self.client.post('/patch/%d/' % patch.id, params) + + self.assertContains(response, + 'added to bundle "%s"' % self.bundle.name, + count = 1) + + self.failUnlessEqual(self.bundle.patches.count(), 2) + self.failUnless(self.patches[0] in self.bundle.patches.all()) + self.failUnless(self.patches[1] in self.bundle.patches.all()) + + # check order + bps = [ BundlePatch.objects.get(bundle = self.bundle, + patch = self.patches[i]) \ + for i in [0, 1] ] + self.failUnless(bps[0].order < bps[1].order) + +class BundleInitialOrderTest(BundleTestBase): + """When creating bundles from a patch list, ensure that the patches in the + bundle are ordered by date""" + + def setUp(self): + super(BundleInitialOrderTest, self).setUp(5) + + # put patches in an arbitrary order + idxs = [2, 4, 3, 1, 0] + self.patches = [ self.patches[i] for i in idxs ] + + # set dates to be sequential + last_patch = self.patches[0] + for patch in self.patches[1:]: + patch.date = last_patch.date + datetime.timedelta(0, 1) + patch.save() + last_patch = patch + + def _testOrder(self, ids, expected_order): + newbundlename = 'testbundle-new' + + # need to define our querystring explicity to enforce ordering + params = {'form': 'patchlistform', + 'bundle_name': newbundlename, + 'action': 'Create', + 'project': defaults.project.id, + } + + data = urlencode(params) + \ + ''.join([ '&patch_id:%d=checked' % i for i in ids ]) + + response = self.client.post( + '/project/%s/list/' % defaults.project.linkname, + data = data, + content_type = 'application/x-www-form-urlencoded', + ) + + self.assertContains(response, 'Bundle %s created' % newbundlename) + self.assertContains(response, 'added to bundle %s' % newbundlename, + count = 5) + + bundle = Bundle.objects.get(name = newbundlename) + + # BundlePatches should be sorted by .order by default + bps = BundlePatch.objects.filter(bundle = bundle) + + for (bp, p) in zip(bps, expected_order): + self.assertEqual(bp.patch.pk, p.pk) + + bundle.delete() + + def testBundleForwardOrder(self): + ids = map(lambda p: p.id, self.patches) + self._testOrder(ids, self.patches) + + def testBundleReverseOrder(self): + ids = map(lambda p: p.id, self.patches) + ids.reverse() + self._testOrder(ids, self.patches) + +class BundleReorderTest(BundleTestBase): + def setUp(self): + super(BundleReorderTest, self).setUp(5) + for i in range(5): + self.bundle.append_patch(self.patches[i]) + + def checkReordering(self, neworder, start, end): + neworder_ids = [ self.patches[i].id for i in neworder ] + + firstpatch = BundlePatch.objects.get(bundle = self.bundle, + patch = self.patches[start]).patch + + slice_ids = neworder_ids[start:end] + params = {'form': 'reorderform', + 'order_start': firstpatch.id, + 'neworder': slice_ids} + + response = self.client.post(bundle_url(self.bundle), params) + + self.failUnlessEqual(response.status_code, 200) + + bps = BundlePatch.objects.filter(bundle = self.bundle) \ + .order_by('order') + + # check if patch IDs are in the expected order: + bundle_ids = [ bp.patch.id for bp in bps ] + self.failUnlessEqual(neworder_ids, bundle_ids) + + # check if order field is still sequential: + order_numbers = [ bp.order for bp in bps ] + expected_order = range(1, len(neworder)+1) # [1 ... len(neworder)] + self.failUnlessEqual(order_numbers, expected_order) + + def testBundleReorderAll(self): + # reorder all patches: + self.checkReordering([2,1,4,0,3], 0, 5) + + def testBundleReorderEnd(self): + # reorder only the last three patches + self.checkReordering([0,1,3,2,4], 2, 5) + + def testBundleReorderBegin(self): + # reorder only the first three patches + self.checkReordering([2,0,1,3,4], 0, 3) + + def testBundleReorderMiddle(self): + # reorder only 2nd, 3rd, and 4th patches + self.checkReordering([0,2,3,1,4], 1, 4) + +class BundleRedirTest(BundleTestBase): + # old URL: private bundles used to be under /user/bundle/ + + def setUp(self): + super(BundleRedirTest, self).setUp() + + @unittest.skipIf(not settings.COMPAT_REDIR, "compat redirections disabled") + def testBundleRedir(self): + url = '/user/bundle/%d/' % self.bundle.id + response = self.client.get(url) + self.assertRedirects(response, bundle_url(self.bundle)) + + @unittest.skipIf(not settings.COMPAT_REDIR, "compat redirections disabled") + def testMboxRedir(self): + url = '/user/bundle/%d/mbox/' % self.bundle.id + response = self.client.get(url) + self.assertRedirects(response,'/bundle/%s/%s/mbox/' % + (self.bundle.owner.username, + self.bundle.name)) diff --git a/patchwork/tests/test_confirm.py b/patchwork/tests/test_confirm.py new file mode 100644 index 0000000..fad5125 --- /dev/null +++ b/patchwork/tests/test_confirm.py @@ -0,0 +1,67 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2011 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import unittest +from django.test import TestCase +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from patchwork.models import EmailConfirmation, Person + +def _confirmation_url(conf): + return reverse('patchwork.views.confirm', kwargs = {'key': conf.key}) + +class TestUser(object): + username = 'testuser' + email = 'test@example.com' + secondary_email = 'test2@example.com' + password = None + + def __init__(self): + self.password = User.objects.make_random_password() + self.user = User.objects.create_user(self.username, + self.email, self.password) + +class InvalidConfirmationTest(TestCase): + def setUp(self): + EmailConfirmation.objects.all().delete() + Person.objects.all().delete() + self.user = TestUser() + self.conf = EmailConfirmation(type = 'userperson', + email = self.user.secondary_email, + user = self.user.user) + self.conf.save() + + def testInactiveConfirmation(self): + self.conf.active = False + self.conf.save() + response = self.client.get(_confirmation_url(self.conf)) + self.assertEquals(response.status_code, 200) + self.assertTemplateUsed(response, 'patchwork/confirm-error.html') + self.assertEqual(response.context['error'], 'inactive') + self.assertEqual(response.context['conf'], self.conf) + + def testExpiredConfirmation(self): + self.conf.date -= self.conf.validity + self.conf.save() + response = self.client.get(_confirmation_url(self.conf)) + self.assertEquals(response.status_code, 200) + self.assertTemplateUsed(response, 'patchwork/confirm-error.html') + self.assertEqual(response.context['error'], 'expired') + self.assertEqual(response.context['conf'], self.conf) + diff --git a/patchwork/tests/test_encodings.py b/patchwork/tests/test_encodings.py new file mode 100644 index 0000000..b9032bb --- /dev/null +++ b/patchwork/tests/test_encodings.py @@ -0,0 +1,87 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import unittest +import os +import time +from patchwork.models import Patch, Person +from patchwork.tests.utils import defaults, read_patch +from django.test import TestCase +from django.test.client import Client + +class UTF8PatchViewTest(TestCase): + patch_filename = '0002-utf-8.patch' + patch_encoding = 'utf-8' + + def setUp(self): + defaults.project.save() + defaults.patch_author_person.save() + self.patch_content = read_patch(self.patch_filename, + encoding = self.patch_encoding) + self.patch = Patch(project = defaults.project, + msgid = 'x', name = defaults.patch_name, + submitter = defaults.patch_author_person, + content = self.patch_content) + self.patch.save() + self.client = Client() + + def testPatchView(self): + response = self.client.get('/patch/%d/' % self.patch.id) + self.assertContains(response, self.patch.name) + + def testMboxView(self): + response = self.client.get('/patch/%d/mbox/' % self.patch.id) + self.assertEquals(response.status_code, 200) + self.assertTrue(self.patch.content in \ + response.content.decode(self.patch_encoding)) + + def testRawView(self): + response = self.client.get('/patch/%d/raw/' % self.patch.id) + self.assertEquals(response.status_code, 200) + self.assertEquals(response.content.decode(self.patch_encoding), + self.patch.content) + + def tearDown(self): + self.patch.delete() + defaults.patch_author_person.delete() + defaults.project.delete() + +class UTF8HeaderPatchViewTest(UTF8PatchViewTest): + patch_filename = '0002-utf-8.patch' + patch_encoding = 'utf-8' + patch_author_name = u'P\xe4tch Author' + + def setUp(self): + defaults.project.save() + self.patch_author = Person(name = self.patch_author_name, + email = defaults.patch_author_person.email) + self.patch_author.save() + self.patch_content = read_patch(self.patch_filename, + encoding = self.patch_encoding) + self.patch = Patch(project = defaults.project, + msgid = 'x', name = defaults.patch_name, + submitter = self.patch_author, + content = self.patch_content) + self.patch.save() + self.client = Client() + + def tearDown(self): + self.patch.delete() + self.patch_author.delete() + defaults.project.delete() diff --git a/patchwork/tests/test_expiry.py b/patchwork/tests/test_expiry.py new file mode 100644 index 0000000..844ed4b --- /dev/null +++ b/patchwork/tests/test_expiry.py @@ -0,0 +1,121 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2014 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import unittest +import datetime +from django.test import TestCase +from django.contrib.auth.models import User +from patchwork.models import EmailConfirmation, Person, Patch +from patchwork.tests.utils import create_user, defaults +from patchwork.utils import do_expiry + +class TestRegistrationExpiry(TestCase): + + def register(self, date): + user = create_user() + user.is_active = False + user.date_joined = user.last_login = date + user.save() + + conf = EmailConfirmation(type='registration', user=user, + email=user.email) + conf.date = date + conf.save() + + return (user, conf) + + def testOldRegistrationExpiry(self): + date = ((datetime.datetime.now() - EmailConfirmation.validity) - + datetime.timedelta(hours = 1)) + (user, conf) = self.register(date) + + do_expiry() + + self.assertFalse(User.objects.filter(pk = user.pk).exists()) + self.assertFalse(EmailConfirmation.objects.filter(pk = conf.pk) + .exists()) + + + def testRecentRegistrationExpiry(self): + date = ((datetime.datetime.now() - EmailConfirmation.validity) + + datetime.timedelta(hours = 1)) + (user, conf) = self.register(date) + + do_expiry() + + self.assertTrue(User.objects.filter(pk = user.pk).exists()) + self.assertTrue(EmailConfirmation.objects.filter(pk = conf.pk) + .exists()) + + def testInactiveRegistrationExpiry(self): + (user, conf) = self.register(datetime.datetime.now()) + + # confirm registration + conf.user.is_active = True + conf.user.save() + conf.deactivate() + + do_expiry() + + self.assertTrue(User.objects.filter(pk = user.pk).exists()) + self.assertFalse(EmailConfirmation.objects.filter(pk = conf.pk) + .exists()) + + def testPatchSubmitterExpiry(self): + defaults.project.save() + defaults.patch_author_person.save() + + # someone submits a patch... + patch = Patch(project = defaults.project, + msgid = 'test@example.com', name = 'test patch', + submitter = defaults.patch_author_person, + content = defaults.patch) + patch.save() + + # ... then starts registration... + date = ((datetime.datetime.now() - EmailConfirmation.validity) - + datetime.timedelta(hours = 1)) + userid = 'test-user' + user = User.objects.create_user(userid, + defaults.patch_author_person.email, userid) + user.is_active = False + user.date_joined = user.last_login = date + user.save() + + self.assertEqual(user.email, patch.submitter.email) + + conf = EmailConfirmation(type='registration', user=user, + email=user.email) + conf.date = date + conf.save() + + # ... which expires + do_expiry() + + # we should see no matching user + self.assertFalse(User.objects.filter(email = patch.submitter.email) + .exists()) + # but the patch and person should still be present + self.assertTrue(Person.objects.filter( + pk = defaults.patch_author_person.pk).exists()) + self.assertTrue(Patch.objects.filter(pk = patch.pk).exists()) + + # and there should be no user associated with the person + self.assertEqual(Person.objects.get(pk = + defaults.patch_author_person.pk).user, None) diff --git a/patchwork/tests/test_filters.py b/patchwork/tests/test_filters.py new file mode 100644 index 0000000..2c464e5 --- /dev/null +++ b/patchwork/tests/test_filters.py @@ -0,0 +1,45 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2011 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import unittest +from django.test import TestCase +from django.test.client import Client +from patchwork.tests.utils import defaults, create_user, find_in_context + +class FilterQueryStringTest(TestCase): + def testFilterQSEscaping(self): + """test that filter fragments in a query string are properly escaped, + and stray ampersands don't get reflected back in the filter + links""" + project = defaults.project + defaults.project.save() + url = '/project/%s/list/?submitter=a%%26b=c' % project.linkname + response = self.client.get(url) + self.failUnlessEqual(response.status_code, 200) + self.failIf('submitter=a&b=c' in response.content) + self.failIf('submitter=a&b=c' in response.content) + + def testUTF8QSHandling(self): + """test that non-ascii characters can be handled by the filter + code""" + project = defaults.project + defaults.project.save() + url = '/project/%s/list/?submitter=%%E2%%98%%83' % project.linkname + response = self.client.get(url) + self.failUnlessEqual(response.status_code, 200) diff --git a/patchwork/tests/test_list.py b/patchwork/tests/test_list.py new file mode 100644 index 0000000..a795a5f --- /dev/null +++ b/patchwork/tests/test_list.py @@ -0,0 +1,116 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2012 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import unittest +import random +import datetime +import string +import re +from django.test import TestCase +from django.test.client import Client +from patchwork.tests.utils import defaults, create_user, find_in_context +from patchwork.models import Person, Patch +from django.core.urlresolvers import reverse + +class EmptyPatchListTest(TestCase): + + def testEmptyPatchList(self): + """test that we don't output an empty table when there are no + patches present""" + project = defaults.project + defaults.project.save() + url = reverse('patchwork.views.patch.list', + kwargs={'project_id': project.linkname}) + response = self.client.get(url) + self.assertContains(response, 'No patches to display') + self.assertNotContains(response, 'tbody') + +class PatchOrderTest(TestCase): + + d = datetime.datetime + patchmeta = [ + ('AlCMyjOsx', 'AlxMyjOsx@nRbqkQV.wBw', d(2014,3,16,13, 4,50, 155643)), + ('MMZnrcDjT', 'MMmnrcDjT@qGaIfOl.tbk', d(2014,1,25,13, 4,50, 162814)), + ('WGirwRXgK', 'WGSrwRXgK@TriIETY.GhE', d(2014,2,14,13, 4,50, 169305)), + ('isjNIuiAc', 'issNIuiAc@OsEirYx.EJh', d(2014,3,15,13, 4,50, 176264)), + ('XkAQpYGws', 'XkFQpYGws@hzntTcm.JSE', d(2014,1,18,13, 4,50, 182493)), + ('uJuCPWMvi', 'uJACPWMvi@AVRBOBl.ecy', d(2014,3,12,13, 4,50, 189554)), + ('TyQmWtcbg', 'TylmWtcbg@DzrNeNH.JuB', d(2014,2, 3,13, 4,50, 195685)), + ('FpvAhWRdX', 'FpKAhWRdX@agxnCAI.wFO', d(2014,3,15,13, 4,50, 201398)), + ('bmoYvnyWa', 'bmdYvnyWa@aeoPnlX.juy', d(2014,3, 4,13, 4,50, 206800)), + ('CiReUQsAq', 'CiieUQsAq@DnOYRuf.TTI', d(2014,3,28,13, 4,50, 212169)), + ] + + def setUp(self): + defaults.project.save() + + for (name, email, date) in self.patchmeta: + patch_name = 'testpatch' + name + person = Person(name = name, email = email) + person.save() + patch = Patch(project = defaults.project, msgid = patch_name, + submitter = person, content = '', date = date) + patch.save() + + def _extract_patch_ids(self, response): + id_re = re.compile(' +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import unittest +import re +from django.test import TestCase +from django.test.client import Client +from django.core import mail +from django.core.urlresolvers import reverse +from django.contrib.auth.models import User +from patchwork.models import EmailOptout, EmailConfirmation, Person +from patchwork.tests.utils import create_user, error_strings + +class MailSettingsTest(TestCase): + view = 'patchwork.views.mail.settings' + url = reverse(view) + + def testMailSettingsGET(self): + response = self.client.get(self.url) + self.assertEquals(response.status_code, 200) + self.assertTrue(response.context['form']) + + def testMailSettingsPOST(self): + email = u'foo@example.com' + response = self.client.post(self.url, {'email': email}) + self.assertEquals(response.status_code, 200) + self.assertTemplateUsed(response, 'patchwork/mail-settings.html') + self.assertEquals(response.context['email'], email) + + def testMailSettingsPOSTEmpty(self): + response = self.client.post(self.url, {'email': ''}) + self.assertEquals(response.status_code, 200) + self.assertTemplateUsed(response, 'patchwork/mail-form.html') + self.assertFormError(response, 'form', 'email', + 'This field is required.') + + def testMailSettingsPOSTInvalid(self): + response = self.client.post(self.url, {'email': 'foo'}) + self.assertEquals(response.status_code, 200) + self.assertTemplateUsed(response, 'patchwork/mail-form.html') + self.assertFormError(response, 'form', 'email', error_strings['email']) + + def testMailSettingsPOSTOptedIn(self): + email = u'foo@example.com' + response = self.client.post(self.url, {'email': email}) + self.assertEquals(response.status_code, 200) + self.assertTemplateUsed(response, 'patchwork/mail-settings.html') + self.assertEquals(response.context['is_optout'], False) + self.assertTrue('may' in response.content) + optout_url = reverse('patchwork.views.mail.optout') + self.assertTrue(('action="%s"' % optout_url) in response.content) + + def testMailSettingsPOSTOptedOut(self): + email = u'foo@example.com' + EmailOptout(email = email).save() + response = self.client.post(self.url, {'email': email}) + self.assertEquals(response.status_code, 200) + self.assertTemplateUsed(response, 'patchwork/mail-settings.html') + self.assertEquals(response.context['is_optout'], True) + self.assertTrue('may not' in response.content) + optin_url = reverse('patchwork.views.mail.optin') + self.assertTrue(('action="%s"' % optin_url) in response.content) + +class OptoutRequestTest(TestCase): + view = 'patchwork.views.mail.optout' + url = reverse(view) + + def testOptOutRequestGET(self): + response = self.client.get(self.url) + self.assertRedirects(response, reverse('patchwork.views.mail.settings')) + + def testOptoutRequestValidPOST(self): + email = u'foo@example.com' + response = self.client.post(self.url, {'email': email}) + + # check for a confirmation object + self.assertEquals(EmailConfirmation.objects.count(), 1) + conf = EmailConfirmation.objects.get(email = email) + + # check confirmation page + self.assertEquals(response.status_code, 200) + self.assertEquals(response.context['confirmation'], conf) + self.assertTrue(email in response.content) + + # check email + url = reverse('patchwork.views.confirm', kwargs = {'key': conf.key}) + self.assertEquals(len(mail.outbox), 1) + msg = mail.outbox[0] + self.assertEquals(msg.to, [email]) + self.assertEquals(msg.subject, 'Patchwork opt-out confirmation') + self.assertTrue(url in msg.body) + + def testOptoutRequestInvalidPOSTEmpty(self): + response = self.client.post(self.url, {'email': ''}) + self.assertEquals(response.status_code, 200) + self.assertFormError(response, 'form', 'email', + 'This field is required.') + self.assertTrue(response.context['error']) + self.assertTrue('email_sent' not in response.context) + self.assertEquals(len(mail.outbox), 0) + + def testOptoutRequestInvalidPOSTNonEmail(self): + response = self.client.post(self.url, {'email': 'foo'}) + self.assertEquals(response.status_code, 200) + self.assertFormError(response, 'form', 'email', error_strings['email']) + self.assertTrue(response.context['error']) + self.assertTrue('email_sent' not in response.context) + self.assertEquals(len(mail.outbox), 0) + +class OptoutTest(TestCase): + view = 'patchwork.views.mail.optout' + url = reverse(view) + + def setUp(self): + self.email = u'foo@example.com' + self.conf = EmailConfirmation(type = 'optout', email = self.email) + self.conf.save() + + def testOptoutValidHash(self): + url = reverse('patchwork.views.confirm', + kwargs = {'key': self.conf.key}) + response = self.client.get(url) + + self.assertEquals(response.status_code, 200) + self.assertTemplateUsed(response, 'patchwork/optout.html') + self.assertTrue(self.email in response.content) + + # check that we've got an optout in the list + self.assertEquals(EmailOptout.objects.count(), 1) + self.assertEquals(EmailOptout.objects.all()[0].email, self.email) + + # check that the confirmation is now inactive + self.assertFalse(EmailConfirmation.objects.get( + pk = self.conf.pk).active) + + +class OptoutPreexistingTest(OptoutTest): + """Test that a duplicated opt-out behaves the same as the initial one""" + def setUp(self): + super(OptoutPreexistingTest, self).setUp() + EmailOptout(email = self.email).save() + +class OptinRequestTest(TestCase): + view = 'patchwork.views.mail.optin' + url = reverse(view) + + def setUp(self): + self.email = u'foo@example.com' + EmailOptout(email = self.email).save() + + def testOptInRequestGET(self): + response = self.client.get(self.url) + self.assertRedirects(response, reverse('patchwork.views.mail.settings')) + + def testOptInRequestValidPOST(self): + response = self.client.post(self.url, {'email': self.email}) + + # check for a confirmation object + self.assertEquals(EmailConfirmation.objects.count(), 1) + conf = EmailConfirmation.objects.get(email = self.email) + + # check confirmation page + self.assertEquals(response.status_code, 200) + self.assertEquals(response.context['confirmation'], conf) + self.assertTrue(self.email in response.content) + + # check email + url = reverse('patchwork.views.confirm', kwargs = {'key': conf.key}) + self.assertEquals(len(mail.outbox), 1) + msg = mail.outbox[0] + self.assertEquals(msg.to, [self.email]) + self.assertEquals(msg.subject, 'Patchwork opt-in confirmation') + self.assertTrue(url in msg.body) + + def testOptoutRequestInvalidPOSTEmpty(self): + response = self.client.post(self.url, {'email': ''}) + self.assertEquals(response.status_code, 200) + self.assertFormError(response, 'form', 'email', + 'This field is required.') + self.assertTrue(response.context['error']) + self.assertTrue('email_sent' not in response.context) + self.assertEquals(len(mail.outbox), 0) + + def testOptoutRequestInvalidPOSTNonEmail(self): + response = self.client.post(self.url, {'email': 'foo'}) + self.assertEquals(response.status_code, 200) + self.assertFormError(response, 'form', 'email', error_strings['email']) + self.assertTrue(response.context['error']) + self.assertTrue('email_sent' not in response.context) + self.assertEquals(len(mail.outbox), 0) + +class OptinTest(TestCase): + + def setUp(self): + self.email = u'foo@example.com' + self.optout = EmailOptout(email = self.email) + self.optout.save() + self.conf = EmailConfirmation(type = 'optin', email = self.email) + self.conf.save() + + def testOptinValidHash(self): + url = reverse('patchwork.views.confirm', + kwargs = {'key': self.conf.key}) + response = self.client.get(url) + + self.assertEquals(response.status_code, 200) + self.assertTemplateUsed(response, 'patchwork/optin.html') + self.assertTrue(self.email in response.content) + + # check that there's no optout remaining + self.assertEquals(EmailOptout.objects.count(), 0) + + # check that the confirmation is now inactive + self.assertFalse(EmailConfirmation.objects.get( + pk = self.conf.pk).active) + +class OptinWithoutOptoutTest(TestCase): + """Test an opt-in with no existing opt-out""" + view = 'patchwork.views.mail.optin' + url = reverse(view) + + def testOptInWithoutOptout(self): + email = u'foo@example.com' + response = self.client.post(self.url, {'email': email}) + + # check for an error message + self.assertEquals(response.status_code, 200) + self.assertTrue(bool(response.context['error'])) + self.assertTrue('not on the patchwork opt-out list' in response.content) + +class UserProfileOptoutFormTest(TestCase): + """Test that the correct optin/optout forms appear on the user profile + page, for logged-in users""" + + view = 'patchwork.views.user.profile' + url = reverse(view) + optout_url = reverse('patchwork.views.mail.optout') + optin_url = reverse('patchwork.views.mail.optin') + form_re_template = (']*action="%(url)s"[^>]*>' + '.*?]*value="%(email)s"[^>]*>.*?' + '') + secondary_email = 'test2@example.com' + + def setUp(self): + self.user = create_user() + self.client.login(username = self.user.username, + password = self.user.username) + + def _form_re(self, url, email): + return re.compile(self.form_re_template % {'url': url, 'email': email}, + re.DOTALL) + + def testMainEmailOptoutForm(self): + form_re = self._form_re(self.optout_url, self.user.email) + response = self.client.get(self.url) + self.assertEquals(response.status_code, 200) + self.assertTrue(form_re.search(response.content) is not None) + + def testMainEmailOptinForm(self): + EmailOptout(email = self.user.email).save() + form_re = self._form_re(self.optin_url, self.user.email) + response = self.client.get(self.url) + self.assertEquals(response.status_code, 200) + self.assertTrue(form_re.search(response.content) is not None) + + def testSecondaryEmailOptoutForm(self): + p = Person(email = self.secondary_email, user = self.user) + p.save() + + form_re = self._form_re(self.optout_url, p.email) + response = self.client.get(self.url) + self.assertEquals(response.status_code, 200) + self.assertTrue(form_re.search(response.content) is not None) + + def testSecondaryEmailOptinForm(self): + p = Person(email = self.secondary_email, user = self.user) + p.save() + EmailOptout(email = p.email).save() + + form_re = self._form_re(self.optin_url, p.email) + response = self.client.get(self.url) + self.assertEquals(response.status_code, 200) + self.assertTrue(form_re.search(response.content) is not None) diff --git a/patchwork/tests/test_mboxviews.py b/patchwork/tests/test_mboxviews.py new file mode 100644 index 0000000..0e57f42 --- /dev/null +++ b/patchwork/tests/test_mboxviews.py @@ -0,0 +1,209 @@ +# vim: set fileencoding=utf-8 : +# +# Patchwork - automated patch tracking system +# Copyright (C) 2009 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import unittest +import email +import datetime +import dateutil.parser, dateutil.tz +from django.test import TestCase +from django.test.client import Client +from patchwork.models import Patch, Comment, Person +from patchwork.tests.utils import defaults, create_user, find_in_context + +class MboxPatchResponseTest(TestCase): + """ Test that the mbox view appends the Acked-by from a patch comment """ + def setUp(self): + defaults.project.save() + + self.person = defaults.patch_author_person + self.person.save() + + self.patch = Patch(project = defaults.project, + msgid = 'p1', name = 'testpatch', + submitter = self.person, content = '') + self.patch.save() + comment = Comment(patch = self.patch, msgid = 'p1', + submitter = self.person, + content = 'comment 1 text\nAcked-by: 1\n') + comment.save() + + comment = Comment(patch = self.patch, msgid = 'p2', + submitter = self.person, + content = 'comment 2 text\nAcked-by: 2\n') + comment.save() + + def testPatchResponse(self): + response = self.client.get('/patch/%d/mbox/' % self.patch.id) + self.assertContains(response, + 'Acked-by: 1\nAcked-by: 2\n') + +class MboxPatchSplitResponseTest(TestCase): + """ Test that the mbox view appends the Acked-by from a patch comment, + and places it before an '---' update line. """ + def setUp(self): + defaults.project.save() + + self.person = defaults.patch_author_person + self.person.save() + + self.patch = Patch(project = defaults.project, + msgid = 'p1', name = 'testpatch', + submitter = self.person, content = '') + self.patch.save() + comment = Comment(patch = self.patch, msgid = 'p1', + submitter = self.person, + content = 'comment 1 text\nAcked-by: 1\n---\nupdate\n') + comment.save() + + comment = Comment(patch = self.patch, msgid = 'p2', + submitter = self.person, + content = 'comment 2 text\nAcked-by: 2\n') + comment.save() + + def testPatchResponse(self): + response = self.client.get('/patch/%d/mbox/' % self.patch.id) + self.assertContains(response, + 'Acked-by: 1\nAcked-by: 2\n') + +class MboxPassThroughHeaderTest(TestCase): + """ Test that we see 'Cc' and 'To' headers passed through from original + message to mbox view """ + + def setUp(self): + defaults.project.save() + self.person = defaults.patch_author_person + self.person.save() + + self.cc_header = 'Cc: CC Person ' + self.to_header = 'To: To Person ' + self.date_header = 'Date: Fri, 7 Jun 2013 15:42:54 +1000' + + self.patch = Patch(project = defaults.project, + msgid = 'p1', name = 'testpatch', + submitter = self.person, content = '') + + def testCCHeader(self): + self.patch.headers = self.cc_header + '\n' + self.patch.save() + + response = self.client.get('/patch/%d/mbox/' % self.patch.id) + self.assertContains(response, self.cc_header) + + def testToHeader(self): + self.patch.headers = self.to_header + '\n' + self.patch.save() + + response = self.client.get('/patch/%d/mbox/' % self.patch.id) + self.assertContains(response, self.to_header) + + def testDateHeader(self): + self.patch.headers = self.date_header + '\n' + self.patch.save() + + response = self.client.get('/patch/%d/mbox/' % self.patch.id) + self.assertContains(response, self.date_header) + +class MboxBrokenFromHeaderTest(TestCase): + """ Test that a person with characters outside ASCII in his name do + produce correct From header. As RFC 2822 state we must retain the + format for the mail while the name part may be coded + in some ways. """ + + def setUp(self): + defaults.project.save() + self.person = defaults.patch_author_person + self.person.name = u'©ool guŷ' + self.person.save() + + self.patch = Patch(project = defaults.project, + msgid = 'p1', name = 'testpatch', + submitter = self.person, content = '') + + def testFromHeader(self): + self.patch.save() + from_email = '<' + self.person.email + '>' + + response = self.client.get('/patch/%d/mbox/' % self.patch.id) + self.assertContains(response, from_email) + +class MboxDateHeaderTest(TestCase): + """ Test that the date provided in the patch mail view is correct """ + + def setUp(self): + defaults.project.save() + self.person = defaults.patch_author_person + self.person.save() + + self.patch = Patch(project = defaults.project, + msgid = 'p1', name = 'testpatch', + submitter = self.person, content = '') + self.patch.save() + + def testDateHeader(self): + response = self.client.get('/patch/%d/mbox/' % self.patch.id) + mail = email.message_from_string(response.content) + mail_date = dateutil.parser.parse(mail['Date']) + # patch dates are all in UTC + patch_date = self.patch.date.replace(tzinfo=dateutil.tz.tzutc(), + microsecond=0) + self.assertEqual(mail_date, patch_date) + + def testSuppliedDateHeader(self): + hour_offset = 3 + tz = dateutil.tz.tzoffset(None, hour_offset * 60 * 60) + date = datetime.datetime.utcnow() - datetime.timedelta(days = 1) + date = date.replace(tzinfo=tz, microsecond=0) + + self.patch.headers = 'Date: %s\n' % date.strftime("%a, %d %b %Y %T %z") + self.patch.save() + + response = self.client.get('/patch/%d/mbox/' % self.patch.id) + mail = email.message_from_string(response.content) + mail_date = dateutil.parser.parse(mail['Date']) + self.assertEqual(mail_date, date) + +class MboxCommentPostcriptUnchangedTest(TestCase): + """ Test that the mbox view doesn't change the postscript part of a mail. + There where always a missing blank right after the postscript + delimiter '---' and an additional newline right before. """ + def setUp(self): + defaults.project.save() + + self.person = defaults.patch_author_person + self.person.save() + + self.patch = Patch(project = defaults.project, + msgid = 'p1', name = 'testpatch', + submitter = self.person, content = '') + self.patch.save() + + self.txt = 'some comment\n---\n some/file | 1 +\n' + + comment = Comment(patch = self.patch, msgid = 'p1', + submitter = self.person, + content = self.txt) + comment.save() + + def testCommentUnchanged(self): + response = self.client.get('/patch/%d/mbox/' % self.patch.id) + self.assertContains(response, self.txt) + self.txt += "\n" + self.assertNotContains(response, self.txt) diff --git a/patchwork/tests/test_notifications.py b/patchwork/tests/test_notifications.py new file mode 100644 index 0000000..ed35140 --- /dev/null +++ b/patchwork/tests/test_notifications.py @@ -0,0 +1,255 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2011 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import datetime +from django.test import TestCase +from django.core.urlresolvers import reverse +from django.core import mail +from django.conf import settings +from django.db.utils import IntegrityError +from patchwork.models import Patch, State, PatchChangeNotification, EmailOptout +from patchwork.tests.utils import defaults, create_maintainer +from patchwork.utils import send_notifications + +class PatchNotificationModelTest(TestCase): + """Tests for the creation & update of the PatchChangeNotification model""" + + def setUp(self): + self.project = defaults.project + self.project.send_notifications = True + self.project.save() + self.submitter = defaults.patch_author_person + self.submitter.save() + self.patch = Patch(project = self.project, msgid = 'testpatch', + name = 'testpatch', content = '', + submitter = self.submitter) + + def tearDown(self): + self.patch.delete() + self.submitter.delete() + self.project.delete() + + def testPatchCreation(self): + """Ensure we don't get a notification on create""" + self.patch.save() + self.assertEqual(PatchChangeNotification.objects.count(), 0) + + def testPatchUninterestingChange(self): + """Ensure we don't get a notification for "uninteresting" changes""" + self.patch.save() + self.patch.archived = True + self.patch.save() + self.assertEqual(PatchChangeNotification.objects.count(), 0) + + def testPatchChange(self): + """Ensure we get a notification for interesting patch changes""" + self.patch.save() + oldstate = self.patch.state + state = State.objects.exclude(pk = oldstate.pk)[0] + + self.patch.state = state + self.patch.save() + self.assertEqual(PatchChangeNotification.objects.count(), 1) + notification = PatchChangeNotification.objects.all()[0] + self.assertEqual(notification.patch, self.patch) + self.assertEqual(notification.orig_state, oldstate) + + def testNotificationCancelled(self): + """Ensure we cancel notifications that are no longer valid""" + self.patch.save() + oldstate = self.patch.state + state = State.objects.exclude(pk = oldstate.pk)[0] + + self.patch.state = state + self.patch.save() + self.assertEqual(PatchChangeNotification.objects.count(), 1) + + self.patch.state = oldstate + self.patch.save() + self.assertEqual(PatchChangeNotification.objects.count(), 0) + + def testNotificationUpdated(self): + """Ensure we update notifications when the patch has a second change, + but keep the original patch details""" + self.patch.save() + oldstate = self.patch.state + newstates = State.objects.exclude(pk = oldstate.pk)[:2] + + self.patch.state = newstates[0] + self.patch.save() + self.assertEqual(PatchChangeNotification.objects.count(), 1) + notification = PatchChangeNotification.objects.all()[0] + self.assertEqual(notification.orig_state, oldstate) + orig_timestamp = notification.last_modified + + self.patch.state = newstates[1] + self.patch.save() + self.assertEqual(PatchChangeNotification.objects.count(), 1) + notification = PatchChangeNotification.objects.all()[0] + self.assertEqual(notification.orig_state, oldstate) + self.assertTrue(notification.last_modified >= orig_timestamp) + + def testProjectNotificationsDisabled(self): + """Ensure we don't see notifications created when a project is + configured not to send them""" + self.project.send_notifications = False + self.project.save() + + self.patch.save() + oldstate = self.patch.state + state = State.objects.exclude(pk = oldstate.pk)[0] + + self.patch.state = state + self.patch.save() + self.assertEqual(PatchChangeNotification.objects.count(), 0) + +class PatchNotificationEmailTest(TestCase): + + def setUp(self): + self.project = defaults.project + self.project.send_notifications = True + self.project.save() + self.submitter = defaults.patch_author_person + self.submitter.save() + self.patch = Patch(project = self.project, msgid = 'testpatch', + name = 'testpatch', content = '', + submitter = self.submitter) + self.patch.save() + + def tearDown(self): + self.patch.delete() + self.submitter.delete() + self.project.delete() + + def _expireNotifications(self, **kwargs): + timestamp = datetime.datetime.now() - \ + datetime.timedelta(minutes = + settings.NOTIFICATION_DELAY_MINUTES + 1) + + qs = PatchChangeNotification.objects.all() + if kwargs: + qs = qs.filter(**kwargs) + + qs.update(last_modified = timestamp) + + def testNoNotifications(self): + self.assertEquals(send_notifications(), []) + + def testNoReadyNotifications(self): + """ We shouldn't see immediate notifications""" + PatchChangeNotification(patch = self.patch, + orig_state = self.patch.state).save() + + errors = send_notifications() + self.assertEquals(errors, []) + self.assertEquals(len(mail.outbox), 0) + + def testNotifications(self): + PatchChangeNotification(patch = self.patch, + orig_state = self.patch.state).save() + self._expireNotifications() + + errors = send_notifications() + self.assertEquals(errors, []) + self.assertEquals(len(mail.outbox), 1) + msg = mail.outbox[0] + self.assertEquals(msg.to, [self.submitter.email]) + self.assertTrue(self.patch.get_absolute_url() in msg.body) + + def testNotificationEscaping(self): + self.patch.name = 'Patch name with " character' + self.patch.save() + PatchChangeNotification(patch = self.patch, + orig_state = self.patch.state).save() + self._expireNotifications() + + errors = send_notifications() + self.assertEquals(errors, []) + self.assertEquals(len(mail.outbox), 1) + msg = mail.outbox[0] + self.assertEquals(msg.to, [self.submitter.email]) + self.assertFalse('"' in msg.body) + + def testNotificationOptout(self): + """ensure opt-out addresses don't get notifications""" + PatchChangeNotification(patch = self.patch, + orig_state = self.patch.state).save() + self._expireNotifications() + + EmailOptout(email = self.submitter.email).save() + + errors = send_notifications() + self.assertEquals(errors, []) + self.assertEquals(len(mail.outbox), 0) + + def testNotificationMerge(self): + patches = [self.patch, + Patch(project = self.project, msgid = 'testpatch-2', + name = 'testpatch 2', content = '', + submitter = self.submitter)] + + for patch in patches: + patch.save() + PatchChangeNotification(patch = patch, + orig_state = patch.state).save() + + self.assertEquals(PatchChangeNotification.objects.count(), len(patches)) + self._expireNotifications() + errors = send_notifications() + self.assertEquals(errors, []) + self.assertEquals(len(mail.outbox), 1) + msg = mail.outbox[0] + self.assertTrue(patches[0].get_absolute_url() in msg.body) + self.assertTrue(patches[1].get_absolute_url() in msg.body) + + def testUnexpiredNotificationMerge(self): + """Test that when there are multiple pending notifications, with + at least one within the notification delay, that other notifications + are held""" + patches = [self.patch, + Patch(project = self.project, msgid = 'testpatch-2', + name = 'testpatch 2', content = '', + submitter = self.submitter)] + + for patch in patches: + patch.save() + PatchChangeNotification(patch = patch, + orig_state = patch.state).save() + + self.assertEquals(PatchChangeNotification.objects.count(), len(patches)) + self._expireNotifications() + + # update one notification, to bring it out of the notification delay + patches[0].state = State.objects.exclude(pk = patches[0].state.pk)[0] + patches[0].save() + + # the updated notification should prevent the other from being sent + errors = send_notifications() + self.assertEquals(errors, []) + self.assertEquals(len(mail.outbox), 0) + + # expire the updated notification + self._expireNotifications() + + errors = send_notifications() + self.assertEquals(errors, []) + self.assertEquals(len(mail.outbox), 1) + msg = mail.outbox[0] + self.assertTrue(patches[0].get_absolute_url() in msg.body) + self.assertTrue(patches[1].get_absolute_url() in msg.body) diff --git a/patchwork/tests/test_patchparser.py b/patchwork/tests/test_patchparser.py new file mode 100644 index 0000000..119936a --- /dev/null +++ b/patchwork/tests/test_patchparser.py @@ -0,0 +1,554 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os +from email import message_from_string +from django.test import TestCase +from patchwork.models import Project, Person, Patch, Comment, State, \ + get_default_initial_patch_state +from patchwork.tests.utils import read_patch, read_mail, create_email, \ + defaults, create_user + +try: + from email.mime.text import MIMEText +except ImportError: + # Python 2.4 compatibility + from email.MIMEText import MIMEText + +class PatchTest(TestCase): + default_sender = defaults.sender + default_subject = defaults.subject + project = defaults.project + +from patchwork.bin.parsemail import find_content, find_author, find_project, \ + parse_mail + +class InlinePatchTest(PatchTest): + patch_filename = '0001-add-line.patch' + test_comment = 'Test for attached patch' + + def setUp(self): + self.orig_patch = read_patch(self.patch_filename) + email = create_email(self.test_comment + '\n' + self.orig_patch) + (self.patch, self.comment) = find_content(self.project, email) + + def testPatchPresence(self): + self.assertTrue(self.patch is not None) + + def testPatchContent(self): + self.assertEquals(self.patch.content, self.orig_patch) + + def testCommentPresence(self): + self.assertTrue(self.comment is not None) + + def testCommentContent(self): + self.assertEquals(self.comment.content, self.test_comment) + + +class AttachmentPatchTest(InlinePatchTest): + patch_filename = '0001-add-line.patch' + test_comment = 'Test for attached patch' + content_subtype = 'x-patch' + + def setUp(self): + self.orig_patch = read_patch(self.patch_filename) + email = create_email(self.test_comment, multipart = True) + attachment = MIMEText(self.orig_patch, _subtype = self.content_subtype) + email.attach(attachment) + (self.patch, self.comment) = find_content(self.project, email) + +class AttachmentXDiffPatchTest(AttachmentPatchTest): + content_subtype = 'x-diff' + +class UTF8InlinePatchTest(InlinePatchTest): + patch_filename = '0002-utf-8.patch' + patch_encoding = 'utf-8' + + def setUp(self): + self.orig_patch = read_patch(self.patch_filename, self.patch_encoding) + email = create_email(self.test_comment + '\n' + self.orig_patch, + content_encoding = self.patch_encoding) + (self.patch, self.comment) = find_content(self.project, email) + +class NoCharsetInlinePatchTest(InlinePatchTest): + """ Test mails with no content-type or content-encoding header """ + patch_filename = '0001-add-line.patch' + + def setUp(self): + self.orig_patch = read_patch(self.patch_filename) + email = create_email(self.test_comment + '\n' + self.orig_patch) + del email['Content-Type'] + del email['Content-Transfer-Encoding'] + (self.patch, self.comment) = find_content(self.project, email) + +class SignatureCommentTest(InlinePatchTest): + patch_filename = '0001-add-line.patch' + test_comment = 'Test comment\nmore comment' + + def setUp(self): + self.orig_patch = read_patch(self.patch_filename) + email = create_email( \ + self.test_comment + '\n' + \ + '-- \nsig\n' + self.orig_patch) + (self.patch, self.comment) = find_content(self.project, email) + + +class ListFooterTest(InlinePatchTest): + patch_filename = '0001-add-line.patch' + test_comment = 'Test comment\nmore comment' + + def setUp(self): + self.orig_patch = read_patch(self.patch_filename) + email = create_email( \ + self.test_comment + '\n' + \ + '_______________________________________________\n' + \ + 'Linuxppc-dev mailing list\n' + \ + self.orig_patch) + (self.patch, self.comment) = find_content(self.project, email) + + +class DiffWordInCommentTest(InlinePatchTest): + test_comment = 'Lines can start with words beginning in "diff"\n' + \ + 'difficult\nDifferent' + + +class UpdateCommentTest(InlinePatchTest): + """ Test for '---\nUpdate: v2' style comments to patches. """ + patch_filename = '0001-add-line.patch' + test_comment = 'Test comment\nmore comment\n---\nUpdate: test update' + +class UpdateSigCommentTest(SignatureCommentTest): + """ Test for '---\nUpdate: v2' style comments to patches, with a sig """ + patch_filename = '0001-add-line.patch' + test_comment = 'Test comment\nmore comment\n---\nUpdate: test update' + +class SenderEncodingTest(TestCase): + sender_name = u'example user' + sender_email = 'user@example.com' + from_header = 'example user ' + + def setUp(self): + mail = 'From: %s\n' % self.from_header + \ + 'Subject: test\n\n' + \ + 'test' + self.email = message_from_string(mail) + (self.person, new) = find_author(self.email) + self.person.save() + + def tearDown(self): + self.person.delete() + + def testName(self): + self.assertEquals(self.person.name, self.sender_name) + + def testEmail(self): + self.assertEquals(self.person.email, self.sender_email) + + def testDBQueryName(self): + db_person = Person.objects.get(name = self.sender_name) + self.assertEquals(self.person, db_person) + + def testDBQueryEmail(self): + db_person = Person.objects.get(email = self.sender_email) + self.assertEquals(self.person, db_person) + + +class SenderUTF8QPEncodingTest(SenderEncodingTest): + sender_name = u'\xe9xample user' + from_header = '=?utf-8?q?=C3=A9xample=20user?= ' + +class SenderUTF8QPSplitEncodingTest(SenderEncodingTest): + sender_name = u'\xe9xample user' + from_header = '=?utf-8?q?=C3=A9xample?= user ' + +class SenderUTF8B64EncodingTest(SenderUTF8QPEncodingTest): + from_header = '=?utf-8?B?w6l4YW1wbGUgdXNlcg==?= ' + +class SubjectEncodingTest(PatchTest): + sender = 'example user ' + subject = 'test subject' + subject_header = 'test subject' + + def setUp(self): + mail = 'From: %s\n' % self.sender + \ + 'Subject: %s\n\n' % self.subject_header + \ + 'test\n\n' + defaults.patch + self.projects = defaults.project + self.email = message_from_string(mail) + + def testSubjectEncoding(self): + (patch, comment) = find_content(self.project, self.email) + self.assertEquals(patch.name, self.subject) + +class SubjectUTF8QPEncodingTest(SubjectEncodingTest): + subject = u'test s\xfcbject' + subject_header = '=?utf-8?q?test=20s=c3=bcbject?=' + +class SubjectUTF8QPMultipleEncodingTest(SubjectEncodingTest): + subject = u'test s\xfcbject' + subject_header = 'test =?utf-8?q?s=c3=bcbject?=' + +class SenderCorrelationTest(TestCase): + existing_sender = 'Existing Sender ' + non_existing_sender = 'Non-existing Sender ' + + def mail(self, sender): + return message_from_string('From: %s\nSubject: Test\n\ntest\n' % sender) + + def setUp(self): + self.existing_sender_mail = self.mail(self.existing_sender) + self.non_existing_sender_mail = self.mail(self.non_existing_sender) + (self.person, new) = find_author(self.existing_sender_mail) + self.person.save() + + def testExisingSender(self): + (person, new) = find_author(self.existing_sender_mail) + self.assertEqual(new, False) + self.assertEqual(person.id, self.person.id) + + def testNonExisingSender(self): + (person, new) = find_author(self.non_existing_sender_mail) + self.assertEqual(new, True) + self.assertEqual(person.id, None) + + def testExistingDifferentFormat(self): + mail = self.mail('existing@example.com') + (person, new) = find_author(mail) + self.assertEqual(new, False) + self.assertEqual(person.id, self.person.id) + + def testExistingDifferentCase(self): + mail = self.mail(self.existing_sender.upper()) + (person, new) = find_author(mail) + self.assertEqual(new, False) + self.assertEqual(person.id, self.person.id) + + def tearDown(self): + self.person.delete() + +class MultipleProjectPatchTest(TestCase): + """ Test that patches sent to multiple patchwork projects are + handled correctly """ + + test_comment = 'Test Comment' + patch_filename = '0001-add-line.patch' + msgid = '<1@example.com>' + + def setUp(self): + self.p1 = Project(linkname = 'test-project-1', name = 'Project 1', + listid = '1.example.com', listemail='1@example.com') + self.p2 = Project(linkname = 'test-project-2', name = 'Project 2', + listid = '2.example.com', listemail='2@example.com') + + self.p1.save() + self.p2.save() + + patch = read_patch(self.patch_filename) + email = create_email(self.test_comment + '\n' + patch) + email['Message-Id'] = self.msgid + + del email['List-ID'] + email['List-ID'] = '<' + self.p1.listid + '>' + parse_mail(email) + + del email['List-ID'] + email['List-ID'] = '<' + self.p2.listid + '>' + parse_mail(email) + + def testParsedProjects(self): + self.assertEquals(Patch.objects.filter(project = self.p1).count(), 1) + self.assertEquals(Patch.objects.filter(project = self.p2).count(), 1) + + def tearDown(self): + self.p1.delete() + self.p2.delete() + + +class MultipleProjectPatchCommentTest(MultipleProjectPatchTest): + """ Test that followups to multiple-project patches end up on the + correct patch """ + + comment_msgid = '<2@example.com>' + comment_content = 'test comment' + + def setUp(self): + super(MultipleProjectPatchCommentTest, self).setUp() + + for project in [self.p1, self.p2]: + email = MIMEText(self.comment_content) + email['From'] = defaults.sender + email['Subject'] = defaults.subject + email['Message-Id'] = self.comment_msgid + email['List-ID'] = '<' + project.listid + '>' + email['In-Reply-To'] = self.msgid + parse_mail(email) + + def testParsedComment(self): + for project in [self.p1, self.p2]: + patch = Patch.objects.filter(project = project)[0] + # we should see two comments now - the original mail with the patch, + # and the one we parsed in setUp() + self.assertEquals(Comment.objects.filter(patch = patch).count(), 2) + +class ListIdHeaderTest(TestCase): + """ Test that we parse List-Id headers from mails correctly """ + def setUp(self): + self.project = Project(linkname = 'test-project-1', name = 'Project 1', + listid = '1.example.com', listemail='1@example.com') + self.project.save() + + def testNoListId(self): + email = MIMEText('') + project = find_project(email) + self.assertEquals(project, None) + + def testBlankListId(self): + email = MIMEText('') + email['List-Id'] = '' + project = find_project(email) + self.assertEquals(project, None) + + def testWhitespaceListId(self): + email = MIMEText('') + email['List-Id'] = ' ' + project = find_project(email) + self.assertEquals(project, None) + + def testSubstringListId(self): + email = MIMEText('') + email['List-Id'] = 'example.com' + project = find_project(email) + self.assertEquals(project, None) + + def testShortListId(self): + """ Some mailing lists have List-Id headers in short formats, where it + is only the list ID itself (without enclosing angle-brackets). """ + email = MIMEText('') + email['List-Id'] = self.project.listid + project = find_project(email) + self.assertEquals(project, self.project) + + def testLongListId(self): + email = MIMEText('') + email['List-Id'] = 'Test text <%s>' % self.project.listid + project = find_project(email) + self.assertEquals(project, self.project) + + def tearDown(self): + self.project.delete() + +class MBoxPatchTest(PatchTest): + def setUp(self): + self.mail = read_mail(self.mail_file, project = self.project) + +class GitPullTest(MBoxPatchTest): + mail_file = '0001-git-pull-request.mbox' + + def testGitPullRequest(self): + (patch, comment) = find_content(self.project, self.mail) + self.assertTrue(patch is not None) + self.assertTrue(patch.pull_url is not None) + self.assertTrue(patch.content is None) + self.assertTrue(comment is not None) + +class GitPullWrappedTest(GitPullTest): + mail_file = '0002-git-pull-request-wrapped.mbox' + +class GitPullWithDiffTest(MBoxPatchTest): + mail_file = '0003-git-pull-request-with-diff.mbox' + + def testGitPullWithDiff(self): + (patch, comment) = find_content(self.project, self.mail) + self.assertTrue(patch is not None) + self.assertEqual('git://git.kernel.org/pub/scm/linux/kernel/git/tip/' + + 'linux-2.6-tip.git x86-fixes-for-linus', patch.pull_url) + self.assertTrue( + patch.content.startswith('diff --git a/arch/x86/include/asm/smp.h'), + patch.content) + self.assertTrue(comment is not None) + +class GitPullGitSSHUrlTest(GitPullTest): + mail_file = '0004-git-pull-request-git+ssh.mbox' + +class GitPullSSHUrlTest(GitPullTest): + mail_file = '0005-git-pull-request-ssh.mbox' + +class GitPullHTTPUrlTest(GitPullTest): + mail_file = '0006-git-pull-request-http.mbox' + +class GitRenameOnlyTest(MBoxPatchTest): + mail_file = '0008-git-rename.mbox' + + def testGitRename(self): + (patch, comment) = find_content(self.project, self.mail) + self.assertTrue(patch is not None) + self.assertTrue(comment is not None) + self.assertEqual(patch.content.count("\nrename from "), 2) + self.assertEqual(patch.content.count("\nrename to "), 2) + +class GitRenameWithDiffTest(MBoxPatchTest): + mail_file = '0009-git-rename-with-diff.mbox' + + def testGitRename(self): + (patch, comment) = find_content(self.project, self.mail) + self.assertTrue(patch is not None) + self.assertTrue(comment is not None) + self.assertEqual(patch.content.count("\nrename from "), 2) + self.assertEqual(patch.content.count("\nrename to "), 2) + self.assertEqual(patch.content.count('\n-a\n+b'), 1) + +class CVSFormatPatchTest(MBoxPatchTest): + mail_file = '0007-cvs-format-diff.mbox' + + def testPatch(self): + (patch, comment) = find_content(self.project, self.mail) + self.assertTrue(patch is not None) + self.assertTrue(comment is not None) + self.assertTrue(patch.content.startswith('Index')) + +class CharsetFallbackPatchTest(MBoxPatchTest): + """ Test mail with and invalid charset name, and check that we can parse + with one of the fallback encodings""" + + mail_file = '0010-invalid-charset.mbox' + + def testPatch(self): + (patch, comment) = find_content(self.project, self.mail) + self.assertTrue(patch is not None) + self.assertTrue(comment is not None) + +class NoNewlineAtEndOfFilePatchTest(MBoxPatchTest): + mail_file = '0011-no-newline-at-end-of-file.mbox' + + def testPatch(self): + (patch, comment) = find_content(self.project, self.mail) + self.assertTrue(patch is not None) + self.assertTrue(comment is not None) + self.assertTrue(patch.content.startswith('diff --git a/tools/testing/selftests/powerpc/Makefile')) + # Confirm the trailing no newline marker doesn't end up in the comment + self.assertFalse(comment.content.rstrip().endswith('\ No newline at end of file')) + # Confirm it's instead at the bottom of the patch + self.assertTrue(patch.content.rstrip().endswith('\ No newline at end of file')) + # Confirm we got both markers + self.assertEqual(2, patch.content.count('\ No newline at end of file')) + +class DelegateRequestTest(TestCase): + patch_filename = '0001-add-line.patch' + msgid = '<1@example.com>' + invalid_delegate_email = "nobody" + + def setUp(self): + self.patch = read_patch(self.patch_filename) + self.user = create_user() + self.p1 = Project(linkname = 'test-project-1', name = 'Project 1', + listid = '1.example.com', listemail='1@example.com') + self.p1.save() + + def get_email(self): + email = create_email(self.patch) + del email['List-ID'] + email['List-ID'] = '<' + self.p1.listid + '>' + email['Message-Id'] = self.msgid + return email + + def _assertDelegate(self, delegate): + query = Patch.objects.filter(project=self.p1) + self.assertEquals(query.count(), 1) + self.assertEquals(query[0].delegate, delegate) + + def testDelegate(self): + email = self.get_email() + email['X-Patchwork-Delegate'] = self.user.email + parse_mail(email) + self._assertDelegate(self.user) + + def testNoDelegate(self): + email = self.get_email() + parse_mail(email) + self._assertDelegate(None) + + def testInvalidDelegateFallsBackToNoDelegate(self): + email = self.get_email() + email['X-Patchwork-Delegate'] = self.invalid_delegate_email + parse_mail(email) + self._assertDelegate(None) + + def tearDown(self): + self.p1.delete() + self.user.delete() + +class InitialPatchStateTest(TestCase): + patch_filename = '0001-add-line.patch' + msgid = '<1@example.com>' + invalid_state_name = "Nonexistent Test State" + + def setUp(self): + self.patch = read_patch(self.patch_filename) + self.user = create_user() + self.p1 = Project(linkname = 'test-project-1', name = 'Project 1', + listid = '1.example.com', listemail='1@example.com') + self.p1.save() + self.default_state = get_default_initial_patch_state() + self.nondefault_state = State.objects.get(name="Accepted") + + def get_email(self): + email = create_email(self.patch) + del email['List-ID'] + email['List-ID'] = '<' + self.p1.listid + '>' + email['Message-Id'] = self.msgid + return email + + def _assertState(self, state): + query = Patch.objects.filter(project=self.p1) + self.assertEquals(query.count(), 1) + self.assertEquals(query[0].state, state) + + def testNonDefaultStateIsActuallyNotTheDefaultState(self): + self.assertNotEqual(self.default_state, self.nondefault_state) + + def testExplicitNonDefaultStateRequest(self): + email = self.get_email() + email['X-Patchwork-State'] = self.nondefault_state.name + parse_mail(email) + self._assertState(self.nondefault_state) + + def testExplicitDefaultStateRequest(self): + email = self.get_email() + email['X-Patchwork-State'] = self.default_state.name + parse_mail(email) + self._assertState(self.default_state) + + def testImplicitDefaultStateRequest(self): + email = self.get_email() + parse_mail(email) + self._assertState(self.default_state) + + def testInvalidTestStateDoesNotExist(self): + with self.assertRaises(State.DoesNotExist): + State.objects.get(name=self.invalid_state_name) + + def testInvalidStateRequestFallsBackToDefaultState(self): + email = self.get_email() + email['X-Patchwork-State'] = self.invalid_state_name + parse_mail(email) + self._assertState(self.default_state) + + def tearDown(self): + self.p1.delete() + self.user.delete() diff --git a/patchwork/tests/test_person.py b/patchwork/tests/test_person.py new file mode 100644 index 0000000..d948096 --- /dev/null +++ b/patchwork/tests/test_person.py @@ -0,0 +1,55 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2013 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import unittest +from django.test import TestCase +from django.test.client import Client +from patchwork.models import EmailConfirmation, Person, Bundle +import json + +class SubmitterCompletionTest(TestCase): + def setUp(self): + self.people = [ + Person(name = "Test Name", email = "test1@example.com"), + Person(email = "test2@example.com"), + ] + map(lambda p: p.save(), self.people) + + def testNameComplete(self): + response = self.client.get('/submitter/', {'q': 'name'}) + self.assertEquals(response.status_code, 200) + data = json.loads(response.content) + self.assertEquals(len(data), 1) + self.assertEquals(data[0]['fields']['name'], 'Test Name') + + def testEmailComplete(self): + response = self.client.get('/submitter/', {'q': 'test2'}) + self.assertEquals(response.status_code, 200) + data = json.loads(response.content) + self.assertEquals(len(data), 1) + self.assertEquals(data[0]['fields']['email'], 'test2@example.com') + + def testCompleteLimit(self): + for i in range(3,10): + person = Person(email = 'test%d@example.com' % i) + person.save() + response = self.client.get('/submitter/', {'q': 'test', 'l': 5}) + self.assertEquals(response.status_code, 200) + data = json.loads(response.content) + self.assertEquals(len(data), 5) diff --git a/patchwork/tests/test_registration.py b/patchwork/tests/test_registration.py new file mode 100644 index 0000000..845b60b --- /dev/null +++ b/patchwork/tests/test_registration.py @@ -0,0 +1,210 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2010 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import unittest +from django.test import TestCase +from django.test.client import Client +from django.core import mail +from django.core.urlresolvers import reverse +from django.contrib.auth.models import User +from patchwork.models import EmailConfirmation, Person +from patchwork.tests.utils import create_user + +def _confirmation_url(conf): + return reverse('patchwork.views.confirm', kwargs = {'key': conf.key}) + +class TestUser(object): + firstname = 'Test' + lastname = 'User' + username = 'testuser' + email = 'test@example.com' + password = 'foobar' + +class RegistrationTest(TestCase): + def setUp(self): + self.user = TestUser() + self.client = Client() + self.default_data = {'username': self.user.username, + 'first_name': self.user.firstname, + 'last_name': self.user.lastname, + 'email': self.user.email, + 'password': self.user.password} + self.required_error = 'This field is required.' + self.invalid_error = 'Enter a valid value.' + + def testRegistrationForm(self): + response = self.client.get('/register/') + self.assertEquals(response.status_code, 200) + self.assertTemplateUsed(response, 'patchwork/registration_form.html') + + def testBlankFields(self): + for field in ['username', 'email', 'password']: + data = self.default_data.copy() + del data[field] + response = self.client.post('/register/', data) + self.assertEquals(response.status_code, 200) + self.assertFormError(response, 'form', field, self.required_error) + + def testInvalidUsername(self): + data = self.default_data.copy() + data['username'] = 'invalid user' + response = self.client.post('/register/', data) + self.assertEquals(response.status_code, 200) + self.assertFormError(response, 'form', 'username', self.invalid_error) + + def testExistingUsername(self): + user = create_user() + data = self.default_data.copy() + data['username'] = user.username + response = self.client.post('/register/', data) + self.assertEquals(response.status_code, 200) + self.assertFormError(response, 'form', 'username', + 'This username is already taken. Please choose another.') + + def testExistingEmail(self): + user = create_user() + data = self.default_data.copy() + data['email'] = user.email + response = self.client.post('/register/', data) + self.assertEquals(response.status_code, 200) + self.assertFormError(response, 'form', 'email', + 'This email address is already in use ' + \ + 'for the account "%s".\n' % user.username) + + def testValidRegistration(self): + response = self.client.post('/register/', self.default_data) + self.assertEquals(response.status_code, 200) + self.assertContains(response, 'confirmation email has been sent') + + # check for presence of an inactive user object + users = User.objects.filter(username = self.user.username) + self.assertEquals(users.count(), 1) + user = users[0] + self.assertEquals(user.username, self.user.username) + self.assertEquals(user.email, self.user.email) + self.assertEquals(user.is_active, False) + + # check for confirmation object + confs = EmailConfirmation.objects.filter(user = user, + type = 'registration') + self.assertEquals(len(confs), 1) + conf = confs[0] + self.assertEquals(conf.email, self.user.email) + + # check for a sent mail + self.assertEquals(len(mail.outbox), 1) + msg = mail.outbox[0] + self.assertEquals(msg.subject, 'Patchwork account confirmation') + self.assertTrue(self.user.email in msg.to) + self.assertTrue(_confirmation_url(conf) in msg.body) + + # ...and that the URL is valid + response = self.client.get(_confirmation_url(conf)) + self.assertEquals(response.status_code, 200) + +class RegistrationConfirmationTest(TestCase): + + def setUp(self): + self.user = TestUser() + self.default_data = {'username': self.user.username, + 'first_name': self.user.firstname, + 'last_name': self.user.lastname, + 'email': self.user.email, + 'password': self.user.password} + + def testRegistrationConfirmation(self): + self.assertEqual(EmailConfirmation.objects.count(), 0) + response = self.client.post('/register/', self.default_data) + self.assertEquals(response.status_code, 200) + self.assertContains(response, 'confirmation email has been sent') + + self.assertEqual(EmailConfirmation.objects.count(), 1) + conf = EmailConfirmation.objects.filter()[0] + self.assertFalse(conf.user.is_active) + self.assertTrue(conf.active) + + response = self.client.get(_confirmation_url(conf)) + self.assertEquals(response.status_code, 200) + self.assertTemplateUsed(response, 'patchwork/registration-confirm.html') + + conf = EmailConfirmation.objects.get(pk = conf.pk) + self.assertTrue(conf.user.is_active) + self.assertFalse(conf.active) + + def testRegistrationNewPersonSetup(self): + """ Check that the person object created after registration has the + correct details """ + + # register + self.assertEqual(EmailConfirmation.objects.count(), 0) + response = self.client.post('/register/', self.default_data) + self.assertEquals(response.status_code, 200) + self.assertFalse(Person.objects.exists()) + + # confirm + conf = EmailConfirmation.objects.filter()[0] + response = self.client.get(_confirmation_url(conf)) + self.assertEquals(response.status_code, 200) + + qs = Person.objects.filter(email = self.user.email) + self.assertTrue(qs.exists()) + person = Person.objects.get(email = self.user.email) + + self.assertEquals(person.name, + self.user.firstname + ' ' + self.user.lastname) + + def testRegistrationExistingPersonSetup(self): + """ Check that the person object created after registration has the + correct details """ + + fullname = self.user.firstname + ' ' + self.user.lastname + person = Person(name = fullname, email = self.user.email) + person.save() + + # register + self.assertEqual(EmailConfirmation.objects.count(), 0) + response = self.client.post('/register/', self.default_data) + self.assertEquals(response.status_code, 200) + + # confirm + conf = EmailConfirmation.objects.filter()[0] + response = self.client.get(_confirmation_url(conf)) + self.assertEquals(response.status_code, 200) + + person = Person.objects.get(email = self.user.email) + + self.assertEquals(person.name, fullname) + + def testRegistrationExistingPersonUnmodified(self): + """ Check that an unconfirmed registration can't modify an existing + Person object""" + + fullname = self.user.firstname + ' ' + self.user.lastname + person = Person(name = fullname, email = self.user.email) + person.save() + + # register + data = self.default_data.copy() + data['first_name'] = 'invalid' + data['last_name'] = 'invalid' + self.assertEquals(data['email'], person.email) + response = self.client.post('/register/', data) + self.assertEquals(response.status_code, 200) + + self.assertEquals(Person.objects.get(pk = person.pk).name, fullname) diff --git a/patchwork/tests/test_updates.py b/patchwork/tests/test_updates.py new file mode 100644 index 0000000..177ee78 --- /dev/null +++ b/patchwork/tests/test_updates.py @@ -0,0 +1,118 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2010 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from django.test import TestCase +from django.core.urlresolvers import reverse +from patchwork.models import Patch, Person, State +from patchwork.tests.utils import defaults, create_maintainer + +class MultipleUpdateTest(TestCase): + def setUp(self): + defaults.project.save() + self.user = create_maintainer(defaults.project) + self.client.login(username = self.user.username, + password = self.user.username) + self.properties_form_id = 'patchform-properties' + self.url = reverse( + 'patchwork.views.patch.list', args = [defaults.project.linkname]) + self.base_data = { + 'action': 'Update', 'project': str(defaults.project.id), + 'form': 'patchlistform', 'archived': '*', 'delegate': '*', + 'state': '*'} + self.patches = [] + for name in ['patch one', 'patch two', 'patch three']: + patch = Patch(project = defaults.project, msgid = name, + name = name, content = '', + submitter = Person.objects.get(user = self.user)) + patch.save() + self.patches.append(patch) + + def _selectAllPatches(self, data): + for patch in self.patches: + data['patch_id:%d' % patch.id] = 'checked' + + def testArchivingPatches(self): + data = self.base_data.copy() + data.update({'archived': 'True'}) + self._selectAllPatches(data) + response = self.client.post(self.url, data) + self.assertContains(response, 'No patches to display', + status_code = 200) + for patch in [Patch.objects.get(pk = p.pk) for p in self.patches]: + self.assertTrue(patch.archived) + + def testUnArchivingPatches(self): + # Start with one patch archived and the remaining ones unarchived. + self.patches[0].archived = True + self.patches[0].save() + data = self.base_data.copy() + data.update({'archived': 'False'}) + self._selectAllPatches(data) + response = self.client.post(self.url, data) + self.assertContains(response, self.properties_form_id, + status_code = 200) + for patch in [Patch.objects.get(pk = p.pk) for p in self.patches]: + self.assertFalse(patch.archived) + + def _testStateChange(self, state): + data = self.base_data.copy() + data.update({'state': str(state)}) + self._selectAllPatches(data) + response = self.client.post(self.url, data) + self.assertContains(response, self.properties_form_id, + status_code = 200) + return response + + def testStateChangeValid(self): + states = [patch.state.pk for patch in self.patches] + state = State.objects.exclude(pk__in = states)[0] + self._testStateChange(state.pk) + for p in self.patches: + self.assertEquals(Patch.objects.get(pk = p.pk).state, state) + + def testStateChangeInvalid(self): + state = max(State.objects.all().values_list('id', flat = True)) + 1 + orig_states = [patch.state for patch in self.patches] + response = self._testStateChange(state) + self.assertEquals( \ + [Patch.objects.get(pk = p.pk).state for p in self.patches], + orig_states) + self.assertFormError(response, 'patchform', 'state', + 'Select a valid choice. That choice is not one ' + \ + 'of the available choices.') + + def _testDelegateChange(self, delegate_str): + data = self.base_data.copy() + data.update({'delegate': delegate_str}) + self._selectAllPatches(data) + response = self.client.post(self.url, data) + self.assertContains(response, self.properties_form_id, + status_code=200) + return response + + def testDelegateChangeValid(self): + delegate = create_maintainer(defaults.project) + response = self._testDelegateChange(str(delegate.pk)) + for p in self.patches: + self.assertEquals(Patch.objects.get(pk = p.pk).delegate, delegate) + + def testDelegateClear(self): + response = self._testDelegateChange('') + for p in self.patches: + self.assertEquals(Patch.objects.get(pk = p.pk).delegate, None) diff --git a/patchwork/tests/test_user.py b/patchwork/tests/test_user.py new file mode 100644 index 0000000..0faa970 --- /dev/null +++ b/patchwork/tests/test_user.py @@ -0,0 +1,195 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2010 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import unittest +from django.test import TestCase +from django.test.client import Client +from django.core import mail +from django.core.urlresolvers import reverse +from django.conf import settings +from django.contrib.auth.models import User +from patchwork.models import EmailConfirmation, Person, Bundle +from patchwork.tests.utils import defaults, error_strings + +def _confirmation_url(conf): + return reverse('patchwork.views.confirm', kwargs = {'key': conf.key}) + +class TestUser(object): + username = 'testuser' + email = 'test@example.com' + secondary_email = 'test2@example.com' + password = None + + def __init__(self): + self.password = User.objects.make_random_password() + self.user = User.objects.create_user(self.username, + self.email, self.password) + +class UserPersonRequestTest(TestCase): + def setUp(self): + self.user = TestUser() + self.client.login(username = self.user.username, + password = self.user.password) + EmailConfirmation.objects.all().delete() + + def testUserPersonRequestForm(self): + response = self.client.get('/user/link/') + self.assertEquals(response.status_code, 200) + self.assertTrue(response.context['linkform']) + + def testUserPersonRequestEmpty(self): + response = self.client.post('/user/link/', {'email': ''}) + self.assertEquals(response.status_code, 200) + self.assertTrue(response.context['linkform']) + self.assertFormError(response, 'linkform', 'email', + 'This field is required.') + + def testUserPersonRequestInvalid(self): + response = self.client.post('/user/link/', {'email': 'foo'}) + self.assertEquals(response.status_code, 200) + self.assertTrue(response.context['linkform']) + self.assertFormError(response, 'linkform', 'email', + error_strings['email']) + + def testUserPersonRequestValid(self): + response = self.client.post('/user/link/', + {'email': self.user.secondary_email}) + self.assertEquals(response.status_code, 200) + self.assertTrue(response.context['confirmation']) + + # check that we have a confirmation saved + self.assertEquals(EmailConfirmation.objects.count(), 1) + conf = EmailConfirmation.objects.all()[0] + self.assertEquals(conf.user, self.user.user) + self.assertEquals(conf.email, self.user.secondary_email) + self.assertEquals(conf.type, 'userperson') + + # check that an email has gone out... + self.assertEquals(len(mail.outbox), 1) + msg = mail.outbox[0] + self.assertEquals(msg.subject, 'Patchwork email address confirmation') + self.assertTrue(self.user.secondary_email in msg.to) + self.assertTrue(_confirmation_url(conf) in msg.body) + + # ...and that the URL is valid + response = self.client.get(_confirmation_url(conf)) + self.assertEquals(response.status_code, 200) + self.assertTemplateUsed(response, 'patchwork/user-link-confirm.html') + +class UserPersonConfirmTest(TestCase): + def setUp(self): + EmailConfirmation.objects.all().delete() + Person.objects.all().delete() + self.user = TestUser() + self.client.login(username = self.user.username, + password = self.user.password) + self.conf = EmailConfirmation(type = 'userperson', + email = self.user.secondary_email, + user = self.user.user) + self.conf.save() + + def testUserPersonConfirm(self): + self.assertEquals(Person.objects.count(), 0) + response = self.client.get(_confirmation_url(self.conf)) + self.assertEquals(response.status_code, 200) + + # check that the Person object has been created and linked + self.assertEquals(Person.objects.count(), 1) + person = Person.objects.get(email = self.user.secondary_email) + self.assertEquals(person.email, self.user.secondary_email) + self.assertEquals(person.user, self.user.user) + + # check that the confirmation has been marked as inactive. We + # need to reload the confirmation to check this. + conf = EmailConfirmation.objects.get(pk = self.conf.pk) + self.assertEquals(conf.active, False) + +class UserLoginRedirectTest(TestCase): + + def testUserLoginRedirect(self): + url = '/user/' + response = self.client.get(url) + self.assertRedirects(response, settings.LOGIN_URL + '?next=' + url) + +class UserProfileTest(TestCase): + + def setUp(self): + self.user = TestUser() + self.client.login(username = self.user.username, + password = self.user.password) + + def testUserProfile(self): + response = self.client.get('/user/') + self.assertContains(response, 'User Profile: %s' % self.user.username) + self.assertContains(response, 'User Profile: %s' % self.user.username) + + def testUserProfileNoBundles(self): + response = self.client.get('/user/') + self.assertContains(response, 'You have no bundles') + + def testUserProfileBundles(self): + project = defaults.project + project.save() + + bundle = Bundle(project = project, name = 'test-1', + owner = self.user.user) + bundle.save() + + response = self.client.get('/user/') + + self.assertContains(response, 'You have the following bundle') + self.assertContains(response, bundle.get_absolute_url()) + +class UserPasswordChangeTest(TestCase): + form_url = reverse('django.contrib.auth.views.password_change') + done_url = reverse('django.contrib.auth.views.password_change_done') + + def testPasswordChangeForm(self): + self.user = TestUser() + self.client.login(username = self.user.username, + password = self.user.password) + + response = self.client.get(self.form_url) + self.assertContains(response, 'Change my password') + + def testPasswordChange(self): + self.user = TestUser() + self.client.login(username = self.user.username, + password = self.user.password) + + old_password = self.user.password + new_password = User.objects.make_random_password() + + data = { + 'old_password': old_password, + 'new_password1': new_password, + 'new_password2': new_password, + } + + response = self.client.post(self.form_url, data) + self.assertRedirects(response, self.done_url) + + user = User.objects.get(id = self.user.user.id) + + self.assertFalse(user.check_password(old_password)) + self.assertTrue(user.check_password(new_password)) + + response = self.client.get(self.done_url) + self.assertContains(response, + "Your password has been changed sucessfully") diff --git a/patchwork/tests/test_xmlrpc.py b/patchwork/tests/test_xmlrpc.py new file mode 100644 index 0000000..2b459b2 --- /dev/null +++ b/patchwork/tests/test_xmlrpc.py @@ -0,0 +1,55 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2014 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import unittest +import xmlrpclib +from django.test import LiveServerTestCase +from django.core.urlresolvers import reverse +from django.conf import settings +from patchwork.models import Person, Patch +from patchwork.tests.utils import defaults + +@unittest.skipUnless(settings.ENABLE_XMLRPC, + "requires xmlrpc interface (use the ENABLE_XMLRPC setting)") +class XMLRPCTest(LiveServerTestCase): + + def setUp(self): + settings.STATIC_URL = '/' + self.url = (self.live_server_url + + reverse('patchwork.views.xmlrpc.xmlrpc')) + self.rpc = xmlrpclib.Server(self.url) + + def testGetRedirect(self): + response = self.client.get(self.url) + self.assertRedirects(response, + reverse('patchwork.views.help', + kwargs = {'path': 'pwclient/'})) + + def testList(self): + defaults.project.save() + defaults.patch_author_person.save() + patch = Patch(project = defaults.project, + submitter = defaults.patch_author_person, + msgid = defaults.patch_name, + content = defaults.patch) + patch.save() + + patches = self.rpc.patch_list() + self.assertEqual(len(patches), 1) + self.assertEqual(patches[0]['id'], patch.id) diff --git a/patchwork/tests/utils.py b/patchwork/tests/utils.py new file mode 100644 index 0000000..782ed36 --- /dev/null +++ b/patchwork/tests/utils.py @@ -0,0 +1,138 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os +import codecs +from patchwork.models import Project, Person +from django.contrib.auth.models import User +from django.forms.fields import EmailField + +from email import message_from_file +try: + from email.mime.text import MIMEText + from email.mime.multipart import MIMEMultipart +except ImportError: + # Python 2.4 compatibility + from email.MIMEText import MIMEText + from email.MIMEMultipart import MIMEMultipart + +# helper functions for tests +_test_mail_dir = os.path.join(os.path.dirname(__file__), 'mail') +_test_patch_dir = os.path.join(os.path.dirname(__file__), 'patches') + +class defaults(object): + project = Project(linkname = 'test-project', name = 'Test Project', + listid = 'test.example.com') + + patch_author = 'Patch Author ' + patch_author_person = Person(name = 'Patch Author', + email = 'patch-author@example.com') + + comment_author = 'Comment Author ' + + sender = 'Test Author ' + + subject = 'Test Subject' + + patch_name = 'Test Patch' + + patch = """--- /dev/null 2011-01-01 00:00:00.000000000 +0800 ++++ a 2011-01-01 00:00:00.000000000 +0800 +@@ -0,0 +1 @@ ++a +""" + +error_strings = { + 'email': 'Enter a valid email address.', +} + +_user_idx = 1 +def create_user(): + global _user_idx + userid = 'test%d' % _user_idx + email = '%s@example.com' % userid + _user_idx += 1 + + user = User.objects.create_user(userid, email, userid) + user.save() + + person = Person(email = email, name = userid, user = user) + person.save() + + return user + +def create_maintainer(project): + user = create_user() + profile = user.profile + profile.maintainer_projects.add(project) + profile.save() + return user + +def find_in_context(context, key): + if isinstance(context, list): + for c in context: + v = find_in_context(c, key) + if v is not None: + return v + else: + if key in context: + return context[key] + return None + +def read_patch(filename, encoding = None): + file_path = os.path.join(_test_patch_dir, filename) + if encoding is not None: + f = codecs.open(file_path, encoding = encoding) + else: + f = file(file_path) + + return f.read() + +def read_mail(filename, project = None): + file_path = os.path.join(_test_mail_dir, filename) + mail = message_from_file(open(file_path)) + if project is not None: + mail['List-Id'] = project.listid + return mail + +def create_email(content, subject = None, sender = None, multipart = False, + project = None, content_encoding = None): + if subject is None: + subject = defaults.subject + if sender is None: + sender = defaults.sender + if project is None: + project = defaults.project + if content_encoding is None: + content_encoding = 'us-ascii' + + if multipart: + msg = MIMEMultipart() + body = MIMEText(content, _subtype = 'plain', + _charset = content_encoding) + msg.attach(body) + else: + msg = MIMEText(content, _charset = content_encoding) + + msg['Subject'] = subject + msg['From'] = sender + msg['List-Id'] = project.listid + + + return msg diff --git a/patchwork/urls.py b/patchwork/urls.py new file mode 100644 index 0000000..b28eb90 --- /dev/null +++ b/patchwork/urls.py @@ -0,0 +1,103 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from django.conf.urls import patterns, url, include +from django.conf import settings +from django.contrib import admin +from django.contrib.auth import views as auth_views + +admin.autodiscover() + +urlpatterns = patterns('', + url(r'^admin/', include(admin.site.urls)), + + (r'^$', 'patchwork.views.projects'), + (r'^project/(?P[^/]+)/list/$', 'patchwork.views.patch.list'), + (r'^project/(?P[^/]+)/$', 'patchwork.views.project.project'), + + # patch views + (r'^patch/(?P\d+)/$', 'patchwork.views.patch.patch'), + (r'^patch/(?P\d+)/raw/$', 'patchwork.views.patch.content'), + (r'^patch/(?P\d+)/mbox/$', 'patchwork.views.patch.mbox'), + + # logged-in user stuff + (r'^user/$', 'patchwork.views.user.profile'), + (r'^user/todo/$', 'patchwork.views.user.todo_lists'), + (r'^user/todo/(?P[^/]+)/$', 'patchwork.views.user.todo_list'), + + (r'^user/bundles/$', + 'patchwork.views.bundle.bundles'), + + (r'^user/link/$', 'patchwork.views.user.link'), + (r'^user/unlink/(?P[^/]+)/$', 'patchwork.views.user.unlink'), + + # password change + url(r'^user/password-change/$', auth_views.password_change, + name='password_change'), + url(r'^user/password-change/done/$', auth_views.password_change_done, + name='password_change_done'), + + # login/logout + url(r'^user/login/$', auth_views.login, + {'template_name': 'patchwork/login.html'}, + name = 'auth_login'), + url(r'^user/logout/$', auth_views.logout, + {'template_name': 'patchwork/logout.html'}, + name = 'auth_logout'), + + # registration + (r'^register/', 'patchwork.views.user.register'), + + # public view for bundles + (r'^bundle/(?P[^/]*)/(?P[^/]*)/$', + 'patchwork.views.bundle.bundle'), + (r'^bundle/(?P[^/]*)/(?P[^/]*)/mbox/$', + 'patchwork.views.bundle.mbox'), + + (r'^confirm/(?P[0-9a-f]+)/$', 'patchwork.views.confirm'), + + # submitter autocomplete + (r'^submitter/$', 'patchwork.views.submitter_complete'), + + # email setup + (r'^mail/$', 'patchwork.views.mail.settings'), + (r'^mail/optout/$', 'patchwork.views.mail.optout'), + (r'^mail/optin/$', 'patchwork.views.mail.optin'), + + # help! + (r'^help/(?P.*)$', 'patchwork.views.help'), +) + +if settings.ENABLE_XMLRPC: + urlpatterns += patterns('', + (r'xmlrpc/$', 'patchwork.views.xmlrpc.xmlrpc'), + (r'^pwclient/$', 'patchwork.views.pwclient'), + (r'^project/(?P[^/]+)/pwclientrc/$', + 'patchwork.views.pwclientrc'), + ) + +# redirect from old urls +if settings.COMPAT_REDIR: + urlpatterns += patterns('', + (r'^user/bundle/(?P[^/]+)/$', + 'patchwork.views.bundle.bundle_redir'), + (r'^user/bundle/(?P[^/]+)/mbox/$', + 'patchwork.views.bundle.mbox_redir'), + ) + diff --git a/patchwork/utils.py b/patchwork/utils.py new file mode 100644 index 0000000..9ed9e41 --- /dev/null +++ b/patchwork/utils.py @@ -0,0 +1,248 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +import itertools +import datetime +from django.shortcuts import get_object_or_404 +from django.template.loader import render_to_string +from django.contrib.auth.models import User +from django.contrib.sites.models import Site +from django.conf import settings +from django.core.mail import EmailMessage +from django.db.models import Max, Q, F +from django.db.utils import IntegrityError +from patchwork.forms import MultiplePatchForm +from patchwork.models import Bundle, Project, BundlePatch, UserProfile, \ + PatchChangeNotification, EmailOptout, EmailConfirmation + +def get_patch_ids(d, prefix = 'patch_id'): + ids = [] + + for (k, v) in d.items(): + a = k.split(':') + if len(a) != 2: + continue + if a[0] != prefix: + continue + if not v: + continue + ids.append(a[1]) + + return ids + +class Order(object): + order_map = { + 'date': 'date', + 'name': 'name', + 'state': 'state__ordering', + 'submitter': 'submitter__name', + 'delegate': 'delegate__username', + } + default_order = ('date', True) + + def __init__(self, str = None, editable = False): + self.reversed = False + self.editable = editable + (self.order, self.reversed) = self.default_order + + if self.editable: + return + + if str is None or str == '': + return + + reversed = False + if str[0] == '-': + str = str[1:] + reversed = True + + if str not in self.order_map.keys(): + return + + self.order = str + self.reversed = reversed + + def __str__(self): + str = self.order + if self.reversed: + str = '-' + str + return str + + def name(self): + return self.order + + def reversed_name(self): + if self.reversed: + return self.order + else: + return '-' + self.order + + def apply(self, qs): + q = self.order_map[self.order] + if self.reversed: + q = '-' + q + + orders = [q] + + # if we're using a non-default order, add the default as a secondary + # ordering. We reverse the default if the primary is reversed. + (default_name, default_reverse) = self.default_order + if self.order != default_name: + q = self.order_map[default_name] + if self.reversed ^ default_reverse: + q = '-' + q + orders.append(q) + + return qs.order_by(*orders) + +bundle_actions = ['create', 'add', 'remove'] +def set_bundle(user, project, action, data, patches, context): + # set up the bundle + bundle = None + if action == 'create': + bundle_name = data['bundle_name'].strip() + if '/' in bundle_name: + return ['Bundle names can\'t contain slashes'] + + if not bundle_name: + return ['No bundle name was specified'] + + if Bundle.objects.filter(owner = user, name = bundle_name).count() > 0: + return ['You already have a bundle called "%s"' % bundle_name] + + bundle = Bundle(owner = user, project = project, + name = bundle_name) + bundle.save() + context.add_message("Bundle %s created" % bundle.name) + + elif action =='add': + bundle = get_object_or_404(Bundle, id = data['bundle_id']) + + elif action =='remove': + bundle = get_object_or_404(Bundle, id = data['removed_bundle_id']) + + if not bundle: + return ['no such bundle'] + + for patch in patches: + if action == 'create' or action == 'add': + bundlepatch_count = BundlePatch.objects.filter(bundle = bundle, + patch = patch).count() + if bundlepatch_count == 0: + bundle.append_patch(patch) + context.add_message("Patch '%s' added to bundle %s" % \ + (patch.name, bundle.name)) + else: + context.add_message("Patch '%s' already in bundle %s" % \ + (patch.name, bundle.name)) + + elif action == 'remove': + try: + bp = BundlePatch.objects.get(bundle = bundle, patch = patch) + bp.delete() + context.add_message("Patch '%s' removed from bundle %s\n" % \ + (patch.name, bundle.name)) + except Exception: + pass + + bundle.save() + + return [] + +def send_notifications(): + date_limit = datetime.datetime.now() - \ + datetime.timedelta(minutes = + settings.NOTIFICATION_DELAY_MINUTES) + + # This gets funky: we want to filter out any notifications that should + # be grouped with other notifications that aren't ready to go out yet. To + # do that, we join back onto PatchChangeNotification (PCN -> Patch -> + # Person -> Patch -> max(PCN.last_modified)), filtering out any maxima + # that are with the date_limit. + qs = PatchChangeNotification.objects \ + .annotate(m = Max('patch__submitter__patch__patchchangenotification' + '__last_modified')) \ + .filter(m__lt = date_limit) + + groups = itertools.groupby(qs.order_by('patch__submitter'), + lambda n: n.patch.submitter) + + errors = [] + + for (recipient, notifications) in groups: + notifications = list(notifications) + projects = set([ n.patch.project.linkname for n in notifications ]) + + def delete_notifications(): + pks = [ n.pk for n in notifications ] + PatchChangeNotification.objects.filter(pk__in = pks).delete() + + if EmailOptout.is_optout(recipient.email): + delete_notifications() + continue + + context = { + 'site': Site.objects.get_current(), + 'person': recipient, + 'notifications': notifications, + 'projects': projects, + } + + subject = render_to_string( + 'patchwork/patch-change-notification-subject.text', + context).strip() + content = render_to_string('patchwork/patch-change-notification.mail', + context) + + message = EmailMessage(subject = subject, body = content, + from_email = settings.NOTIFICATION_FROM_EMAIL, + to = [recipient.email], + headers = {'Precedence': 'bulk'}) + + try: + message.send() + except ex: + errors.append((recipient, ex)) + continue + + delete_notifications() + + return errors + +def do_expiry(): + # expire any pending confirmations + q = (Q(date__lt = datetime.datetime.now() - EmailConfirmation.validity) | + Q(active = False)) + EmailConfirmation.objects.filter(q).delete() + + # expire inactive users with no pending confirmation + pending_confs = EmailConfirmation.objects.values('user') + users = User.objects.filter( + is_active = False, + last_login = F('date_joined') + ).exclude( + id__in = pending_confs + ) + + # delete users + users.delete() + + + diff --git a/patchwork/views/__init__.py b/patchwork/views/__init__.py new file mode 100644 index 0000000..dfca56d --- /dev/null +++ b/patchwork/views/__init__.py @@ -0,0 +1,220 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +from base import * +from patchwork.utils import Order, get_patch_ids, bundle_actions, set_bundle +from patchwork.paginator import Paginator +from patchwork.forms import MultiplePatchForm +from patchwork.models import Comment +import re +import datetime + +try: + from email.mime.nonmultipart import MIMENonMultipart + from email.encoders import encode_7or8bit + from email.parser import HeaderParser + from email.header import Header + import email.utils +except ImportError: + # Python 2.4 compatibility + from email.MIMENonMultipart import MIMENonMultipart + from email.Encoders import encode_7or8bit + from email.Parser import HeaderParser + from email.Header import Header + import email.Utils + email.utils = email.Utils + +def generic_list(request, project, view, + view_args = {}, filter_settings = [], patches = None, + editable_order = False): + + context = PatchworkRequestContext(request, + list_view = view, + list_view_params = view_args) + + context.project = project + order = Order(request.REQUEST.get('order'), editable = editable_order) + + # Explicitly set data to None because request.POST will be an empty dict + # when the form is not submitted, but passing a non-None data argument to + # a forms.Form will make it bound and we don't want that to happen unless + # there's been a form submission. + data = None + if request.method == 'POST': + data = request.POST + user = request.user + properties_form = None + if project.is_editable(user): + + # we only pass the post data to the MultiplePatchForm if that was + # the actual form submitted + data_tmp = None + if data and data.get('form', '') == 'patchlistform': + data_tmp = data + + properties_form = MultiplePatchForm(project, data = data_tmp) + + if request.method == 'POST' and data.get('form') == 'patchlistform': + action = data.get('action', '').lower() + + # special case: the user may have hit enter in the 'create bundle' + # text field, so if non-empty, assume the create action: + if data.get('bundle_name', False): + action = 'create' + + ps = Patch.objects.filter(id__in = get_patch_ids(data)) + + if action in bundle_actions: + errors = set_bundle(user, project, action, data, ps, context) + + elif properties_form and action == properties_form.action: + errors = process_multiplepatch_form(properties_form, user, + action, ps, context) + else: + errors = [] + + if errors: + context['errors'] = errors + + for (filterclass, setting) in filter_settings: + if isinstance(setting, dict): + context.filters.set_status(filterclass, **setting) + elif isinstance(setting, list): + context.filters.set_status(filterclass, *setting) + else: + context.filters.set_status(filterclass, setting) + + if patches is None: + patches = Patch.objects.filter(project=project) + + patches = context.filters.apply(patches) + if not editable_order: + patches = order.apply(patches) + + # we don't need the content or headers for a list; they're text fields + # that can potentially contain a lot of data + patches = patches.defer('content', 'headers') + + # but we will need to follow the state and submitter relations for + # rendering the list template + patches = patches.select_related('state', 'submitter') + + paginator = Paginator(request, patches) + + context.update({ + 'page': paginator.current_page, + 'patchform': properties_form, + 'project': project, + 'order': order, + }) + + return context + + +def process_multiplepatch_form(form, user, action, patches, context): + errors = [] + if not form.is_valid() or action != form.action: + return ['The submitted form data was invalid'] + + if len(patches) == 0: + context.add_message("No patches selected; nothing updated") + return errors + + changed_patches = 0 + for patch in patches: + if not patch.is_editable(user): + errors.append("You don't have permissions to edit patch '%s'" + % patch.name) + continue + + changed_patches += 1 + form.save(patch) + + if changed_patches == 1: + context.add_message("1 patch updated") + elif changed_patches > 1: + context.add_message("%d patches updated" % changed_patches) + else: + context.add_message("No patches updated") + + return errors + +class PatchMbox(MIMENonMultipart): + patch_charset = 'utf-8' + def __init__(self, _text): + MIMENonMultipart.__init__(self, 'text', 'plain', + **{'charset': self.patch_charset}) + self.set_payload(_text.encode(self.patch_charset)) + encode_7or8bit(self) + +def patch_to_mbox(patch): + postscript_re = re.compile('\n-{2,3} ?\n') + + comment = None + try: + comment = Comment.objects.get(patch = patch, msgid = patch.msgid) + except Exception: + pass + + body = '' + if comment: + body = comment.content.strip() + "\n" + + parts = postscript_re.split(body, 1) + if len(parts) == 2: + (body, postscript) = parts + body = body.strip() + "\n" + postscript = postscript.rstrip() + else: + postscript = '' + + for comment in Comment.objects.filter(patch = patch) \ + .exclude(msgid = patch.msgid): + body += comment.patch_responses() + + if postscript: + body += '---\n' + postscript + '\n' + + if patch.content: + body += '\n' + patch.content + + delta = patch.date - datetime.datetime.utcfromtimestamp(0) + utc_timestamp = delta.seconds + delta.days*24*3600 + + mail = PatchMbox(body) + mail['Subject'] = patch.name + mail['From'] = email.utils.formataddr(( + str(Header(patch.submitter.name, mail.patch_charset)), + patch.submitter.email)) + mail['X-Patchwork-Id'] = str(patch.id) + mail['Message-Id'] = patch.msgid + mail.set_unixfrom('From patchwork ' + patch.date.ctime()) + + + copied_headers = ['To', 'Cc', 'Date'] + orig_headers = HeaderParser().parsestr(str(patch.headers)) + for header in copied_headers: + if header in orig_headers: + mail[header] = orig_headers[header] + + if 'Date' not in mail: + mail['Date'] = email.utils.formatdate(utc_timestamp) + + return mail diff --git a/patchwork/views/base.py b/patchwork/views/base.py new file mode 100644 index 0000000..6d7dd13 --- /dev/null +++ b/patchwork/views/base.py @@ -0,0 +1,122 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +from patchwork.models import Patch, Project, Person, EmailConfirmation +from django.shortcuts import render_to_response, get_object_or_404 +from django.http import HttpResponse, HttpResponseRedirect, Http404 +from patchwork.requestcontext import PatchworkRequestContext +from django.core import serializers, urlresolvers +from django.template.loader import render_to_string +from django.conf import settings +from django.db.models import Q + +def projects(request): + context = PatchworkRequestContext(request) + projects = Project.objects.all() + + if projects.count() == 1: + return HttpResponseRedirect( + urlresolvers.reverse('patchwork.views.patch.list', + kwargs = {'project_id': projects[0].linkname})) + + context['projects'] = projects + return render_to_response('patchwork/projects.html', context) + +def pwclientrc(request, project_id): + project = get_object_or_404(Project, linkname = project_id) + context = PatchworkRequestContext(request) + context.project = project + if settings.FORCE_HTTPS_LINKS or request.is_secure(): + context['scheme'] = 'https' + else: + context['scheme'] = 'http' + response = HttpResponse(content_type = "text/plain") + response['Content-Disposition'] = 'attachment; filename=.pwclientrc' + response.write(render_to_string('patchwork/pwclientrc', context)) + return response + +def pwclient(request): + context = PatchworkRequestContext(request) + response = HttpResponse(content_type = "text/x-python") + response['Content-Disposition'] = 'attachment; filename=pwclient' + response.write(render_to_string('patchwork/pwclient', context)) + return response + +def confirm(request, key): + import patchwork.views.user, patchwork.views.mail + views = { + 'userperson': patchwork.views.user.link_confirm, + 'registration': patchwork.views.user.register_confirm, + 'optout': patchwork.views.mail.optout_confirm, + 'optin': patchwork.views.mail.optin_confirm, + } + + conf = get_object_or_404(EmailConfirmation, key = key) + if conf.type not in views: + raise Http404 + + if conf.active and conf.is_valid(): + return views[conf.type](request, conf) + + context = PatchworkRequestContext(request) + context['conf'] = conf + if not conf.active: + context['error'] = 'inactive' + elif not conf.is_valid(): + context['error'] = 'expired' + + return render_to_response('patchwork/confirm-error.html', context) + +def submitter_complete(request): + search = request.GET.get('q', '') + limit = request.GET.get('l', None) + response = HttpResponse(content_type = "text/plain") + + if len(search) <= 3: + return response + + queryset = Person.objects.filter(Q(name__icontains = search) | + Q(email__icontains = search)) + if limit is not None: + try: + limit = int(limit) + except ValueError: + limit = None + + if limit is not None and limit > 0: + queryset = queryset[:limit] + + json_serializer = serializers.get_serializer("json")() + json_serializer.serialize(queryset, ensure_ascii=False, stream=response) + return response + +help_pages = {'': 'index.html', + 'about/': 'about.html', + } + +if settings.ENABLE_XMLRPC: + help_pages['pwclient/'] = 'pwclient.html' + +def help(request, path): + context = PatchworkRequestContext(request) + if path in help_pages: + return render_to_response('patchwork/help/' + help_pages[path], context) + raise Http404 + diff --git a/patchwork/views/bundle.py b/patchwork/views/bundle.py new file mode 100644 index 0000000..3fb47e2 --- /dev/null +++ b/patchwork/views/bundle.py @@ -0,0 +1,221 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User +from django.shortcuts import render_to_response, get_object_or_404 +from patchwork.requestcontext import PatchworkRequestContext +from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound +import django.core.urlresolvers +from patchwork.models import Patch, Bundle, BundlePatch, Project +from patchwork.utils import get_patch_ids +from patchwork.forms import BundleForm, DeleteBundleForm +from patchwork.views import generic_list, patch_to_mbox +from patchwork.filters import DelegateFilter + +@login_required +def setbundle(request): + context = PatchworkRequestContext(request) + + bundle = None + + if request.method == 'POST': + action = request.POST.get('action', None) + if action is None: + pass + elif action == 'create': + project = get_object_or_404(Project, + id = request.POST.get('project')) + bundle = Bundle(owner = request.user, project = project, + name = request.POST['name']) + bundle.save() + patch_id = request.POST.get('patch_id', None) + if patch_id: + patch = get_object_or_404(Patch, id = patch_id) + try: + bundle.append_patch(patch) + except Exception: + pass + bundle.save() + elif action == 'add': + bundle = get_object_or_404(Bundle, + owner = request.user, id = request.POST['id']) + bundle.save() + + patch_id = request.get('patch_id', None) + if patch_id: + patch_ids = patch_id + else: + patch_ids = get_patch_ids(request.POST) + + for id in patch_ids: + try: + patch = Patch.objects.get(id = id) + bundle.append_patch(patch) + except: + pass + + bundle.save() + elif action == 'delete': + try: + bundle = Bundle.objects.get(owner = request.user, + id = request.POST['id']) + bundle.delete() + except Exception: + pass + + bundle = None + + else: + bundle = get_object_or_404(Bundle, owner = request.user, + id = request.POST['bundle_id']) + + if 'error' in context: + pass + + if bundle: + return HttpResponseRedirect( + django.core.urlresolvers.reverse( + 'patchwork.views.bundle.bundle', + kwargs = {'bundle_id': bundle.id} + ) + ) + else: + return HttpResponseRedirect( + django.core.urlresolvers.reverse( + 'patchwork.views.bundle.list') + ) + +@login_required +def bundles(request): + context = PatchworkRequestContext(request) + + if request.method == 'POST': + form_name = request.POST.get('form_name', '') + + if form_name == DeleteBundleForm.name: + form = DeleteBundleForm(request.POST) + if form.is_valid(): + bundle = get_object_or_404(Bundle, + id = form.cleaned_data['bundle_id']) + bundle.delete() + + bundles = Bundle.objects.filter(owner = request.user) + for bundle in bundles: + bundle.delete_form = DeleteBundleForm(auto_id = False, + initial = {'bundle_id': bundle.id}) + + context['bundles'] = bundles + + return render_to_response('patchwork/bundles.html', context) + +def bundle(request, username, bundlename): + bundle = get_object_or_404(Bundle, owner__username = username, + name = bundlename) + filter_settings = [(DelegateFilter, DelegateFilter.AnyDelegate)] + + is_owner = request.user == bundle.owner + + if not (is_owner or bundle.public): + return HttpResponseNotFound() + + if is_owner: + if request.method == 'POST' and request.POST.get('form') == 'bundle': + action = request.POST.get('action', '').lower() + if action == 'delete': + bundle.delete() + return HttpResponseRedirect( + django.core.urlresolvers.reverse( + 'patchwork.views.user.profile') + ) + elif action == 'update': + form = BundleForm(request.POST, instance = bundle) + if form.is_valid(): + form.save() + + # if we've changed the bundle name, redirect to new URL + bundle = Bundle.objects.get(pk = bundle.pk) + if bundle.name != bundlename: + return HttpResponseRedirect(bundle.get_absolute_url()) + + else: + form = BundleForm(instance = bundle) + else: + form = BundleForm(instance = bundle) + + if request.method == 'POST' and \ + request.POST.get('form') == 'reorderform': + order = get_object_or_404(BundlePatch, bundle = bundle, + patch__id = request.POST.get('order_start')).order + + for patch_id in request.POST.getlist('neworder'): + bundlepatch = get_object_or_404(BundlePatch, + bundle = bundle, patch__id = patch_id) + bundlepatch.order = order + bundlepatch.save() + order += 1 + else: + form = None + + context = generic_list(request, bundle.project, + 'patchwork.views.bundle.bundle', + view_args = {'username': bundle.owner.username, + 'bundlename': bundle.name}, + filter_settings = filter_settings, + patches = bundle.ordered_patches(), + editable_order = is_owner) + + context['bundle'] = bundle + context['bundleform'] = form + + return render_to_response('patchwork/bundle.html', context) + +def mbox(request, username, bundlename): + bundle = get_object_or_404(Bundle, owner__username = username, + name = bundlename) + + if not (request.user == bundle.owner or bundle.public): + return HttpResponseNotFound() + + mbox = '\n'.join([patch_to_mbox(p).as_string(True) + for p in bundle.ordered_patches()]) + + response = HttpResponse(content_type='text/plain') + response['Content-Disposition'] = \ + 'attachment; filename=bundle-%d-%s.mbox' % (bundle.id, bundle.name) + + response.write(mbox) + return response + +@login_required +def bundle_redir(request, bundle_id): + bundle = get_object_or_404(Bundle, id = bundle_id, owner = request.user) + return HttpResponseRedirect(bundle.get_absolute_url()) + +@login_required +def mbox_redir(request, bundle_id): + bundle = get_object_or_404(Bundle, id = bundle_id, owner = request.user) + return HttpResponseRedirect(django.core.urlresolvers.reverse( + 'patchwork.views.bundle.mbox', kwargs = { + 'username': request.user.username, + 'bundlename': bundle.name, + })) + + + diff --git a/patchwork/views/mail.py b/patchwork/views/mail.py new file mode 100644 index 0000000..aebba34 --- /dev/null +++ b/patchwork/views/mail.py @@ -0,0 +1,119 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2010 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from patchwork.requestcontext import PatchworkRequestContext +from patchwork.models import EmailOptout, EmailConfirmation +from patchwork.forms import OptinoutRequestForm, EmailForm +from django.shortcuts import render_to_response +from django.template.loader import render_to_string +from django.conf import settings as conf_settings +from django.core.mail import send_mail +from django.core.urlresolvers import reverse +from django.http import HttpResponseRedirect + +def settings(request): + context = PatchworkRequestContext(request) + if request.method == 'POST': + form = EmailForm(data = request.POST) + if form.is_valid(): + email = form.cleaned_data['email'] + is_optout = EmailOptout.objects.filter(email = email).count() > 0 + context.update({ + 'email': email, + 'is_optout': is_optout, + }) + return render_to_response('patchwork/mail-settings.html', context) + + else: + form = EmailForm() + context['form'] = form + return render_to_response('patchwork/mail-form.html', context) + +def optout_confirm(request, conf): + context = PatchworkRequestContext(request) + + email = conf.email.strip().lower() + # silently ignore duplicated optouts + if EmailOptout.objects.filter(email = email).count() == 0: + optout = EmailOptout(email = email) + optout.save() + + conf.deactivate() + context['email'] = conf.email + + return render_to_response('patchwork/optout.html', context) + +def optin_confirm(request, conf): + context = PatchworkRequestContext(request) + + email = conf.email.strip().lower() + EmailOptout.objects.filter(email = email).delete() + + conf.deactivate() + context['email'] = conf.email + + return render_to_response('patchwork/optin.html', context) + +def optinout(request, action, description): + context = PatchworkRequestContext(request) + + mail_template = 'patchwork/%s-request.mail' % action + html_template = 'patchwork/%s-request.html' % action + + if request.method != 'POST': + return HttpResponseRedirect(reverse(settings)) + + form = OptinoutRequestForm(data = request.POST) + if not form.is_valid(): + context['error'] = ('There was an error in the %s form. ' + + 'Please review the form and re-submit.') % \ + description + context['form'] = form + return render_to_response(html_template, context) + + email = form.cleaned_data['email'] + if action == 'optin' and \ + EmailOptout.objects.filter(email = email).count() == 0: + context['error'] = ('The email address %s is not on the ' + + 'patchwork opt-out list, so you don\'t ' + + 'need to opt back in') % email + context['form'] = form + return render_to_response(html_template, context) + + conf = EmailConfirmation(type = action, email = email) + conf.save() + context['confirmation'] = conf + mail = render_to_string(mail_template, context) + try: + send_mail('Patchwork %s confirmation' % description, mail, + conf_settings.DEFAULT_FROM_EMAIL, [email]) + context['email'] = mail + context['email_sent'] = True + except Exception, ex: + context['error'] = 'An error occurred during confirmation . ' + \ + 'Please try again later.' + context['admins'] = conf_settings.ADMINS + + return render_to_response(html_template, context) + +def optout(request): + return optinout(request, 'optout', 'opt-out') + +def optin(request): + return optinout(request, 'optin', 'opt-in') diff --git a/patchwork/views/patch.py b/patchwork/views/patch.py new file mode 100644 index 0000000..62ff853 --- /dev/null +++ b/patchwork/views/patch.py @@ -0,0 +1,107 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +from patchwork.models import Patch, Project, Bundle +from patchwork.forms import PatchForm, CreateBundleForm +from patchwork.requestcontext import PatchworkRequestContext +from django.shortcuts import render_to_response, get_object_or_404 +from django.http import HttpResponse, HttpResponseForbidden +from patchwork.views import generic_list, patch_to_mbox + +def patch(request, patch_id): + context = PatchworkRequestContext(request) + patch = get_object_or_404(Patch, id=patch_id) + context.project = patch.project + editable = patch.is_editable(request.user) + + form = None + createbundleform = None + + if editable: + form = PatchForm(instance = patch) + if request.user.is_authenticated(): + createbundleform = CreateBundleForm() + + if request.method == 'POST': + action = request.POST.get('action', None) + if action: + action = action.lower() + + if action == 'createbundle': + bundle = Bundle(owner = request.user, project = patch.project) + createbundleform = CreateBundleForm(instance = bundle, + data = request.POST) + if createbundleform.is_valid(): + createbundleform.save() + bundle.append_patch(patch) + bundle.save() + createbundleform = CreateBundleForm() + context.add_message('Bundle %s created' % bundle.name) + + elif action == 'addtobundle': + bundle = get_object_or_404(Bundle, id = \ + request.POST.get('bundle_id')) + try: + bundle.append_patch(patch) + bundle.save() + context.add_message('Patch added to bundle "%s"' % bundle.name) + except Exception, ex: + context.add_message("Couldn't add patch '%s' to bundle %s: %s" \ + % (patch.name, bundle.name, ex.message)) + + # all other actions require edit privs + elif not editable: + return HttpResponseForbidden() + + elif action is None: + form = PatchForm(data = request.POST, instance = patch) + if form.is_valid(): + form.save() + context.add_message('Patch updated') + + context['patch'] = patch + context['patchform'] = form + context['createbundleform'] = createbundleform + context['project'] = patch.project + + return render_to_response('patchwork/patch.html', context) + +def content(request, patch_id): + patch = get_object_or_404(Patch, id=patch_id) + response = HttpResponse(content_type="text/x-patch") + response.write(patch.content) + response['Content-Disposition'] = 'attachment; filename=' + \ + patch.filename().replace(';', '').replace('\n', '') + return response + +def mbox(request, patch_id): + patch = get_object_or_404(Patch, id=patch_id) + response = HttpResponse(content_type="text/plain") + response.write(patch_to_mbox(patch).as_string(True)) + response['Content-Disposition'] = 'attachment; filename=' + \ + patch.filename().replace(';', '').replace('\n', '') + return response + + +def list(request, project_id): + project = get_object_or_404(Project, linkname=project_id) + context = generic_list(request, project, 'patchwork.views.patch.list', + view_args = {'project_id': project.linkname}) + return render_to_response('patchwork/list.html', context) diff --git a/patchwork/views/project.py b/patchwork/views/project.py new file mode 100644 index 0000000..114dbe0 --- /dev/null +++ b/patchwork/views/project.py @@ -0,0 +1,38 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2009 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +from patchwork.models import Patch, Project +from django.shortcuts import render_to_response, get_object_or_404 +from django.contrib.auth.models import User +from patchwork.requestcontext import PatchworkRequestContext + +def project(request, project_id): + context = PatchworkRequestContext(request) + project = get_object_or_404(Project, linkname = project_id) + context.project = project + + context['maintainers'] = User.objects.filter( \ + profile__maintainer_projects = project) + context['n_patches'] = Patch.objects.filter(project = project, + archived = False).count() + context['n_archived_patches'] = Patch.objects.filter(project = project, + archived = True).count() + + return render_to_response('patchwork/project.html', context) diff --git a/patchwork/views/user.py b/patchwork/views/user.py new file mode 100644 index 0000000..126ecc9 --- /dev/null +++ b/patchwork/views/user.py @@ -0,0 +1,216 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +from django.contrib.auth.decorators import login_required +from patchwork.requestcontext import PatchworkRequestContext +from django.shortcuts import render_to_response, get_object_or_404 +from django.contrib import auth +from django.contrib.sites.models import Site +from django.http import HttpResponseRedirect +from patchwork.models import Project, Bundle, Person, EmailConfirmation, \ + State, EmailOptout +from patchwork.forms import UserProfileForm, UserPersonLinkForm, \ + RegistrationForm +from patchwork.filters import DelegateFilter +from patchwork.views import generic_list +from django.template.loader import render_to_string +from django.conf import settings +from django.core.mail import send_mail +import django.core.urlresolvers + +def register(request): + context = PatchworkRequestContext(request) + if request.method == 'POST': + form = RegistrationForm(request.POST) + if form.is_valid(): + data = form.cleaned_data + # create inactive user + user = auth.models.User.objects.create_user(data['username'], + data['email'], + data['password']) + user.is_active = False; + user.first_name = data.get('first_name', '') + user.last_name = data.get('last_name', '') + user.save() + + # create confirmation + conf = EmailConfirmation(type = 'registration', user = user, + email = user.email) + conf.save() + + # send email + mail_ctx = {'site': Site.objects.get_current(), + 'confirmation': conf} + + subject = render_to_string('patchwork/activation_email_subject.txt', + mail_ctx).replace('\n', ' ').strip() + + message = render_to_string('patchwork/activation_email.txt', + mail_ctx) + + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, + [conf.email]) + + # setting 'confirmation' in the template indicates success + context['confirmation'] = conf + + else: + form = RegistrationForm() + + return render_to_response('patchwork/registration_form.html', + { 'form': form }, + context_instance=context) + +def register_confirm(request, conf): + conf.user.is_active = True + conf.user.save() + conf.deactivate() + try: + person = Person.objects.get(email__iexact = conf.user.email) + except Person.DoesNotExist: + person = Person(email = conf.user.email, + name = conf.user.profile.name()) + person.user = conf.user + person.save() + + return render_to_response('patchwork/registration-confirm.html') + +@login_required +def profile(request): + context = PatchworkRequestContext(request) + + if request.method == 'POST': + form = UserProfileForm(instance = request.user.profile, + data = request.POST) + if form.is_valid(): + form.save() + else: + form = UserProfileForm(instance = request.user.profile) + + context.project = request.user.profile.primary_project + context['bundles'] = Bundle.objects.filter(owner = request.user) + context['profileform'] = form + + optout_query = '%s.%s IN (SELECT %s FROM %s)' % ( + Person._meta.db_table, + Person._meta.get_field('email').column, + EmailOptout._meta.get_field('email').column, + EmailOptout._meta.db_table) + people = Person.objects.filter(user = request.user) \ + .extra(select = {'is_optout': optout_query}) + context['linked_emails'] = people + context['linkform'] = UserPersonLinkForm() + + return render_to_response('patchwork/profile.html', context) + +@login_required +def link(request): + context = PatchworkRequestContext(request) + + if request.method == 'POST': + form = UserPersonLinkForm(request.POST) + if form.is_valid(): + conf = EmailConfirmation(type = 'userperson', + user = request.user, + email = form.cleaned_data['email']) + conf.save() + context['confirmation'] = conf + + try: + send_mail('Patchwork email address confirmation', + render_to_string('patchwork/user-link.mail', + context), + settings.DEFAULT_FROM_EMAIL, + [form.cleaned_data['email']]) + except Exception: + context['confirmation'] = None + context['error'] = 'An error occurred during confirmation. ' + \ + 'Please try again later' + else: + form = UserPersonLinkForm() + context['linkform'] = form + + return render_to_response('patchwork/user-link.html', context) + +@login_required +def link_confirm(request, conf): + context = PatchworkRequestContext(request) + + try: + person = Person.objects.get(email__iexact = conf.email) + except Person.DoesNotExist: + person = Person(email = conf.email) + + person.link_to_user(conf.user) + person.save() + conf.deactivate() + + context['person'] = person + + return render_to_response('patchwork/user-link-confirm.html', context) + +@login_required +def unlink(request, person_id): + person = get_object_or_404(Person, id = person_id) + + if request.method == 'POST': + if person.email != request.user.email: + person.user = None + person.save() + + url = django.core.urlresolvers.reverse('patchwork.views.user.profile') + return HttpResponseRedirect(url) + + +@login_required +def todo_lists(request): + todo_lists = [] + + for project in Project.objects.all(): + patches = request.user.profile.todo_patches(project = project) + if not patches.count(): + continue + + todo_lists.append({'project': project, 'n_patches': patches.count()}) + + if len(todo_lists) == 1: + return todo_list(request, todo_lists[0]['project'].linkname) + + context = PatchworkRequestContext(request) + context['todo_lists'] = todo_lists + context.project = request.user.profile.primary_project + return render_to_response('patchwork/todo-lists.html', context) + +@login_required +def todo_list(request, project_id): + project = get_object_or_404(Project, linkname = project_id) + patches = request.user.profile.todo_patches(project = project) + filter_settings = [(DelegateFilter, + {'delegate': request.user})] + + context = generic_list(request, project, + 'patchwork.views.user.todo_list', + view_args = {'project_id': project.linkname}, + filter_settings = filter_settings, + patches = patches) + + context['action_required_states'] = \ + State.objects.filter(action_required = True).all() + return render_to_response('patchwork/todo-list.html', context) diff --git a/patchwork/views/xmlrpc.py b/patchwork/views/xmlrpc.py new file mode 100644 index 0000000..84ed408 --- /dev/null +++ b/patchwork/views/xmlrpc.py @@ -0,0 +1,450 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Patchwork XMLRPC interface +# + +from SimpleXMLRPCServer import SimpleXMLRPCDispatcher +from django.http import HttpResponse, HttpResponseRedirect, \ + HttpResponseServerError +from django.core import urlresolvers +from django.contrib.auth import authenticate +from patchwork.models import Patch, Project, Person, State +from patchwork.views import patch_to_mbox +from django.views.decorators.csrf import csrf_exempt + +import sys +import base64 +import xmlrpclib + +class PatchworkXMLRPCDispatcher(SimpleXMLRPCDispatcher): + def __init__(self): + if sys.version_info[:3] >= (2,5,): + SimpleXMLRPCDispatcher.__init__(self, allow_none=False, + encoding=None) + def _dumps(obj, *args, **kwargs): + kwargs['allow_none'] = self.allow_none + kwargs['encoding'] = self.encoding + return xmlrpclib.dumps(obj, *args, **kwargs) + else: + def _dumps(obj, *args, **kwargs): + return xmlrpclib.dumps(obj, *args, **kwargs) + SimpleXMLRPCDispatcher.__init__(self) + + self.dumps = _dumps + + # map of name => (auth, func) + self.func_map = {} + + def register_function(self, fn, auth_required): + self.func_map[fn.__name__] = (auth_required, fn) + + + def _user_for_request(self, request): + auth_header = None + + if 'HTTP_AUTHORIZATION' in request.META: + auth_header = request.META.get('HTTP_AUTHORIZATION') + elif 'Authorization' in request.META: + auth_header = request.META.get('Authorization') + + if auth_header is None or auth_header == '': + raise Exception("No authentication credentials given") + + str = auth_header.strip() + + if not str.startswith('Basic '): + raise Exception("Authentication scheme not supported") + + str = str[len('Basic '):].strip() + + try: + decoded = base64.decodestring(str) + username, password = decoded.split(':', 1) + except: + raise Exception("Invalid authentication credentials") + + return authenticate(username = username, password = password) + + + def _dispatch(self, request, method, params): + if method not in self.func_map.keys(): + raise Exception('method "%s" is not supported' % method) + + auth_required, fn = self.func_map[method] + + if auth_required: + user = self._user_for_request(request) + if not user: + raise Exception("Invalid username/password") + + params = (user,) + params + + return fn(*params) + + def _marshaled_dispatch(self, request): + try: + params, method = xmlrpclib.loads(request.body) + + response = self._dispatch(request, method, params) + # wrap response in a singleton tuple + response = (response,) + response = self.dumps(response, methodresponse=1) + except xmlrpclib.Fault, fault: + response = self.dumps(fault) + except: + # report exception back to server + response = self.dumps( + xmlrpclib.Fault(1, "%s:%s" % (sys.exc_type, sys.exc_value)), + ) + + return response + +dispatcher = PatchworkXMLRPCDispatcher() + +# XMLRPC view function +@csrf_exempt +def xmlrpc(request): + if request.method != 'POST': + return HttpResponseRedirect( + urlresolvers.reverse('patchwork.views.help', + kwargs = {'path': 'pwclient/'})) + + response = HttpResponse() + try: + ret = dispatcher._marshaled_dispatch(request) + response.write(ret) + except Exception: + return HttpResponseServerError() + + return response + +# decorator for XMLRPC methods. Setting login_required to true will call +# the decorated function with a non-optional user as the first argument. +def xmlrpc_method(login_required = False): + def wrap(f): + dispatcher.register_function(f, login_required) + return f + + return wrap + + + +# We allow most of the Django field lookup types for remote queries +LOOKUP_TYPES = ["iexact", "contains", "icontains", "gt", "gte", "lt", + "in", "startswith", "istartswith", "endswith", + "iendswith", "range", "year", "month", "day", "isnull" ] + +####################################################################### +# Helper functions +####################################################################### + +def project_to_dict(obj): + """Return a trimmed down dictionary representation of a Project + object which is OK to send to the client.""" + return \ + { + 'id' : obj.id, + 'linkname' : obj.linkname, + 'name' : obj.name, + } + +def person_to_dict(obj): + """Return a trimmed down dictionary representation of a Person + object which is OK to send to the client.""" + + # Make sure we don't return None even if the user submitted a patch + # with no real name. XMLRPC can't marshall None. + if obj.name is not None: + name = obj.name + else: + name = obj.email + + return \ + { + 'id' : obj.id, + 'email' : obj.email, + 'name' : name, + 'user' : unicode(obj.user).encode("utf-8"), + } + +def patch_to_dict(obj): + """Return a trimmed down dictionary representation of a Patch + object which is OK to send to the client.""" + return \ + { + 'id' : obj.id, + 'date' : unicode(obj.date).encode("utf-8"), + 'filename' : obj.filename(), + 'msgid' : obj.msgid, + 'name' : obj.name, + 'project' : unicode(obj.project).encode("utf-8"), + 'project_id' : obj.project_id, + 'state' : unicode(obj.state).encode("utf-8"), + 'state_id' : obj.state_id, + 'archived' : obj.archived, + 'submitter' : unicode(obj.submitter).encode("utf-8"), + 'submitter_id' : obj.submitter_id, + 'delegate' : unicode(obj.delegate).encode("utf-8"), + 'delegate_id' : max(obj.delegate_id, 0), + 'commit_ref' : max(obj.commit_ref, ''), + } + +def bundle_to_dict(obj): + """Return a trimmed down dictionary representation of a Bundle + object which is OK to send to the client.""" + return \ + { + 'id' : obj.id, + 'name' : obj.name, + 'n_patches' : obj.n_patches(), + 'public_url' : obj.public_url(), + } + +def state_to_dict(obj): + """Return a trimmed down dictionary representation of a State + object which is OK to send to the client.""" + return \ + { + 'id' : obj.id, + 'name' : obj.name, + } + +####################################################################### +# Public XML-RPC methods +####################################################################### + +@xmlrpc_method(False) +def pw_rpc_version(): + """Return Patchwork XML-RPC interface version.""" + return 1 + +@xmlrpc_method(False) +def project_list(search_str="", max_count=0): + """Get a list of projects matching the given filters.""" + try: + if len(search_str) > 0: + projects = Project.objects.filter(linkname__icontains = search_str) + else: + projects = Project.objects.all() + + if max_count > 0: + return map(project_to_dict, projects)[:max_count] + else: + return map(project_to_dict, projects) + except: + return [] + +@xmlrpc_method(False) +def project_get(project_id): + """Return structure for the given project ID.""" + try: + project = Project.objects.filter(id = project_id)[0] + return project_to_dict(project) + except: + return {} + +@xmlrpc_method(False) +def person_list(search_str="", max_count=0): + """Get a list of Person objects matching the given filters.""" + try: + if len(search_str) > 0: + people = (Person.objects.filter(name__icontains = search_str) | + Person.objects.filter(email__icontains = search_str)) + else: + people = Person.objects.all() + + if max_count > 0: + return map(person_to_dict, people)[:max_count] + else: + return map(person_to_dict, people) + + except: + return [] + +@xmlrpc_method(False) +def person_get(person_id): + """Return structure for the given person ID.""" + try: + person = Person.objects.filter(id = person_id)[0] + return person_to_dict(person) + except: + return {} + +@xmlrpc_method(False) +def patch_list(filter={}): + """Get a list of patches matching the given filters.""" + try: + # We allow access to many of the fields. But, some fields are + # filtered by raw object so we must lookup by ID instead over + # XML-RPC. + ok_fields = [ + "id", + "name", + "project_id", + "submitter_id", + "delegate_id", + "archived", + "state_id", + "date", + "commit_ref", + "hash", + "msgid", + "max_count", + ] + + dfilter = {} + max_count = 0 + + for key in filter: + parts = key.split("__") + if parts[0] not in ok_fields: + # Invalid field given + return [] + if len(parts) > 1: + if LOOKUP_TYPES.count(parts[1]) == 0: + # Invalid lookup type given + return [] + + if parts[0] == 'project_id': + dfilter['project'] = Project.objects.filter(id = + filter[key])[0] + elif parts[0] == 'submitter_id': + dfilter['submitter'] = Person.objects.filter(id = + filter[key])[0] + elif parts[0] == 'delegate_id': + dfilter['delegate'] = Person.objects.filter(id = + filter[key])[0] + elif parts[0] == 'state_id': + dfilter['state'] = State.objects.filter(id = + filter[key])[0] + elif parts[0] == 'max_count': + max_count = filter[key] + else: + dfilter[key] = filter[key] + + patches = Patch.objects.filter(**dfilter) + + if max_count > 0: + return map(patch_to_dict, patches[:max_count]) + else: + return map(patch_to_dict, patches) + + except: + return [] + +@xmlrpc_method(False) +def patch_get(patch_id): + """Return structure for the given patch ID.""" + try: + patch = Patch.objects.filter(id = patch_id)[0] + return patch_to_dict(patch) + except: + return {} + +@xmlrpc_method(False) +def patch_get_by_hash(hash): + """Return structure for the given patch hash.""" + try: + patch = Patch.objects.filter(hash = hash)[0] + return patch_to_dict(patch) + except: + return {} + +@xmlrpc_method(False) +def patch_get_by_project_hash(project, hash): + """Return structure for the given patch hash.""" + try: + patch = Patch.objects.filter(project__linkname = project, + hash = hash)[0] + return patch_to_dict(patch) + except: + return {} + +@xmlrpc_method(False) +def patch_get_mbox(patch_id): + """Return mbox string for the given patch ID.""" + try: + patch = Patch.objects.filter(id = patch_id)[0] + return patch_to_mbox(patch).as_string(True) + except: + return "" + +@xmlrpc_method(False) +def patch_get_diff(patch_id): + """Return diff for the given patch ID.""" + try: + patch = Patch.objects.filter(id = patch_id)[0] + return patch.content + except: + return "" + +@xmlrpc_method(True) +def patch_set(user, patch_id, params): + """Update a patch with the key,value pairs in params. Only some parameters + can be set""" + try: + ok_params = ['state', 'commit_ref', 'archived'] + + patch = Patch.objects.get(id = patch_id) + + if not patch.is_editable(user): + raise Exception('No permissions to edit this patch') + + for (k, v) in params.iteritems(): + if k not in ok_params: + continue + + if k == 'state': + patch.state = State.objects.get(id = v) + + else: + setattr(patch, k, v) + + patch.save() + + return True + + except: + raise + +@xmlrpc_method(False) +def state_list(search_str="", max_count=0): + """Get a list of state structures matching the given search string.""" + try: + if len(search_str) > 0: + states = State.objects.filter(name__icontains = search_str) + else: + states = State.objects.all() + + if max_count > 0: + return map(state_to_dict, states)[:max_count] + else: + return map(state_to_dict, states) + except: + return [] + +@xmlrpc_method(False) +def state_get(state_id): + """Return structure for the given state ID.""" + try: + state = State.objects.filter(id = state_id)[0] + return state_to_dict(state) + except: + return {} -- cgit v1.2.3