/breezy-svn/trunk

To get this branch, use:
bzr branch https://code.breezy-vcs.org/breezy-svn/trunk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# Copyright (C) 2009 Jelmer Vernooij <jelmer@samba.org>

# Portions copied from the bzr-keywords plugin, written by Ian Clatworthy and:
# Copyright (C) 2008 Canonical Ltd

# This program 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.

# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA


"""Support for keyword expansion similar to that done by svn:keywords."""

from __future__ import absolute_import

import re

from subvertpy import (
    properties,
    )

from breezy import (
    urlutils,
    )
from breezy.errors import (
    BzrError,
    InvalidRevisionId,
    )
from breezy.filters import (
    ContentFilter,
    )

from breezy.plugins.svn import gettext
from breezy.plugins.svn.mapping import (
    mapping_registry,
    )


def keyword_date(revid, rev, relpath, revmeta):
    """last changed date"""
    if revmeta is not None:
        return revmeta.revprops.get(properties.PROP_REVISION_DATE, "")
    if rev is not None:
        return properties.time_to_cstring(1000000*rev.timestamp)
    return None


def keyword_rev(revid, rev, relpath, revmeta):
    """last revno that changed this file"""
    #  See if c.revision() can be traced back to a subversion revision

    # Revision comes directly from a foreign repository
    if revmeta is not None:
        return str(revmeta.revnum)

    if revid is None:
        return None

    # Revision was once imported from a foreign repository
    try:
        foreign_revid, mapping = mapping_registry.parse_revision_id(revid)
    except InvalidRevisionId:
        pass
    else:
        return str(foreign_revid[2])

    # If we can't find the actual svn revision, just return the bzr revid
    return revid


def keyword_author(revid, rev, relpath, revmeta):
    """author of the last commit."""
    if revmeta is not None:
        return revmeta.revprops.get(properties.PROP_REVISION_AUTHOR, "")
    if rev is not None:
        return rev.committer.encode("utf-8")
    return None


def keyword_id(*args):
    """basename <space> revno <space> date <space> author"""
    return "%s %s %s %s" % (urlutils.basename(keyword_url(*args)),
            keyword_rev(*args), keyword_date(*args),
            keyword_author(*args))


def keyword_url(revid, rev, relpath, revmeta):
    # URL in the svn repository
    # See if c.revision() can be traced back to a subversion revision
    if revmeta is not None:
        return urlutils.join(revmeta.repository.base, revmeta.branch_path,
                             relpath)
    return relpath


# callback(revision_id, revision, relpath, revmeta) -> str
keywords = {
    "LastChangedDate": keyword_date,
    "Date": keyword_date,
    "LastChangedRevision": keyword_rev,
    "Rev": keyword_rev,
    "Revision": keyword_rev,
    "LastChangedBy": keyword_author,
    "Author": keyword_author,
    "HeadURL": keyword_url,
    "URL": keyword_url,
    "Id": keyword_id,
}


# Regular expressions for matching the raw and cooked patterns
_KW_RAW_RE = re.compile(r'\$([\w\-]+)(:[^$]*)?\$')
_KW_COOKED_RE = re.compile(r'\$([\w\-]+):([^$]+)\$')


def expand_keywords(s, allowed_keywords, context=None, encoder=None):
    """Replace raw style keywords in a string.

    Note: If the keyword is already in the expanded style, the value is
    not replaced.

    :param s: the string
    :param keyword_dicts: an iterable of keyword dictionaries. If values
      are callables, they are executed to find the real value.
    :param context: the parameter to pass to callable values
    :param style: the style of expansion to use of None for the default
    :return: the string with keywords expanded
    """
    result = ''
    rest = s
    while True:
        match = _KW_RAW_RE.search(rest)
        if not match:
            break
        result += rest[:match.start()]
        original_expansion = match.group(0)
        keyword = match.group(1)
        if not keyword in allowed_keywords:
            # Unknown expansion - leave as is
            result += original_expansion
            rest = rest[match.end():]
            continue
        expansion = keywords[keyword](context.revision_id(),
            context.revision(), context.relpath().encode("utf-8"),
            getattr(context.revision(), "svn_meta", None))
        if expansion is not None and type(expansion) is not str:
            raise AssertionError("Expansion string not plain: %r" % expansion)
        if expansion is not None:
            if '$' in expansion:
                # Expansion is not safe to be collapsed later
                expansion = gettext("(value unsafe to expand)")
            if encoder is not None:
                expansion = encoder(expansion)
            expanded = "$%s: %s $" % (keyword, expansion)
        else:
            expanded = "$%s$" % keyword
        result += expanded
        rest = rest[match.end():]
    return result + rest


def compress_keywords(s, allowed_keywords):
    """Replace cooked style keywords with raw style in a string.

    :param s: the string
    :return: the string with keywords compressed
    """
    result = ''
    rest = s
    while True:
        match = _KW_COOKED_RE.search(rest)
        if not match:
            break
        result += rest[:match.start()]
        keyword = match.group(1)
        if keyword in allowed_keywords:
            result += "$%s$" % keyword
        else:
            result += match.group(0)
        rest = rest[match.end():]
    return result + rest


class SubversionKeywordContentFilter(ContentFilter):

    def __init__(self, allowed_keywords):
        self.allowed_keywords = allowed_keywords

    def reader(self, chunks, context=None):
        """Filter that replaces keywords with their compressed form."""
        text = ''.join(chunks)
        return [compress_keywords(text, self.allowed_keywords)]

    def writer(self, chunks, context, encoder=None):
        """Keyword expander."""
        text = ''.join(chunks)
        return [expand_keywords(text, self.allowed_keywords, context=context,
            encoder=encoder)]


def create_svn_keywords_filter(value):
    if not value:
        return
    kws = value.split(" ")
    for k in kws:
        if not k in keywords:
            raise BzrError(gettext("Unknown svn keyword %s") % k)
    if kws == []:
        return []
    return [SubversionKeywordContentFilter(kws)]