144 |
145 | {%- endblock %}
146 |
--------------------------------------------------------------------------------
/vcs/tests/test_repository.py:
--------------------------------------------------------------------------------
1 | from __future__ import with_statement
2 | import datetime
3 | from vcs.tests.base import BackendTestMixin
4 | from vcs.tests.conf import SCM_TESTS
5 | from vcs.tests.conf import TEST_USER_CONFIG_FILE
6 | from vcs.nodes import FileNode
7 | from vcs.utils.compat import unittest
8 | from vcs.exceptions import ChangesetDoesNotExistError
9 |
10 |
11 | class RepositoryBaseTest(BackendTestMixin):
12 | recreate_repo_per_test = False
13 |
14 | @classmethod
15 | def _get_commits(cls):
16 | return super(RepositoryBaseTest, cls)._get_commits()[:1]
17 |
18 | def test_get_config_value(self):
19 | self.assertEqual(self.repo.get_config_value('universal', 'foo',
20 | TEST_USER_CONFIG_FILE), 'bar')
21 |
22 | def test_get_config_value_defaults_to_None(self):
23 | self.assertEqual(self.repo.get_config_value('universal', 'nonexist',
24 | TEST_USER_CONFIG_FILE), None)
25 |
26 | def test_get_user_name(self):
27 | self.assertEqual(self.repo.get_user_name(TEST_USER_CONFIG_FILE),
28 | 'Foo Bar')
29 |
30 | def test_get_user_email(self):
31 | self.assertEqual(self.repo.get_user_email(TEST_USER_CONFIG_FILE),
32 | 'foo.bar@example.com')
33 |
34 | def test_repo_equality(self):
35 | self.assertTrue(self.repo == self.repo)
36 |
37 | def test_repo_equality_broken_object(self):
38 | import copy
39 | _repo = copy.copy(self.repo)
40 | delattr(_repo, 'path')
41 | self.assertTrue(self.repo != _repo)
42 |
43 | def test_repo_equality_other_object(self):
44 | class dummy(object):
45 | path = self.repo.path
46 | self.assertTrue(self.repo != dummy())
47 |
48 |
49 | class RepositoryGetDiffTest(BackendTestMixin):
50 |
51 | @classmethod
52 | def _get_commits(cls):
53 | commits = [
54 | {
55 | 'message': 'Initial commit',
56 | 'author': 'Joe Doe ',
57 | 'date': datetime.datetime(2010, 1, 1, 20),
58 | 'added': [
59 | FileNode('foobar', content='foobar'),
60 | FileNode('foobar2', content='foobar2'),
61 | ],
62 | },
63 | {
64 | 'message': 'Changed foobar, added foobar3',
65 | 'author': 'Jane Doe ',
66 | 'date': datetime.datetime(2010, 1, 1, 21),
67 | 'added': [
68 | FileNode('foobar3', content='foobar3'),
69 | ],
70 | 'changed': [
71 | FileNode('foobar', 'FOOBAR'),
72 | ],
73 | },
74 | {
75 | 'message': 'Removed foobar, changed foobar3',
76 | 'author': 'Jane Doe ',
77 | 'date': datetime.datetime(2010, 1, 1, 22),
78 | 'changed': [
79 | FileNode('foobar3', content='FOOBAR\nFOOBAR\nFOOBAR\n'),
80 | ],
81 | 'removed': [FileNode('foobar')],
82 | },
83 | ]
84 | return commits
85 |
86 | def test_raise_for_wrong(self):
87 | with self.assertRaises(ChangesetDoesNotExistError):
88 | self.repo.get_diff('a' * 40, 'b' * 40)
89 |
90 |
91 | class GitRepositoryGetDiffTest(RepositoryGetDiffTest, unittest.TestCase):
92 | backend_alias = 'git'
93 |
94 | def test_initial_commit_diff(self):
95 | initial_rev = self.repo.revisions[0]
96 | self.assertEqual(self.repo.get_diff(self.repo.EMPTY_CHANGESET, initial_rev), '''diff --git a/foobar b/foobar
97 | new file mode 100644
98 | index 0000000000000000000000000000000000000000..f6ea0495187600e7b2288c8ac19c5886383a4632
99 | --- /dev/null
100 | +++ b/foobar
101 | @@ -0,0 +1 @@
102 | +foobar
103 | \ No newline at end of file
104 | diff --git a/foobar2 b/foobar2
105 | new file mode 100644
106 | index 0000000000000000000000000000000000000000..e8c9d6b98e3dce993a464935e1a53f50b56a3783
107 | --- /dev/null
108 | +++ b/foobar2
109 | @@ -0,0 +1 @@
110 | +foobar2
111 | \ No newline at end of file
112 | ''')
113 |
114 | def test_second_changeset_diff(self):
115 | revs = self.repo.revisions
116 | self.assertEqual(self.repo.get_diff(revs[0], revs[1]), '''diff --git a/foobar b/foobar
117 | index f6ea0495187600e7b2288c8ac19c5886383a4632..389865bb681b358c9b102d79abd8d5f941e96551 100644
118 | --- a/foobar
119 | +++ b/foobar
120 | @@ -1 +1 @@
121 | -foobar
122 | \ No newline at end of file
123 | +FOOBAR
124 | \ No newline at end of file
125 | diff --git a/foobar3 b/foobar3
126 | new file mode 100644
127 | index 0000000000000000000000000000000000000000..c11c37d41d33fb47741cff93fa5f9d798c1535b0
128 | --- /dev/null
129 | +++ b/foobar3
130 | @@ -0,0 +1 @@
131 | +foobar3
132 | \ No newline at end of file
133 | ''')
134 |
135 | def test_third_changeset_diff(self):
136 | revs = self.repo.revisions
137 | self.assertEqual(self.repo.get_diff(revs[1], revs[2]), '''diff --git a/foobar b/foobar
138 | deleted file mode 100644
139 | index 389865bb681b358c9b102d79abd8d5f941e96551..0000000000000000000000000000000000000000
140 | --- a/foobar
141 | +++ /dev/null
142 | @@ -1 +0,0 @@
143 | -FOOBAR
144 | \ No newline at end of file
145 | diff --git a/foobar3 b/foobar3
146 | index c11c37d41d33fb47741cff93fa5f9d798c1535b0..f9324477362684ff692aaf5b9a81e01b9e9a671c 100644
147 | --- a/foobar3
148 | +++ b/foobar3
149 | @@ -1 +1,3 @@
150 | -foobar3
151 | \ No newline at end of file
152 | +FOOBAR
153 | +FOOBAR
154 | +FOOBAR
155 | ''')
156 |
157 |
158 | class HgRepositoryGetDiffTest(RepositoryGetDiffTest, unittest.TestCase):
159 | backend_alias = 'hg'
160 |
161 | def test_initial_commit_diff(self):
162 | initial_rev = self.repo.revisions[0]
163 | self.assertEqual(self.repo.get_diff(self.repo.EMPTY_CHANGESET, initial_rev), '''diff --git a/foobar b/foobar
164 | new file mode 100755
165 | --- /dev/null
166 | +++ b/foobar
167 | @@ -0,0 +1,1 @@
168 | +foobar
169 | \ No newline at end of file
170 | diff --git a/foobar2 b/foobar2
171 | new file mode 100755
172 | --- /dev/null
173 | +++ b/foobar2
174 | @@ -0,0 +1,1 @@
175 | +foobar2
176 | \ No newline at end of file
177 | ''')
178 |
179 | def test_second_changeset_diff(self):
180 | revs = self.repo.revisions
181 | self.assertEqual(self.repo.get_diff(revs[0], revs[1]), '''diff --git a/foobar b/foobar
182 | --- a/foobar
183 | +++ b/foobar
184 | @@ -1,1 +1,1 @@
185 | -foobar
186 | \ No newline at end of file
187 | +FOOBAR
188 | \ No newline at end of file
189 | diff --git a/foobar3 b/foobar3
190 | new file mode 100755
191 | --- /dev/null
192 | +++ b/foobar3
193 | @@ -0,0 +1,1 @@
194 | +foobar3
195 | \ No newline at end of file
196 | ''')
197 |
198 | def test_third_changeset_diff(self):
199 | revs = self.repo.revisions
200 | self.assertEqual(self.repo.get_diff(revs[1], revs[2]), '''diff --git a/foobar b/foobar
201 | deleted file mode 100755
202 | --- a/foobar
203 | +++ /dev/null
204 | @@ -1,1 +0,0 @@
205 | -FOOBAR
206 | \ No newline at end of file
207 | diff --git a/foobar3 b/foobar3
208 | --- a/foobar3
209 | +++ b/foobar3
210 | @@ -1,1 +1,3 @@
211 | -foobar3
212 | \ No newline at end of file
213 | +FOOBAR
214 | +FOOBAR
215 | +FOOBAR
216 | ''')
217 |
218 |
219 | # For each backend create test case class
220 | for alias in SCM_TESTS:
221 | attrs = {
222 | 'backend_alias': alias,
223 | }
224 | cls_name = alias.capitalize() + RepositoryBaseTest.__name__
225 | bases = (RepositoryBaseTest, unittest.TestCase)
226 | globals()[cls_name] = type(cls_name, bases, attrs)
227 |
228 | if __name__ == '__main__':
229 | unittest.main()
230 |
--------------------------------------------------------------------------------
/vcs/utils/termcolors.py:
--------------------------------------------------------------------------------
1 | """
2 | termcolors.py
3 |
4 | Grabbed from Django (http://www.djangoproject.com)
5 | """
6 |
7 | color_names = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white')
8 | foreground = dict([(color_names[x], '3%s' % x) for x in range(8)])
9 | background = dict([(color_names[x], '4%s' % x) for x in range(8)])
10 |
11 | RESET = '0'
12 | opt_dict = {'bold': '1', 'underscore': '4', 'blink': '5', 'reverse': '7', 'conceal': '8'}
13 |
14 | def colorize(text='', opts=(), **kwargs):
15 | """
16 | Returns your text, enclosed in ANSI graphics codes.
17 |
18 | Depends on the keyword arguments 'fg' and 'bg', and the contents of
19 | the opts tuple/list.
20 |
21 | Returns the RESET code if no parameters are given.
22 |
23 | Valid colors:
24 | 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'
25 |
26 | Valid options:
27 | 'bold'
28 | 'underscore'
29 | 'blink'
30 | 'reverse'
31 | 'conceal'
32 | 'noreset' - string will not be auto-terminated with the RESET code
33 |
34 | Examples:
35 | colorize('hello', fg='red', bg='blue', opts=('blink',))
36 | colorize()
37 | colorize('goodbye', opts=('underscore',))
38 | print colorize('first line', fg='red', opts=('noreset',))
39 | print 'this should be red too'
40 | print colorize('and so should this')
41 | print 'this should not be red'
42 | """
43 | code_list = []
44 | if text == '' and len(opts) == 1 and opts[0] == 'reset':
45 | return '\x1b[%sm' % RESET
46 | for k, v in kwargs.iteritems():
47 | if k == 'fg':
48 | code_list.append(foreground[v])
49 | elif k == 'bg':
50 | code_list.append(background[v])
51 | for o in opts:
52 | if o in opt_dict:
53 | code_list.append(opt_dict[o])
54 | if 'noreset' not in opts:
55 | text = text + '\x1b[%sm' % RESET
56 | return ('\x1b[%sm' % ';'.join(code_list)) + text
57 |
58 | def make_style(opts=(), **kwargs):
59 | """
60 | Returns a function with default parameters for colorize()
61 |
62 | Example:
63 | bold_red = make_style(opts=('bold',), fg='red')
64 | print bold_red('hello')
65 | KEYWORD = make_style(fg='yellow')
66 | COMMENT = make_style(fg='blue', opts=('bold',))
67 | """
68 | return lambda text: colorize(text, opts, **kwargs)
69 |
70 | NOCOLOR_PALETTE = 'nocolor'
71 | DARK_PALETTE = 'dark'
72 | LIGHT_PALETTE = 'light'
73 |
74 | PALETTES = {
75 | NOCOLOR_PALETTE: {
76 | 'ERROR': {},
77 | 'NOTICE': {},
78 | 'SQL_FIELD': {},
79 | 'SQL_COLTYPE': {},
80 | 'SQL_KEYWORD': {},
81 | 'SQL_TABLE': {},
82 | 'HTTP_INFO': {},
83 | 'HTTP_SUCCESS': {},
84 | 'HTTP_REDIRECT': {},
85 | 'HTTP_NOT_MODIFIED': {},
86 | 'HTTP_BAD_REQUEST': {},
87 | 'HTTP_NOT_FOUND': {},
88 | 'HTTP_SERVER_ERROR': {},
89 | },
90 | DARK_PALETTE: {
91 | 'ERROR': { 'fg': 'red', 'opts': ('bold',) },
92 | 'NOTICE': { 'fg': 'red' },
93 | 'SQL_FIELD': { 'fg': 'green', 'opts': ('bold',) },
94 | 'SQL_COLTYPE': { 'fg': 'green' },
95 | 'SQL_KEYWORD': { 'fg': 'yellow' },
96 | 'SQL_TABLE': { 'opts': ('bold',) },
97 | 'HTTP_INFO': { 'opts': ('bold',) },
98 | 'HTTP_SUCCESS': { },
99 | 'HTTP_REDIRECT': { 'fg': 'green' },
100 | 'HTTP_NOT_MODIFIED': { 'fg': 'cyan' },
101 | 'HTTP_BAD_REQUEST': { 'fg': 'red', 'opts': ('bold',) },
102 | 'HTTP_NOT_FOUND': { 'fg': 'yellow' },
103 | 'HTTP_SERVER_ERROR': { 'fg': 'magenta', 'opts': ('bold',) },
104 | },
105 | LIGHT_PALETTE: {
106 | 'ERROR': { 'fg': 'red', 'opts': ('bold',) },
107 | 'NOTICE': { 'fg': 'red' },
108 | 'SQL_FIELD': { 'fg': 'green', 'opts': ('bold',) },
109 | 'SQL_COLTYPE': { 'fg': 'green' },
110 | 'SQL_KEYWORD': { 'fg': 'blue' },
111 | 'SQL_TABLE': { 'opts': ('bold',) },
112 | 'HTTP_INFO': { 'opts': ('bold',) },
113 | 'HTTP_SUCCESS': { },
114 | 'HTTP_REDIRECT': { 'fg': 'green', 'opts': ('bold',) },
115 | 'HTTP_NOT_MODIFIED': { 'fg': 'green' },
116 | 'HTTP_BAD_REQUEST': { 'fg': 'red', 'opts': ('bold',) },
117 | 'HTTP_NOT_FOUND': { 'fg': 'red' },
118 | 'HTTP_SERVER_ERROR': { 'fg': 'magenta', 'opts': ('bold',) },
119 | }
120 | }
121 | DEFAULT_PALETTE = DARK_PALETTE
122 |
123 | def parse_color_setting(config_string):
124 | """Parse a DJANGO_COLORS environment variable to produce the system palette
125 |
126 | The general form of a pallete definition is:
127 |
128 | "palette;role=fg;role=fg/bg;role=fg,option,option;role=fg/bg,option,option"
129 |
130 | where:
131 | palette is a named palette; one of 'light', 'dark', or 'nocolor'.
132 | role is a named style used by Django
133 | fg is a background color.
134 | bg is a background color.
135 | option is a display options.
136 |
137 | Specifying a named palette is the same as manually specifying the individual
138 | definitions for each role. Any individual definitions following the pallete
139 | definition will augment the base palette definition.
140 |
141 | Valid roles:
142 | 'error', 'notice', 'sql_field', 'sql_coltype', 'sql_keyword', 'sql_table',
143 | 'http_info', 'http_success', 'http_redirect', 'http_bad_request',
144 | 'http_not_found', 'http_server_error'
145 |
146 | Valid colors:
147 | 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'
148 |
149 | Valid options:
150 | 'bold', 'underscore', 'blink', 'reverse', 'conceal'
151 |
152 | """
153 | if not config_string:
154 | return PALETTES[DEFAULT_PALETTE]
155 |
156 | # Split the color configuration into parts
157 | parts = config_string.lower().split(';')
158 | palette = PALETTES[NOCOLOR_PALETTE].copy()
159 | for part in parts:
160 | if part in PALETTES:
161 | # A default palette has been specified
162 | palette.update(PALETTES[part])
163 | elif '=' in part:
164 | # Process a palette defining string
165 | definition = {}
166 |
167 | # Break the definition into the role,
168 | # plus the list of specific instructions.
169 | # The role must be in upper case
170 | role, instructions = part.split('=')
171 | role = role.upper()
172 |
173 | styles = instructions.split(',')
174 | styles.reverse()
175 |
176 | # The first instruction can contain a slash
177 | # to break apart fg/bg.
178 | colors = styles.pop().split('/')
179 | colors.reverse()
180 | fg = colors.pop()
181 | if fg in color_names:
182 | definition['fg'] = fg
183 | if colors and colors[-1] in color_names:
184 | definition['bg'] = colors[-1]
185 |
186 | # All remaining instructions are options
187 | opts = tuple(s for s in styles if s in opt_dict.keys())
188 | if opts:
189 | definition['opts'] = opts
190 |
191 | # The nocolor palette has all available roles.
192 | # Use that palette as the basis for determining
193 | # if the role is valid.
194 | if role in PALETTES[NOCOLOR_PALETTE] and definition:
195 | palette[role] = definition
196 |
197 | # If there are no colors specified, return the empty palette.
198 | if palette == PALETTES[NOCOLOR_PALETTE]:
199 | return None
200 | return palette
201 |
--------------------------------------------------------------------------------
/vcs/utils/annotate.py:
--------------------------------------------------------------------------------
1 | import StringIO
2 |
3 | from pygments.formatters import HtmlFormatter
4 | from pygments import highlight
5 |
6 | from vcs.exceptions import VCSError
7 | from vcs.nodes import FileNode
8 |
9 |
10 | def annotate_highlight(filenode, annotate_from_changeset_func=None,
11 | order=None, headers=None, **options):
12 | """
13 | Returns html portion containing annotated table with 3 columns: line
14 | numbers, changeset information and pygmentized line of code.
15 |
16 | :param filenode: FileNode object
17 | :param annotate_from_changeset_func: function taking changeset and
18 | returning single annotate cell; needs break line at the end
19 | :param order: ordered sequence of ``ls`` (line numbers column),
20 | ``annotate`` (annotate column), ``code`` (code column); Default is
21 | ``['ls', 'annotate', 'code']``
22 | :param headers: dictionary with headers (keys are whats in ``order``
23 | parameter)
24 | """
25 | options['linenos'] = True
26 | formatter = AnnotateHtmlFormatter(filenode=filenode, order=order,
27 | headers=headers,
28 | annotate_from_changeset_func=annotate_from_changeset_func, **options)
29 | lexer = filenode.lexer
30 | highlighted = highlight(filenode.content, lexer, formatter)
31 | return highlighted
32 |
33 |
34 | class AnnotateHtmlFormatter(HtmlFormatter):
35 |
36 | def __init__(self, filenode, annotate_from_changeset_func=None,
37 | order=None, **options):
38 | """
39 | If ``annotate_from_changeset_func`` is passed it should be a function
40 | which returns string from the given changeset. For example, we may pass
41 | following function as ``annotate_from_changeset_func``::
42 |
43 | def changeset_to_anchor(changeset):
44 | return '%s\n' %\
45 | (changeset.id, changeset.id)
46 |
47 | :param annotate_from_changeset_func: see above
48 | :param order: (default: ``['ls', 'annotate', 'code']``); order of
49 | columns;
50 | :param options: standard pygment's HtmlFormatter options, there is
51 | extra option tough, ``headers``. For instance we can pass::
52 |
53 | formatter = AnnotateHtmlFormatter(filenode, headers={
54 | 'ls': '#',
55 | 'annotate': 'Annotate',
56 | 'code': 'Code',
57 | })
58 |
59 | """
60 | super(AnnotateHtmlFormatter, self).__init__(**options)
61 | self.annotate_from_changeset_func = annotate_from_changeset_func
62 | self.order = order or ('ls', 'annotate', 'code')
63 | headers = options.pop('headers', None)
64 | if headers and not ('ls' in headers and 'annotate' in headers and
65 | 'code' in headers):
66 | raise ValueError("If headers option dict is specified it must "
67 | "all 'ls', 'annotate' and 'code' keys")
68 | self.headers = headers
69 | if isinstance(filenode, FileNode):
70 | self.filenode = filenode
71 | else:
72 | raise VCSError("This formatter expect FileNode parameter, not %r"
73 | % type(filenode))
74 |
75 | def annotate_from_changeset(self, changeset):
76 | """
77 | Returns full html line for single changeset per annotated line.
78 | """
79 | if self.annotate_from_changeset_func:
80 | return self.annotate_from_changeset_func(changeset)
81 | else:
82 | return ''.join((changeset.id, '\n'))
83 |
84 | def _wrap_tablelinenos(self, inner):
85 | dummyoutfile = StringIO.StringIO()
86 | lncount = 0
87 | for t, line in inner:
88 | if t:
89 | lncount += 1
90 | dummyoutfile.write(line)
91 |
92 | fl = self.linenostart
93 | mw = len(str(lncount + fl - 1))
94 | sp = self.linenospecial
95 | st = self.linenostep
96 | la = self.lineanchors
97 | aln = self.anchorlinenos
98 | if sp:
99 | lines = []
100 |
101 | for i in range(fl, fl + lncount):
102 | if i % st == 0:
103 | if i % sp == 0:
104 | if aln:
105 | lines.append(''
106 | '%*d' %
107 | (la, i, mw, i))
108 | else:
109 | lines.append(''
110 | '%*d' % (mw, i))
111 | else:
112 | if aln:
113 | lines.append(''
114 | '%*d' % (la, i, mw, i))
115 | else:
116 | lines.append('%*d' % (mw, i))
117 | else:
118 | lines.append('')
119 | ls = '\n'.join(lines)
120 | else:
121 | lines = []
122 | for i in range(fl, fl + lncount):
123 | if i % st == 0:
124 | if aln:
125 | lines.append('%*d' \
126 | % (la, i, mw, i))
127 | else:
128 | lines.append('%*d' % (mw, i))
129 | else:
130 | lines.append('')
131 | ls = '\n'.join(lines)
132 |
133 | annotate_changesets = [tup[1] for tup in self.filenode.annotate]
134 | # If pygments cropped last lines break we need do that too
135 | ln_cs = len(annotate_changesets)
136 | ln_ = len(ls.splitlines())
137 | if ln_cs > ln_:
138 | annotate_changesets = annotate_changesets[:ln_ - ln_cs]
139 | annotate = ''.join((self.annotate_from_changeset(changeset)
140 | for changeset in annotate_changesets))
141 | # in case you wonder about the seemingly redundant
here:
142 | # since the content in the other cell also is wrapped in a div,
143 | # some browsers in some configurations seem to mess up the formatting.
144 | '''
145 | yield 0, ('