##// END OF EJS Templates
vcs: introduce 'branches' attribute on changesets, making it possible for Git to show multiple branches for a changeset...
Mads Kiilerich -
r7067:3dbb625d default
parent child Browse files
Show More
@@ -1,167 +1,167 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.feed
15 kallithea.controllers.feed
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Feed controller for Kallithea
18 Feed controller for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 23, 2010
22 :created_on: Apr 23, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28
28
29 import logging
29 import logging
30
30
31 from tg import response, tmpl_context as c
31 from tg import response, tmpl_context as c
32 from tg.i18n import ugettext as _
32 from tg.i18n import ugettext as _
33
33
34 from beaker.cache import cache_region, region_invalidate
34 from beaker.cache import cache_region, region_invalidate
35 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
35 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
36
36
37 from kallithea import CONFIG
37 from kallithea import CONFIG
38 from kallithea.lib import helpers as h
38 from kallithea.lib import helpers as h
39 from kallithea.lib.auth import LoginRequired, HasRepoPermissionLevelDecorator
39 from kallithea.lib.auth import LoginRequired, HasRepoPermissionLevelDecorator
40 from kallithea.lib.base import BaseRepoController
40 from kallithea.lib.base import BaseRepoController
41 from kallithea.lib.diffs import DiffProcessor
41 from kallithea.lib.diffs import DiffProcessor
42 from kallithea.model.db import CacheInvalidation
42 from kallithea.model.db import CacheInvalidation
43 from kallithea.lib.utils2 import safe_int, str2bool, safe_unicode
43 from kallithea.lib.utils2 import safe_int, str2bool, safe_unicode
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47
47
48 language = 'en-us'
48 language = 'en-us'
49 ttl = "5"
49 ttl = "5"
50
50
51
51
52 class FeedController(BaseRepoController):
52 class FeedController(BaseRepoController):
53
53
54 @LoginRequired(api_access=True, allow_default_user=True)
54 @LoginRequired(api_access=True, allow_default_user=True)
55 @HasRepoPermissionLevelDecorator('read')
55 @HasRepoPermissionLevelDecorator('read')
56 def _before(self, *args, **kwargs):
56 def _before(self, *args, **kwargs):
57 super(FeedController, self)._before(*args, **kwargs)
57 super(FeedController, self)._before(*args, **kwargs)
58
58
59 def _get_title(self, cs):
59 def _get_title(self, cs):
60 return h.shorter(cs.message, 160)
60 return h.shorter(cs.message, 160)
61
61
62 def __get_desc(self, cs):
62 def __get_desc(self, cs):
63 desc_msg = [(_('%s committed on %s')
63 desc_msg = [(_('%s committed on %s')
64 % (h.person(cs.author), h.fmt_date(cs.date))) + '<br/>']
64 % (h.person(cs.author), h.fmt_date(cs.date))) + '<br/>']
65 # branches, tags, bookmarks
65 # branches, tags, bookmarks
66 if cs.branch:
66 for branch in cs.branches:
67 desc_msg.append('branch: %s<br/>' % cs.branch)
67 desc_msg.append('branch: %s<br/>' % branch)
68 for book in cs.bookmarks:
68 for book in cs.bookmarks:
69 desc_msg.append('bookmark: %s<br/>' % book)
69 desc_msg.append('bookmark: %s<br/>' % book)
70 for tag in cs.tags:
70 for tag in cs.tags:
71 desc_msg.append('tag: %s<br/>' % tag)
71 desc_msg.append('tag: %s<br/>' % tag)
72
72
73 changes = []
73 changes = []
74 diff_limit = safe_int(CONFIG.get('rss_cut_off_limit', 32 * 1024))
74 diff_limit = safe_int(CONFIG.get('rss_cut_off_limit', 32 * 1024))
75 raw_diff = cs.diff()
75 raw_diff = cs.diff()
76 diff_processor = DiffProcessor(raw_diff,
76 diff_processor = DiffProcessor(raw_diff,
77 diff_limit=diff_limit,
77 diff_limit=diff_limit,
78 inline_diff=False)
78 inline_diff=False)
79
79
80 for st in diff_processor.parsed:
80 for st in diff_processor.parsed:
81 st.update({'added': st['stats']['added'],
81 st.update({'added': st['stats']['added'],
82 'removed': st['stats']['deleted']})
82 'removed': st['stats']['deleted']})
83 changes.append('\n %(operation)s %(filename)s '
83 changes.append('\n %(operation)s %(filename)s '
84 '(%(added)s lines added, %(removed)s lines removed)'
84 '(%(added)s lines added, %(removed)s lines removed)'
85 % st)
85 % st)
86 if diff_processor.limited_diff:
86 if diff_processor.limited_diff:
87 changes = changes + ['\n ' +
87 changes = changes + ['\n ' +
88 _('Changeset was too big and was cut off...')]
88 _('Changeset was too big and was cut off...')]
89
89
90 # rev link
90 # rev link
91 _url = h.canonical_url('changeset_home', repo_name=c.db_repo.repo_name,
91 _url = h.canonical_url('changeset_home', repo_name=c.db_repo.repo_name,
92 revision=cs.raw_id)
92 revision=cs.raw_id)
93 desc_msg.append('changeset: <a href="%s">%s</a>' % (_url, cs.raw_id[:8]))
93 desc_msg.append('changeset: <a href="%s">%s</a>' % (_url, cs.raw_id[:8]))
94
94
95 desc_msg.append('<pre>')
95 desc_msg.append('<pre>')
96 desc_msg.append(h.urlify_text(cs.message))
96 desc_msg.append(h.urlify_text(cs.message))
97 desc_msg.append('\n')
97 desc_msg.append('\n')
98 desc_msg.extend(changes)
98 desc_msg.extend(changes)
99 if str2bool(CONFIG.get('rss_include_diff', False)):
99 if str2bool(CONFIG.get('rss_include_diff', False)):
100 desc_msg.append('\n\n')
100 desc_msg.append('\n\n')
101 desc_msg.append(raw_diff)
101 desc_msg.append(raw_diff)
102 desc_msg.append('</pre>')
102 desc_msg.append('</pre>')
103 return map(safe_unicode, desc_msg)
103 return map(safe_unicode, desc_msg)
104
104
105 def atom(self, repo_name):
105 def atom(self, repo_name):
106 """Produce an atom-1.0 feed via feedgenerator module"""
106 """Produce an atom-1.0 feed via feedgenerator module"""
107
107
108 @cache_region('long_term', '_get_feed_from_cache')
108 @cache_region('long_term', '_get_feed_from_cache')
109 def _get_feed_from_cache(key, kind):
109 def _get_feed_from_cache(key, kind):
110 feed = Atom1Feed(
110 feed = Atom1Feed(
111 title=_('%s %s feed') % (c.site_name, repo_name),
111 title=_('%s %s feed') % (c.site_name, repo_name),
112 link=h.canonical_url('summary_home', repo_name=repo_name),
112 link=h.canonical_url('summary_home', repo_name=repo_name),
113 description=_('Changes on %s repository') % repo_name,
113 description=_('Changes on %s repository') % repo_name,
114 language=language,
114 language=language,
115 ttl=ttl
115 ttl=ttl
116 )
116 )
117
117
118 rss_items_per_page = safe_int(CONFIG.get('rss_items_per_page', 20))
118 rss_items_per_page = safe_int(CONFIG.get('rss_items_per_page', 20))
119 for cs in reversed(list(c.db_repo_scm_instance[-rss_items_per_page:])):
119 for cs in reversed(list(c.db_repo_scm_instance[-rss_items_per_page:])):
120 feed.add_item(title=self._get_title(cs),
120 feed.add_item(title=self._get_title(cs),
121 link=h.canonical_url('changeset_home', repo_name=repo_name,
121 link=h.canonical_url('changeset_home', repo_name=repo_name,
122 revision=cs.raw_id),
122 revision=cs.raw_id),
123 author_name=cs.author,
123 author_name=cs.author,
124 description=''.join(self.__get_desc(cs)),
124 description=''.join(self.__get_desc(cs)),
125 pubdate=cs.date,
125 pubdate=cs.date,
126 )
126 )
127
127
128 response.content_type = feed.mime_type
128 response.content_type = feed.mime_type
129 return feed.writeString('utf-8')
129 return feed.writeString('utf-8')
130
130
131 kind = 'ATOM'
131 kind = 'ATOM'
132 valid = CacheInvalidation.test_and_set_valid(repo_name, kind)
132 valid = CacheInvalidation.test_and_set_valid(repo_name, kind)
133 if not valid:
133 if not valid:
134 region_invalidate(_get_feed_from_cache, None, '_get_feed_from_cache', repo_name, kind)
134 region_invalidate(_get_feed_from_cache, None, '_get_feed_from_cache', repo_name, kind)
135 return _get_feed_from_cache(repo_name, kind)
135 return _get_feed_from_cache(repo_name, kind)
136
136
137 def rss(self, repo_name):
137 def rss(self, repo_name):
138 """Produce an rss2 feed via feedgenerator module"""
138 """Produce an rss2 feed via feedgenerator module"""
139
139
140 @cache_region('long_term', '_get_feed_from_cache')
140 @cache_region('long_term', '_get_feed_from_cache')
141 def _get_feed_from_cache(key, kind):
141 def _get_feed_from_cache(key, kind):
142 feed = Rss201rev2Feed(
142 feed = Rss201rev2Feed(
143 title=_('%s %s feed') % (c.site_name, repo_name),
143 title=_('%s %s feed') % (c.site_name, repo_name),
144 link=h.canonical_url('summary_home', repo_name=repo_name),
144 link=h.canonical_url('summary_home', repo_name=repo_name),
145 description=_('Changes on %s repository') % repo_name,
145 description=_('Changes on %s repository') % repo_name,
146 language=language,
146 language=language,
147 ttl=ttl
147 ttl=ttl
148 )
148 )
149
149
150 rss_items_per_page = safe_int(CONFIG.get('rss_items_per_page', 20))
150 rss_items_per_page = safe_int(CONFIG.get('rss_items_per_page', 20))
151 for cs in reversed(list(c.db_repo_scm_instance[-rss_items_per_page:])):
151 for cs in reversed(list(c.db_repo_scm_instance[-rss_items_per_page:])):
152 feed.add_item(title=self._get_title(cs),
152 feed.add_item(title=self._get_title(cs),
153 link=h.canonical_url('changeset_home', repo_name=repo_name,
153 link=h.canonical_url('changeset_home', repo_name=repo_name,
154 revision=cs.raw_id),
154 revision=cs.raw_id),
155 author_name=cs.author,
155 author_name=cs.author,
156 description=''.join(self.__get_desc(cs)),
156 description=''.join(self.__get_desc(cs)),
157 pubdate=cs.date,
157 pubdate=cs.date,
158 )
158 )
159
159
160 response.content_type = feed.mime_type
160 response.content_type = feed.mime_type
161 return feed.writeString('utf-8')
161 return feed.writeString('utf-8')
162
162
163 kind = 'RSS'
163 kind = 'RSS'
164 valid = CacheInvalidation.test_and_set_valid(repo_name, kind)
164 valid = CacheInvalidation.test_and_set_valid(repo_name, kind)
165 if not valid:
165 if not valid:
166 region_invalidate(_get_feed_from_cache, None, '_get_feed_from_cache', repo_name, kind)
166 region_invalidate(_get_feed_from_cache, None, '_get_feed_from_cache', repo_name, kind)
167 return _get_feed_from_cache(repo_name, kind)
167 return _get_feed_from_cache(repo_name, kind)
@@ -1,782 +1,782 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.files
15 kallithea.controllers.files
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Files controller for Kallithea
18 Files controller for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 21, 2010
22 :created_on: Apr 21, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import os
28 import os
29 import posixpath
29 import posixpath
30 import logging
30 import logging
31 import traceback
31 import traceback
32 import tempfile
32 import tempfile
33 import shutil
33 import shutil
34
34
35 from tg import request, response, tmpl_context as c
35 from tg import request, response, tmpl_context as c
36 from tg.i18n import ugettext as _
36 from tg.i18n import ugettext as _
37 from webob.exc import HTTPFound
37 from webob.exc import HTTPFound
38
38
39 from kallithea.config.routing import url
39 from kallithea.config.routing import url
40 from kallithea.lib.utils import action_logger
40 from kallithea.lib.utils import action_logger
41 from kallithea.lib import diffs
41 from kallithea.lib import diffs
42 from kallithea.lib import helpers as h
42 from kallithea.lib import helpers as h
43
43
44 from kallithea.lib.compat import OrderedDict
44 from kallithea.lib.compat import OrderedDict
45 from kallithea.lib.utils2 import convert_line_endings, detect_mode, safe_str, \
45 from kallithea.lib.utils2 import convert_line_endings, detect_mode, safe_str, \
46 str2bool, safe_int
46 str2bool, safe_int
47 from kallithea.lib.auth import LoginRequired, HasRepoPermissionLevelDecorator
47 from kallithea.lib.auth import LoginRequired, HasRepoPermissionLevelDecorator
48 from kallithea.lib.base import BaseRepoController, render, jsonify
48 from kallithea.lib.base import BaseRepoController, render, jsonify
49 from kallithea.lib.vcs.backends.base import EmptyChangeset
49 from kallithea.lib.vcs.backends.base import EmptyChangeset
50 from kallithea.lib.vcs.conf import settings
50 from kallithea.lib.vcs.conf import settings
51 from kallithea.lib.vcs.exceptions import RepositoryError, \
51 from kallithea.lib.vcs.exceptions import RepositoryError, \
52 ChangesetDoesNotExistError, EmptyRepositoryError, \
52 ChangesetDoesNotExistError, EmptyRepositoryError, \
53 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError, \
53 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError, \
54 NodeDoesNotExistError, ChangesetError, NodeError
54 NodeDoesNotExistError, ChangesetError, NodeError
55 from kallithea.lib.vcs.nodes import FileNode
55 from kallithea.lib.vcs.nodes import FileNode
56
56
57 from kallithea.model.repo import RepoModel
57 from kallithea.model.repo import RepoModel
58 from kallithea.model.scm import ScmModel
58 from kallithea.model.scm import ScmModel
59 from kallithea.model.db import Repository
59 from kallithea.model.db import Repository
60
60
61 from kallithea.controllers.changeset import anchor_url, _ignorews_url, \
61 from kallithea.controllers.changeset import anchor_url, _ignorews_url, \
62 _context_url, get_line_ctx, get_ignore_ws
62 _context_url, get_line_ctx, get_ignore_ws
63 from webob.exc import HTTPNotFound
63 from webob.exc import HTTPNotFound
64 from kallithea.lib.exceptions import NonRelativePathError
64 from kallithea.lib.exceptions import NonRelativePathError
65
65
66
66
67 log = logging.getLogger(__name__)
67 log = logging.getLogger(__name__)
68
68
69
69
70 class FilesController(BaseRepoController):
70 class FilesController(BaseRepoController):
71
71
72 def _before(self, *args, **kwargs):
72 def _before(self, *args, **kwargs):
73 super(FilesController, self)._before(*args, **kwargs)
73 super(FilesController, self)._before(*args, **kwargs)
74
74
75 def __get_cs(self, rev, silent_empty=False):
75 def __get_cs(self, rev, silent_empty=False):
76 """
76 """
77 Safe way to get changeset if error occur it redirects to tip with
77 Safe way to get changeset if error occur it redirects to tip with
78 proper message
78 proper message
79
79
80 :param rev: revision to fetch
80 :param rev: revision to fetch
81 :silent_empty: return None if repository is empty
81 :silent_empty: return None if repository is empty
82 """
82 """
83
83
84 try:
84 try:
85 return c.db_repo_scm_instance.get_changeset(rev)
85 return c.db_repo_scm_instance.get_changeset(rev)
86 except EmptyRepositoryError as e:
86 except EmptyRepositoryError as e:
87 if silent_empty:
87 if silent_empty:
88 return None
88 return None
89 url_ = url('files_add_home',
89 url_ = url('files_add_home',
90 repo_name=c.repo_name,
90 repo_name=c.repo_name,
91 revision=0, f_path='', anchor='edit')
91 revision=0, f_path='', anchor='edit')
92 add_new = h.link_to(_('Click here to add new file'), url_, class_="alert-link")
92 add_new = h.link_to(_('Click here to add new file'), url_, class_="alert-link")
93 h.flash(h.literal(_('There are no files yet. %s') % add_new),
93 h.flash(h.literal(_('There are no files yet. %s') % add_new),
94 category='warning')
94 category='warning')
95 raise HTTPNotFound()
95 raise HTTPNotFound()
96 except (ChangesetDoesNotExistError, LookupError):
96 except (ChangesetDoesNotExistError, LookupError):
97 msg = _('Such revision does not exist for this repository')
97 msg = _('Such revision does not exist for this repository')
98 h.flash(msg, category='error')
98 h.flash(msg, category='error')
99 raise HTTPNotFound()
99 raise HTTPNotFound()
100 except RepositoryError as e:
100 except RepositoryError as e:
101 h.flash(safe_str(e), category='error')
101 h.flash(safe_str(e), category='error')
102 raise HTTPNotFound()
102 raise HTTPNotFound()
103
103
104 def __get_filenode(self, cs, path):
104 def __get_filenode(self, cs, path):
105 """
105 """
106 Returns file_node or raise HTTP error.
106 Returns file_node or raise HTTP error.
107
107
108 :param cs: given changeset
108 :param cs: given changeset
109 :param path: path to lookup
109 :param path: path to lookup
110 """
110 """
111
111
112 try:
112 try:
113 file_node = cs.get_node(path)
113 file_node = cs.get_node(path)
114 if file_node.is_dir():
114 if file_node.is_dir():
115 raise RepositoryError('given path is a directory')
115 raise RepositoryError('given path is a directory')
116 except ChangesetDoesNotExistError:
116 except ChangesetDoesNotExistError:
117 msg = _('Such revision does not exist for this repository')
117 msg = _('Such revision does not exist for this repository')
118 h.flash(msg, category='error')
118 h.flash(msg, category='error')
119 raise HTTPNotFound()
119 raise HTTPNotFound()
120 except RepositoryError as e:
120 except RepositoryError as e:
121 h.flash(safe_str(e), category='error')
121 h.flash(safe_str(e), category='error')
122 raise HTTPNotFound()
122 raise HTTPNotFound()
123
123
124 return file_node
124 return file_node
125
125
126 @LoginRequired(allow_default_user=True)
126 @LoginRequired(allow_default_user=True)
127 @HasRepoPermissionLevelDecorator('read')
127 @HasRepoPermissionLevelDecorator('read')
128 def index(self, repo_name, revision, f_path, annotate=False):
128 def index(self, repo_name, revision, f_path, annotate=False):
129 # redirect to given revision from form if given
129 # redirect to given revision from form if given
130 post_revision = request.POST.get('at_rev', None)
130 post_revision = request.POST.get('at_rev', None)
131 if post_revision:
131 if post_revision:
132 cs = self.__get_cs(post_revision) # FIXME - unused!
132 cs = self.__get_cs(post_revision) # FIXME - unused!
133
133
134 c.revision = revision
134 c.revision = revision
135 c.changeset = self.__get_cs(revision)
135 c.changeset = self.__get_cs(revision)
136 c.branch = request.GET.get('branch', None)
136 c.branch = request.GET.get('branch', None)
137 c.f_path = f_path
137 c.f_path = f_path
138 c.annotate = annotate
138 c.annotate = annotate
139 cur_rev = c.changeset.revision
139 cur_rev = c.changeset.revision
140 # used in files_source.html:
140 # used in files_source.html:
141 c.cut_off_limit = self.cut_off_limit
141 c.cut_off_limit = self.cut_off_limit
142 c.fulldiff = request.GET.get('fulldiff')
142 c.fulldiff = request.GET.get('fulldiff')
143
143
144 # prev link
144 # prev link
145 try:
145 try:
146 prev_rev = c.db_repo_scm_instance.get_changeset(cur_rev).prev(c.branch)
146 prev_rev = c.db_repo_scm_instance.get_changeset(cur_rev).prev(c.branch)
147 c.url_prev = url('files_home', repo_name=c.repo_name,
147 c.url_prev = url('files_home', repo_name=c.repo_name,
148 revision=prev_rev.raw_id, f_path=f_path)
148 revision=prev_rev.raw_id, f_path=f_path)
149 if c.branch:
149 if c.branch:
150 c.url_prev += '?branch=%s' % c.branch
150 c.url_prev += '?branch=%s' % c.branch
151 except (ChangesetDoesNotExistError, VCSError):
151 except (ChangesetDoesNotExistError, VCSError):
152 c.url_prev = '#'
152 c.url_prev = '#'
153
153
154 # next link
154 # next link
155 try:
155 try:
156 next_rev = c.db_repo_scm_instance.get_changeset(cur_rev).next(c.branch)
156 next_rev = c.db_repo_scm_instance.get_changeset(cur_rev).next(c.branch)
157 c.url_next = url('files_home', repo_name=c.repo_name,
157 c.url_next = url('files_home', repo_name=c.repo_name,
158 revision=next_rev.raw_id, f_path=f_path)
158 revision=next_rev.raw_id, f_path=f_path)
159 if c.branch:
159 if c.branch:
160 c.url_next += '?branch=%s' % c.branch
160 c.url_next += '?branch=%s' % c.branch
161 except (ChangesetDoesNotExistError, VCSError):
161 except (ChangesetDoesNotExistError, VCSError):
162 c.url_next = '#'
162 c.url_next = '#'
163
163
164 # files or dirs
164 # files or dirs
165 try:
165 try:
166 c.file = c.changeset.get_node(f_path)
166 c.file = c.changeset.get_node(f_path)
167
167
168 if c.file.is_file():
168 if c.file.is_file():
169 c.load_full_history = False
169 c.load_full_history = False
170 # determine if we're on branch head
170 # determine if we're on branch head
171 _branches = c.db_repo_scm_instance.branches
171 _branches = c.db_repo_scm_instance.branches
172 c.on_branch_head = revision in _branches.keys() + _branches.values()
172 c.on_branch_head = revision in _branches.keys() + _branches.values()
173 _hist = []
173 _hist = []
174 c.file_history = []
174 c.file_history = []
175 if c.load_full_history:
175 if c.load_full_history:
176 c.file_history, _hist = self._get_node_history(c.changeset, f_path)
176 c.file_history, _hist = self._get_node_history(c.changeset, f_path)
177
177
178 c.authors = []
178 c.authors = []
179 for a in set([x.author for x in _hist]):
179 for a in set([x.author for x in _hist]):
180 c.authors.append((h.email(a), h.person(a)))
180 c.authors.append((h.email(a), h.person(a)))
181 else:
181 else:
182 c.authors = c.file_history = []
182 c.authors = c.file_history = []
183 except RepositoryError as e:
183 except RepositoryError as e:
184 h.flash(safe_str(e), category='error')
184 h.flash(safe_str(e), category='error')
185 raise HTTPNotFound()
185 raise HTTPNotFound()
186
186
187 if request.environ.get('HTTP_X_PARTIAL_XHR'):
187 if request.environ.get('HTTP_X_PARTIAL_XHR'):
188 return render('files/files_ypjax.html')
188 return render('files/files_ypjax.html')
189
189
190 # TODO: tags and bookmarks?
190 # TODO: tags and bookmarks?
191 c.revision_options = [(c.changeset.raw_id,
191 c.revision_options = [(c.changeset.raw_id,
192 _('%s at %s') % (c.changeset.branch, h.short_id(c.changeset.raw_id)))] + \
192 _('%s at %s') % (b, h.short_id(c.changeset.raw_id))) for b in c.changeset.branches] + \
193 [(n, b) for b, n in c.db_repo_scm_instance.branches.items()]
193 [(n, b) for b, n in c.db_repo_scm_instance.branches.items()]
194 if c.db_repo_scm_instance.closed_branches:
194 if c.db_repo_scm_instance.closed_branches:
195 prefix = _('(closed)') + ' '
195 prefix = _('(closed)') + ' '
196 c.revision_options += [('-', '-')] + \
196 c.revision_options += [('-', '-')] + \
197 [(n, prefix + b) for b, n in c.db_repo_scm_instance.closed_branches.items()]
197 [(n, prefix + b) for b, n in c.db_repo_scm_instance.closed_branches.items()]
198
198
199 return render('files/files.html')
199 return render('files/files.html')
200
200
201 @LoginRequired(allow_default_user=True)
201 @LoginRequired(allow_default_user=True)
202 @HasRepoPermissionLevelDecorator('read')
202 @HasRepoPermissionLevelDecorator('read')
203 @jsonify
203 @jsonify
204 def history(self, repo_name, revision, f_path):
204 def history(self, repo_name, revision, f_path):
205 changeset = self.__get_cs(revision)
205 changeset = self.__get_cs(revision)
206 _file = changeset.get_node(f_path)
206 _file = changeset.get_node(f_path)
207 if _file.is_file():
207 if _file.is_file():
208 file_history, _hist = self._get_node_history(changeset, f_path)
208 file_history, _hist = self._get_node_history(changeset, f_path)
209
209
210 res = []
210 res = []
211 for obj in file_history:
211 for obj in file_history:
212 res.append({
212 res.append({
213 'text': obj[1],
213 'text': obj[1],
214 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
214 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
215 })
215 })
216
216
217 data = {
217 data = {
218 'more': False,
218 'more': False,
219 'results': res
219 'results': res
220 }
220 }
221 return data
221 return data
222
222
223 @LoginRequired(allow_default_user=True)
223 @LoginRequired(allow_default_user=True)
224 @HasRepoPermissionLevelDecorator('read')
224 @HasRepoPermissionLevelDecorator('read')
225 def authors(self, repo_name, revision, f_path):
225 def authors(self, repo_name, revision, f_path):
226 changeset = self.__get_cs(revision)
226 changeset = self.__get_cs(revision)
227 _file = changeset.get_node(f_path)
227 _file = changeset.get_node(f_path)
228 if _file.is_file():
228 if _file.is_file():
229 file_history, _hist = self._get_node_history(changeset, f_path)
229 file_history, _hist = self._get_node_history(changeset, f_path)
230 c.authors = []
230 c.authors = []
231 for a in set([x.author for x in _hist]):
231 for a in set([x.author for x in _hist]):
232 c.authors.append((h.email(a), h.person(a)))
232 c.authors.append((h.email(a), h.person(a)))
233 return render('files/files_history_box.html')
233 return render('files/files_history_box.html')
234
234
235 @LoginRequired(allow_default_user=True)
235 @LoginRequired(allow_default_user=True)
236 @HasRepoPermissionLevelDecorator('read')
236 @HasRepoPermissionLevelDecorator('read')
237 def rawfile(self, repo_name, revision, f_path):
237 def rawfile(self, repo_name, revision, f_path):
238 cs = self.__get_cs(revision)
238 cs = self.__get_cs(revision)
239 file_node = self.__get_filenode(cs, f_path)
239 file_node = self.__get_filenode(cs, f_path)
240
240
241 response.content_disposition = 'attachment; filename=%s' % \
241 response.content_disposition = 'attachment; filename=%s' % \
242 safe_str(f_path.split(Repository.url_sep())[-1])
242 safe_str(f_path.split(Repository.url_sep())[-1])
243
243
244 response.content_type = file_node.mimetype
244 response.content_type = file_node.mimetype
245 return file_node.content
245 return file_node.content
246
246
247 @LoginRequired(allow_default_user=True)
247 @LoginRequired(allow_default_user=True)
248 @HasRepoPermissionLevelDecorator('read')
248 @HasRepoPermissionLevelDecorator('read')
249 def raw(self, repo_name, revision, f_path):
249 def raw(self, repo_name, revision, f_path):
250 cs = self.__get_cs(revision)
250 cs = self.__get_cs(revision)
251 file_node = self.__get_filenode(cs, f_path)
251 file_node = self.__get_filenode(cs, f_path)
252
252
253 raw_mimetype_mapping = {
253 raw_mimetype_mapping = {
254 # map original mimetype to a mimetype used for "show as raw"
254 # map original mimetype to a mimetype used for "show as raw"
255 # you can also provide a content-disposition to override the
255 # you can also provide a content-disposition to override the
256 # default "attachment" disposition.
256 # default "attachment" disposition.
257 # orig_type: (new_type, new_dispo)
257 # orig_type: (new_type, new_dispo)
258
258
259 # show images inline:
259 # show images inline:
260 'image/x-icon': ('image/x-icon', 'inline'),
260 'image/x-icon': ('image/x-icon', 'inline'),
261 'image/png': ('image/png', 'inline'),
261 'image/png': ('image/png', 'inline'),
262 'image/gif': ('image/gif', 'inline'),
262 'image/gif': ('image/gif', 'inline'),
263 'image/jpeg': ('image/jpeg', 'inline'),
263 'image/jpeg': ('image/jpeg', 'inline'),
264 'image/svg+xml': ('image/svg+xml', 'inline'),
264 'image/svg+xml': ('image/svg+xml', 'inline'),
265 }
265 }
266
266
267 mimetype = file_node.mimetype
267 mimetype = file_node.mimetype
268 try:
268 try:
269 mimetype, dispo = raw_mimetype_mapping[mimetype]
269 mimetype, dispo = raw_mimetype_mapping[mimetype]
270 except KeyError:
270 except KeyError:
271 # we don't know anything special about this, handle it safely
271 # we don't know anything special about this, handle it safely
272 if file_node.is_binary:
272 if file_node.is_binary:
273 # do same as download raw for binary files
273 # do same as download raw for binary files
274 mimetype, dispo = 'application/octet-stream', 'attachment'
274 mimetype, dispo = 'application/octet-stream', 'attachment'
275 else:
275 else:
276 # do not just use the original mimetype, but force text/plain,
276 # do not just use the original mimetype, but force text/plain,
277 # otherwise it would serve text/html and that might be unsafe.
277 # otherwise it would serve text/html and that might be unsafe.
278 # Note: underlying vcs library fakes text/plain mimetype if the
278 # Note: underlying vcs library fakes text/plain mimetype if the
279 # mimetype can not be determined and it thinks it is not
279 # mimetype can not be determined and it thinks it is not
280 # binary.This might lead to erroneous text display in some
280 # binary.This might lead to erroneous text display in some
281 # cases, but helps in other cases, like with text files
281 # cases, but helps in other cases, like with text files
282 # without extension.
282 # without extension.
283 mimetype, dispo = 'text/plain', 'inline'
283 mimetype, dispo = 'text/plain', 'inline'
284
284
285 if dispo == 'attachment':
285 if dispo == 'attachment':
286 dispo = 'attachment; filename=%s' % \
286 dispo = 'attachment; filename=%s' % \
287 safe_str(f_path.split(os.sep)[-1])
287 safe_str(f_path.split(os.sep)[-1])
288
288
289 response.content_disposition = dispo
289 response.content_disposition = dispo
290 response.content_type = mimetype
290 response.content_type = mimetype
291 return file_node.content
291 return file_node.content
292
292
293 @LoginRequired()
293 @LoginRequired()
294 @HasRepoPermissionLevelDecorator('write')
294 @HasRepoPermissionLevelDecorator('write')
295 def delete(self, repo_name, revision, f_path):
295 def delete(self, repo_name, revision, f_path):
296 repo = c.db_repo
296 repo = c.db_repo
297 if repo.enable_locking and repo.locked[0]:
297 if repo.enable_locking and repo.locked[0]:
298 h.flash(_('This repository has been locked by %s on %s')
298 h.flash(_('This repository has been locked by %s on %s')
299 % (h.person_by_id(repo.locked[0]),
299 % (h.person_by_id(repo.locked[0]),
300 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
300 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
301 'warning')
301 'warning')
302 raise HTTPFound(location=h.url('files_home',
302 raise HTTPFound(location=h.url('files_home',
303 repo_name=repo_name, revision='tip'))
303 repo_name=repo_name, revision='tip'))
304
304
305 # check if revision is a branch identifier- basically we cannot
305 # check if revision is a branch identifier- basically we cannot
306 # create multiple heads via file editing
306 # create multiple heads via file editing
307 _branches = repo.scm_instance.branches
307 _branches = repo.scm_instance.branches
308 # check if revision is a branch name or branch hash
308 # check if revision is a branch name or branch hash
309 if revision not in _branches.keys() + _branches.values():
309 if revision not in _branches.keys() + _branches.values():
310 h.flash(_('You can only delete files with revision '
310 h.flash(_('You can only delete files with revision '
311 'being a valid branch'), category='warning')
311 'being a valid branch'), category='warning')
312 raise HTTPFound(location=h.url('files_home',
312 raise HTTPFound(location=h.url('files_home',
313 repo_name=repo_name, revision='tip',
313 repo_name=repo_name, revision='tip',
314 f_path=f_path))
314 f_path=f_path))
315
315
316 r_post = request.POST
316 r_post = request.POST
317
317
318 c.cs = self.__get_cs(revision)
318 c.cs = self.__get_cs(revision)
319 c.file = self.__get_filenode(c.cs, f_path)
319 c.file = self.__get_filenode(c.cs, f_path)
320
320
321 c.default_message = _('Deleted file %s via Kallithea') % (f_path)
321 c.default_message = _('Deleted file %s via Kallithea') % (f_path)
322 c.f_path = f_path
322 c.f_path = f_path
323 node_path = f_path
323 node_path = f_path
324 author = request.authuser.full_contact
324 author = request.authuser.full_contact
325
325
326 if r_post:
326 if r_post:
327 message = r_post.get('message') or c.default_message
327 message = r_post.get('message') or c.default_message
328
328
329 try:
329 try:
330 nodes = {
330 nodes = {
331 node_path: {
331 node_path: {
332 'content': ''
332 'content': ''
333 }
333 }
334 }
334 }
335 self.scm_model.delete_nodes(
335 self.scm_model.delete_nodes(
336 user=request.authuser.user_id, repo=c.db_repo,
336 user=request.authuser.user_id, repo=c.db_repo,
337 message=message,
337 message=message,
338 nodes=nodes,
338 nodes=nodes,
339 parent_cs=c.cs,
339 parent_cs=c.cs,
340 author=author,
340 author=author,
341 )
341 )
342
342
343 h.flash(_('Successfully deleted file %s') % f_path,
343 h.flash(_('Successfully deleted file %s') % f_path,
344 category='success')
344 category='success')
345 except Exception:
345 except Exception:
346 log.error(traceback.format_exc())
346 log.error(traceback.format_exc())
347 h.flash(_('Error occurred during commit'), category='error')
347 h.flash(_('Error occurred during commit'), category='error')
348 raise HTTPFound(location=url('changeset_home',
348 raise HTTPFound(location=url('changeset_home',
349 repo_name=c.repo_name, revision='tip'))
349 repo_name=c.repo_name, revision='tip'))
350
350
351 return render('files/files_delete.html')
351 return render('files/files_delete.html')
352
352
353 @LoginRequired()
353 @LoginRequired()
354 @HasRepoPermissionLevelDecorator('write')
354 @HasRepoPermissionLevelDecorator('write')
355 def edit(self, repo_name, revision, f_path):
355 def edit(self, repo_name, revision, f_path):
356 repo = c.db_repo
356 repo = c.db_repo
357 if repo.enable_locking and repo.locked[0]:
357 if repo.enable_locking and repo.locked[0]:
358 h.flash(_('This repository has been locked by %s on %s')
358 h.flash(_('This repository has been locked by %s on %s')
359 % (h.person_by_id(repo.locked[0]),
359 % (h.person_by_id(repo.locked[0]),
360 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
360 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
361 'warning')
361 'warning')
362 raise HTTPFound(location=h.url('files_home',
362 raise HTTPFound(location=h.url('files_home',
363 repo_name=repo_name, revision='tip'))
363 repo_name=repo_name, revision='tip'))
364
364
365 # check if revision is a branch identifier- basically we cannot
365 # check if revision is a branch identifier- basically we cannot
366 # create multiple heads via file editing
366 # create multiple heads via file editing
367 _branches = repo.scm_instance.branches
367 _branches = repo.scm_instance.branches
368 # check if revision is a branch name or branch hash
368 # check if revision is a branch name or branch hash
369 if revision not in _branches.keys() + _branches.values():
369 if revision not in _branches.keys() + _branches.values():
370 h.flash(_('You can only edit files with revision '
370 h.flash(_('You can only edit files with revision '
371 'being a valid branch'), category='warning')
371 'being a valid branch'), category='warning')
372 raise HTTPFound(location=h.url('files_home',
372 raise HTTPFound(location=h.url('files_home',
373 repo_name=repo_name, revision='tip',
373 repo_name=repo_name, revision='tip',
374 f_path=f_path))
374 f_path=f_path))
375
375
376 r_post = request.POST
376 r_post = request.POST
377
377
378 c.cs = self.__get_cs(revision)
378 c.cs = self.__get_cs(revision)
379 c.file = self.__get_filenode(c.cs, f_path)
379 c.file = self.__get_filenode(c.cs, f_path)
380
380
381 if c.file.is_binary:
381 if c.file.is_binary:
382 raise HTTPFound(location=url('files_home', repo_name=c.repo_name,
382 raise HTTPFound(location=url('files_home', repo_name=c.repo_name,
383 revision=c.cs.raw_id, f_path=f_path))
383 revision=c.cs.raw_id, f_path=f_path))
384 c.default_message = _('Edited file %s via Kallithea') % (f_path)
384 c.default_message = _('Edited file %s via Kallithea') % (f_path)
385 c.f_path = f_path
385 c.f_path = f_path
386
386
387 if r_post:
387 if r_post:
388
388
389 old_content = c.file.content
389 old_content = c.file.content
390 sl = old_content.splitlines(1)
390 sl = old_content.splitlines(1)
391 first_line = sl[0] if sl else ''
391 first_line = sl[0] if sl else ''
392 # modes: 0 - Unix, 1 - Mac, 2 - DOS
392 # modes: 0 - Unix, 1 - Mac, 2 - DOS
393 mode = detect_mode(first_line, 0)
393 mode = detect_mode(first_line, 0)
394 content = convert_line_endings(r_post.get('content', ''), mode)
394 content = convert_line_endings(r_post.get('content', ''), mode)
395
395
396 message = r_post.get('message') or c.default_message
396 message = r_post.get('message') or c.default_message
397 author = request.authuser.full_contact
397 author = request.authuser.full_contact
398
398
399 if content == old_content:
399 if content == old_content:
400 h.flash(_('No changes'), category='warning')
400 h.flash(_('No changes'), category='warning')
401 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
401 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
402 revision='tip'))
402 revision='tip'))
403 try:
403 try:
404 self.scm_model.commit_change(repo=c.db_repo_scm_instance,
404 self.scm_model.commit_change(repo=c.db_repo_scm_instance,
405 repo_name=repo_name, cs=c.cs,
405 repo_name=repo_name, cs=c.cs,
406 user=request.authuser.user_id,
406 user=request.authuser.user_id,
407 author=author, message=message,
407 author=author, message=message,
408 content=content, f_path=f_path)
408 content=content, f_path=f_path)
409 h.flash(_('Successfully committed to %s') % f_path,
409 h.flash(_('Successfully committed to %s') % f_path,
410 category='success')
410 category='success')
411 except Exception:
411 except Exception:
412 log.error(traceback.format_exc())
412 log.error(traceback.format_exc())
413 h.flash(_('Error occurred during commit'), category='error')
413 h.flash(_('Error occurred during commit'), category='error')
414 raise HTTPFound(location=url('changeset_home',
414 raise HTTPFound(location=url('changeset_home',
415 repo_name=c.repo_name, revision='tip'))
415 repo_name=c.repo_name, revision='tip'))
416
416
417 return render('files/files_edit.html')
417 return render('files/files_edit.html')
418
418
419 @LoginRequired()
419 @LoginRequired()
420 @HasRepoPermissionLevelDecorator('write')
420 @HasRepoPermissionLevelDecorator('write')
421 def add(self, repo_name, revision, f_path):
421 def add(self, repo_name, revision, f_path):
422
422
423 repo = c.db_repo
423 repo = c.db_repo
424 if repo.enable_locking and repo.locked[0]:
424 if repo.enable_locking and repo.locked[0]:
425 h.flash(_('This repository has been locked by %s on %s')
425 h.flash(_('This repository has been locked by %s on %s')
426 % (h.person_by_id(repo.locked[0]),
426 % (h.person_by_id(repo.locked[0]),
427 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
427 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
428 'warning')
428 'warning')
429 raise HTTPFound(location=h.url('files_home',
429 raise HTTPFound(location=h.url('files_home',
430 repo_name=repo_name, revision='tip'))
430 repo_name=repo_name, revision='tip'))
431
431
432 r_post = request.POST
432 r_post = request.POST
433 c.cs = self.__get_cs(revision, silent_empty=True)
433 c.cs = self.__get_cs(revision, silent_empty=True)
434 if c.cs is None:
434 if c.cs is None:
435 c.cs = EmptyChangeset(alias=c.db_repo_scm_instance.alias)
435 c.cs = EmptyChangeset(alias=c.db_repo_scm_instance.alias)
436 c.default_message = (_('Added file via Kallithea'))
436 c.default_message = (_('Added file via Kallithea'))
437 c.f_path = f_path
437 c.f_path = f_path
438
438
439 if r_post:
439 if r_post:
440 unix_mode = 0
440 unix_mode = 0
441 content = convert_line_endings(r_post.get('content', ''), unix_mode)
441 content = convert_line_endings(r_post.get('content', ''), unix_mode)
442
442
443 message = r_post.get('message') or c.default_message
443 message = r_post.get('message') or c.default_message
444 filename = r_post.get('filename')
444 filename = r_post.get('filename')
445 location = r_post.get('location', '')
445 location = r_post.get('location', '')
446 file_obj = r_post.get('upload_file', None)
446 file_obj = r_post.get('upload_file', None)
447
447
448 if file_obj is not None and hasattr(file_obj, 'filename'):
448 if file_obj is not None and hasattr(file_obj, 'filename'):
449 filename = file_obj.filename
449 filename = file_obj.filename
450 content = file_obj.file
450 content = file_obj.file
451
451
452 if hasattr(content, 'file'):
452 if hasattr(content, 'file'):
453 # non posix systems store real file under file attr
453 # non posix systems store real file under file attr
454 content = content.file
454 content = content.file
455
455
456 if not content:
456 if not content:
457 h.flash(_('No content'), category='warning')
457 h.flash(_('No content'), category='warning')
458 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
458 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
459 revision='tip'))
459 revision='tip'))
460 if not filename:
460 if not filename:
461 h.flash(_('No filename'), category='warning')
461 h.flash(_('No filename'), category='warning')
462 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
462 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
463 revision='tip'))
463 revision='tip'))
464 # strip all crap out of file, just leave the basename
464 # strip all crap out of file, just leave the basename
465 filename = os.path.basename(filename)
465 filename = os.path.basename(filename)
466 node_path = posixpath.join(location, filename)
466 node_path = posixpath.join(location, filename)
467 author = request.authuser.full_contact
467 author = request.authuser.full_contact
468
468
469 try:
469 try:
470 nodes = {
470 nodes = {
471 node_path: {
471 node_path: {
472 'content': content
472 'content': content
473 }
473 }
474 }
474 }
475 self.scm_model.create_nodes(
475 self.scm_model.create_nodes(
476 user=request.authuser.user_id, repo=c.db_repo,
476 user=request.authuser.user_id, repo=c.db_repo,
477 message=message,
477 message=message,
478 nodes=nodes,
478 nodes=nodes,
479 parent_cs=c.cs,
479 parent_cs=c.cs,
480 author=author,
480 author=author,
481 )
481 )
482
482
483 h.flash(_('Successfully committed to %s') % node_path,
483 h.flash(_('Successfully committed to %s') % node_path,
484 category='success')
484 category='success')
485 except NonRelativePathError as e:
485 except NonRelativePathError as e:
486 h.flash(_('Location must be relative path and must not '
486 h.flash(_('Location must be relative path and must not '
487 'contain .. in path'), category='warning')
487 'contain .. in path'), category='warning')
488 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
488 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
489 revision='tip'))
489 revision='tip'))
490 except (NodeError, NodeAlreadyExistsError) as e:
490 except (NodeError, NodeAlreadyExistsError) as e:
491 h.flash(_(e), category='error')
491 h.flash(_(e), category='error')
492 except Exception:
492 except Exception:
493 log.error(traceback.format_exc())
493 log.error(traceback.format_exc())
494 h.flash(_('Error occurred during commit'), category='error')
494 h.flash(_('Error occurred during commit'), category='error')
495 raise HTTPFound(location=url('changeset_home',
495 raise HTTPFound(location=url('changeset_home',
496 repo_name=c.repo_name, revision='tip'))
496 repo_name=c.repo_name, revision='tip'))
497
497
498 return render('files/files_add.html')
498 return render('files/files_add.html')
499
499
500 @LoginRequired(allow_default_user=True)
500 @LoginRequired(allow_default_user=True)
501 @HasRepoPermissionLevelDecorator('read')
501 @HasRepoPermissionLevelDecorator('read')
502 def archivefile(self, repo_name, fname):
502 def archivefile(self, repo_name, fname):
503 fileformat = None
503 fileformat = None
504 revision = None
504 revision = None
505 ext = None
505 ext = None
506 subrepos = request.GET.get('subrepos') == 'true'
506 subrepos = request.GET.get('subrepos') == 'true'
507
507
508 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
508 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
509 archive_spec = fname.split(ext_data[1])
509 archive_spec = fname.split(ext_data[1])
510 if len(archive_spec) == 2 and archive_spec[1] == '':
510 if len(archive_spec) == 2 and archive_spec[1] == '':
511 fileformat = a_type or ext_data[1]
511 fileformat = a_type or ext_data[1]
512 revision = archive_spec[0]
512 revision = archive_spec[0]
513 ext = ext_data[1]
513 ext = ext_data[1]
514
514
515 try:
515 try:
516 dbrepo = RepoModel().get_by_repo_name(repo_name)
516 dbrepo = RepoModel().get_by_repo_name(repo_name)
517 if not dbrepo.enable_downloads:
517 if not dbrepo.enable_downloads:
518 return _('Downloads disabled') # TODO: do something else?
518 return _('Downloads disabled') # TODO: do something else?
519
519
520 if c.db_repo_scm_instance.alias == 'hg':
520 if c.db_repo_scm_instance.alias == 'hg':
521 # patch and reset hooks section of UI config to not run any
521 # patch and reset hooks section of UI config to not run any
522 # hooks on fetching archives with subrepos
522 # hooks on fetching archives with subrepos
523 for k, v in c.db_repo_scm_instance._repo.ui.configitems('hooks'):
523 for k, v in c.db_repo_scm_instance._repo.ui.configitems('hooks'):
524 c.db_repo_scm_instance._repo.ui.setconfig('hooks', k, None)
524 c.db_repo_scm_instance._repo.ui.setconfig('hooks', k, None)
525
525
526 cs = c.db_repo_scm_instance.get_changeset(revision)
526 cs = c.db_repo_scm_instance.get_changeset(revision)
527 content_type = settings.ARCHIVE_SPECS[fileformat][0]
527 content_type = settings.ARCHIVE_SPECS[fileformat][0]
528 except ChangesetDoesNotExistError:
528 except ChangesetDoesNotExistError:
529 return _('Unknown revision %s') % revision
529 return _('Unknown revision %s') % revision
530 except EmptyRepositoryError:
530 except EmptyRepositoryError:
531 return _('Empty repository')
531 return _('Empty repository')
532 except (ImproperArchiveTypeError, KeyError):
532 except (ImproperArchiveTypeError, KeyError):
533 return _('Unknown archive type')
533 return _('Unknown archive type')
534
534
535 from kallithea import CONFIG
535 from kallithea import CONFIG
536 rev_name = cs.raw_id[:12]
536 rev_name = cs.raw_id[:12]
537 archive_name = '%s-%s%s' % (safe_str(repo_name.replace('/', '_')),
537 archive_name = '%s-%s%s' % (safe_str(repo_name.replace('/', '_')),
538 safe_str(rev_name), ext)
538 safe_str(rev_name), ext)
539
539
540 archive_path = None
540 archive_path = None
541 cached_archive_path = None
541 cached_archive_path = None
542 archive_cache_dir = CONFIG.get('archive_cache_dir')
542 archive_cache_dir = CONFIG.get('archive_cache_dir')
543 if archive_cache_dir and not subrepos: # TODO: subrepo caching?
543 if archive_cache_dir and not subrepos: # TODO: subrepo caching?
544 if not os.path.isdir(archive_cache_dir):
544 if not os.path.isdir(archive_cache_dir):
545 os.makedirs(archive_cache_dir)
545 os.makedirs(archive_cache_dir)
546 cached_archive_path = os.path.join(archive_cache_dir, archive_name)
546 cached_archive_path = os.path.join(archive_cache_dir, archive_name)
547 if os.path.isfile(cached_archive_path):
547 if os.path.isfile(cached_archive_path):
548 log.debug('Found cached archive in %s', cached_archive_path)
548 log.debug('Found cached archive in %s', cached_archive_path)
549 archive_path = cached_archive_path
549 archive_path = cached_archive_path
550 else:
550 else:
551 log.debug('Archive %s is not yet cached', archive_name)
551 log.debug('Archive %s is not yet cached', archive_name)
552
552
553 if archive_path is None:
553 if archive_path is None:
554 # generate new archive
554 # generate new archive
555 fd, archive_path = tempfile.mkstemp()
555 fd, archive_path = tempfile.mkstemp()
556 log.debug('Creating new temp archive in %s', archive_path)
556 log.debug('Creating new temp archive in %s', archive_path)
557 with os.fdopen(fd, 'wb') as stream:
557 with os.fdopen(fd, 'wb') as stream:
558 cs.fill_archive(stream=stream, kind=fileformat, subrepos=subrepos)
558 cs.fill_archive(stream=stream, kind=fileformat, subrepos=subrepos)
559 # stream (and thus fd) has been closed by cs.fill_archive
559 # stream (and thus fd) has been closed by cs.fill_archive
560 if cached_archive_path is not None:
560 if cached_archive_path is not None:
561 # we generated the archive - move it to cache
561 # we generated the archive - move it to cache
562 log.debug('Storing new archive in %s', cached_archive_path)
562 log.debug('Storing new archive in %s', cached_archive_path)
563 shutil.move(archive_path, cached_archive_path)
563 shutil.move(archive_path, cached_archive_path)
564 archive_path = cached_archive_path
564 archive_path = cached_archive_path
565
565
566 def get_chunked_archive(archive_path):
566 def get_chunked_archive(archive_path):
567 stream = open(archive_path, 'rb')
567 stream = open(archive_path, 'rb')
568 while True:
568 while True:
569 data = stream.read(16 * 1024)
569 data = stream.read(16 * 1024)
570 if not data:
570 if not data:
571 break
571 break
572 yield data
572 yield data
573 stream.close()
573 stream.close()
574 if archive_path != cached_archive_path:
574 if archive_path != cached_archive_path:
575 log.debug('Destroying temp archive %s', archive_path)
575 log.debug('Destroying temp archive %s', archive_path)
576 os.remove(archive_path)
576 os.remove(archive_path)
577
577
578 action_logger(user=request.authuser,
578 action_logger(user=request.authuser,
579 action='user_downloaded_archive:%s' % (archive_name),
579 action='user_downloaded_archive:%s' % (archive_name),
580 repo=repo_name, ipaddr=request.ip_addr, commit=True)
580 repo=repo_name, ipaddr=request.ip_addr, commit=True)
581
581
582 response.content_disposition = str('attachment; filename=%s' % (archive_name))
582 response.content_disposition = str('attachment; filename=%s' % (archive_name))
583 response.content_type = str(content_type)
583 response.content_type = str(content_type)
584 return get_chunked_archive(archive_path)
584 return get_chunked_archive(archive_path)
585
585
586 @LoginRequired(allow_default_user=True)
586 @LoginRequired(allow_default_user=True)
587 @HasRepoPermissionLevelDecorator('read')
587 @HasRepoPermissionLevelDecorator('read')
588 def diff(self, repo_name, f_path):
588 def diff(self, repo_name, f_path):
589 ignore_whitespace = request.GET.get('ignorews') == '1'
589 ignore_whitespace = request.GET.get('ignorews') == '1'
590 line_context = safe_int(request.GET.get('context'), 3)
590 line_context = safe_int(request.GET.get('context'), 3)
591 diff2 = request.GET.get('diff2', '')
591 diff2 = request.GET.get('diff2', '')
592 diff1 = request.GET.get('diff1', '') or diff2
592 diff1 = request.GET.get('diff1', '') or diff2
593 c.action = request.GET.get('diff')
593 c.action = request.GET.get('diff')
594 c.no_changes = diff1 == diff2
594 c.no_changes = diff1 == diff2
595 c.f_path = f_path
595 c.f_path = f_path
596 c.big_diff = False
596 c.big_diff = False
597 fulldiff = request.GET.get('fulldiff')
597 fulldiff = request.GET.get('fulldiff')
598 c.anchor_url = anchor_url
598 c.anchor_url = anchor_url
599 c.ignorews_url = _ignorews_url
599 c.ignorews_url = _ignorews_url
600 c.context_url = _context_url
600 c.context_url = _context_url
601 c.changes = OrderedDict()
601 c.changes = OrderedDict()
602 c.changes[diff2] = []
602 c.changes[diff2] = []
603
603
604 # special case if we want a show rev only, it's impl here
604 # special case if we want a show rev only, it's impl here
605 # to reduce JS and callbacks
605 # to reduce JS and callbacks
606
606
607 if request.GET.get('show_rev'):
607 if request.GET.get('show_rev'):
608 if str2bool(request.GET.get('annotate', 'False')):
608 if str2bool(request.GET.get('annotate', 'False')):
609 _url = url('files_annotate_home', repo_name=c.repo_name,
609 _url = url('files_annotate_home', repo_name=c.repo_name,
610 revision=diff1, f_path=c.f_path)
610 revision=diff1, f_path=c.f_path)
611 else:
611 else:
612 _url = url('files_home', repo_name=c.repo_name,
612 _url = url('files_home', repo_name=c.repo_name,
613 revision=diff1, f_path=c.f_path)
613 revision=diff1, f_path=c.f_path)
614
614
615 raise HTTPFound(location=_url)
615 raise HTTPFound(location=_url)
616 try:
616 try:
617 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
617 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
618 c.changeset_1 = c.db_repo_scm_instance.get_changeset(diff1)
618 c.changeset_1 = c.db_repo_scm_instance.get_changeset(diff1)
619 try:
619 try:
620 node1 = c.changeset_1.get_node(f_path)
620 node1 = c.changeset_1.get_node(f_path)
621 if node1.is_dir():
621 if node1.is_dir():
622 raise NodeError('%s path is a %s not a file'
622 raise NodeError('%s path is a %s not a file'
623 % (node1, type(node1)))
623 % (node1, type(node1)))
624 except NodeDoesNotExistError:
624 except NodeDoesNotExistError:
625 c.changeset_1 = EmptyChangeset(cs=diff1,
625 c.changeset_1 = EmptyChangeset(cs=diff1,
626 revision=c.changeset_1.revision,
626 revision=c.changeset_1.revision,
627 repo=c.db_repo_scm_instance)
627 repo=c.db_repo_scm_instance)
628 node1 = FileNode(f_path, '', changeset=c.changeset_1)
628 node1 = FileNode(f_path, '', changeset=c.changeset_1)
629 else:
629 else:
630 c.changeset_1 = EmptyChangeset(repo=c.db_repo_scm_instance)
630 c.changeset_1 = EmptyChangeset(repo=c.db_repo_scm_instance)
631 node1 = FileNode(f_path, '', changeset=c.changeset_1)
631 node1 = FileNode(f_path, '', changeset=c.changeset_1)
632
632
633 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
633 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
634 c.changeset_2 = c.db_repo_scm_instance.get_changeset(diff2)
634 c.changeset_2 = c.db_repo_scm_instance.get_changeset(diff2)
635 try:
635 try:
636 node2 = c.changeset_2.get_node(f_path)
636 node2 = c.changeset_2.get_node(f_path)
637 if node2.is_dir():
637 if node2.is_dir():
638 raise NodeError('%s path is a %s not a file'
638 raise NodeError('%s path is a %s not a file'
639 % (node2, type(node2)))
639 % (node2, type(node2)))
640 except NodeDoesNotExistError:
640 except NodeDoesNotExistError:
641 c.changeset_2 = EmptyChangeset(cs=diff2,
641 c.changeset_2 = EmptyChangeset(cs=diff2,
642 revision=c.changeset_2.revision,
642 revision=c.changeset_2.revision,
643 repo=c.db_repo_scm_instance)
643 repo=c.db_repo_scm_instance)
644 node2 = FileNode(f_path, '', changeset=c.changeset_2)
644 node2 = FileNode(f_path, '', changeset=c.changeset_2)
645 else:
645 else:
646 c.changeset_2 = EmptyChangeset(repo=c.db_repo_scm_instance)
646 c.changeset_2 = EmptyChangeset(repo=c.db_repo_scm_instance)
647 node2 = FileNode(f_path, '', changeset=c.changeset_2)
647 node2 = FileNode(f_path, '', changeset=c.changeset_2)
648 except (RepositoryError, NodeError):
648 except (RepositoryError, NodeError):
649 log.error(traceback.format_exc())
649 log.error(traceback.format_exc())
650 raise HTTPFound(location=url('files_home', repo_name=c.repo_name,
650 raise HTTPFound(location=url('files_home', repo_name=c.repo_name,
651 f_path=f_path))
651 f_path=f_path))
652
652
653 if c.action == 'download':
653 if c.action == 'download':
654 raw_diff = diffs.get_gitdiff(node1, node2,
654 raw_diff = diffs.get_gitdiff(node1, node2,
655 ignore_whitespace=ignore_whitespace,
655 ignore_whitespace=ignore_whitespace,
656 context=line_context)
656 context=line_context)
657 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
657 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
658 response.content_type = 'text/plain'
658 response.content_type = 'text/plain'
659 response.content_disposition = (
659 response.content_disposition = (
660 'attachment; filename=%s' % diff_name
660 'attachment; filename=%s' % diff_name
661 )
661 )
662 return raw_diff
662 return raw_diff
663
663
664 elif c.action == 'raw':
664 elif c.action == 'raw':
665 raw_diff = diffs.get_gitdiff(node1, node2,
665 raw_diff = diffs.get_gitdiff(node1, node2,
666 ignore_whitespace=ignore_whitespace,
666 ignore_whitespace=ignore_whitespace,
667 context=line_context)
667 context=line_context)
668 response.content_type = 'text/plain'
668 response.content_type = 'text/plain'
669 return raw_diff
669 return raw_diff
670
670
671 else:
671 else:
672 fid = h.FID(diff2, node2.path)
672 fid = h.FID(diff2, node2.path)
673 line_context_lcl = get_line_ctx(fid, request.GET)
673 line_context_lcl = get_line_ctx(fid, request.GET)
674 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
674 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
675
675
676 diff_limit = None if fulldiff else self.cut_off_limit
676 diff_limit = None if fulldiff else self.cut_off_limit
677 c.a_rev, c.cs_rev, a_path, diff, st, op = diffs.wrapped_diff(filenode_old=node1,
677 c.a_rev, c.cs_rev, a_path, diff, st, op = diffs.wrapped_diff(filenode_old=node1,
678 filenode_new=node2,
678 filenode_new=node2,
679 diff_limit=diff_limit,
679 diff_limit=diff_limit,
680 ignore_whitespace=ign_whitespace_lcl,
680 ignore_whitespace=ign_whitespace_lcl,
681 line_context=line_context_lcl,
681 line_context=line_context_lcl,
682 enable_comments=False)
682 enable_comments=False)
683 c.file_diff_data = [(fid, fid, op, a_path, node2.path, diff, st)]
683 c.file_diff_data = [(fid, fid, op, a_path, node2.path, diff, st)]
684
684
685 return render('files/file_diff.html')
685 return render('files/file_diff.html')
686
686
687 @LoginRequired(allow_default_user=True)
687 @LoginRequired(allow_default_user=True)
688 @HasRepoPermissionLevelDecorator('read')
688 @HasRepoPermissionLevelDecorator('read')
689 def diff_2way(self, repo_name, f_path):
689 def diff_2way(self, repo_name, f_path):
690 diff1 = request.GET.get('diff1', '')
690 diff1 = request.GET.get('diff1', '')
691 diff2 = request.GET.get('diff2', '')
691 diff2 = request.GET.get('diff2', '')
692 try:
692 try:
693 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
693 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
694 c.changeset_1 = c.db_repo_scm_instance.get_changeset(diff1)
694 c.changeset_1 = c.db_repo_scm_instance.get_changeset(diff1)
695 try:
695 try:
696 node1 = c.changeset_1.get_node(f_path)
696 node1 = c.changeset_1.get_node(f_path)
697 if node1.is_dir():
697 if node1.is_dir():
698 raise NodeError('%s path is a %s not a file'
698 raise NodeError('%s path is a %s not a file'
699 % (node1, type(node1)))
699 % (node1, type(node1)))
700 except NodeDoesNotExistError:
700 except NodeDoesNotExistError:
701 c.changeset_1 = EmptyChangeset(cs=diff1,
701 c.changeset_1 = EmptyChangeset(cs=diff1,
702 revision=c.changeset_1.revision,
702 revision=c.changeset_1.revision,
703 repo=c.db_repo_scm_instance)
703 repo=c.db_repo_scm_instance)
704 node1 = FileNode(f_path, '', changeset=c.changeset_1)
704 node1 = FileNode(f_path, '', changeset=c.changeset_1)
705 else:
705 else:
706 c.changeset_1 = EmptyChangeset(repo=c.db_repo_scm_instance)
706 c.changeset_1 = EmptyChangeset(repo=c.db_repo_scm_instance)
707 node1 = FileNode(f_path, '', changeset=c.changeset_1)
707 node1 = FileNode(f_path, '', changeset=c.changeset_1)
708
708
709 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
709 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
710 c.changeset_2 = c.db_repo_scm_instance.get_changeset(diff2)
710 c.changeset_2 = c.db_repo_scm_instance.get_changeset(diff2)
711 try:
711 try:
712 node2 = c.changeset_2.get_node(f_path)
712 node2 = c.changeset_2.get_node(f_path)
713 if node2.is_dir():
713 if node2.is_dir():
714 raise NodeError('%s path is a %s not a file'
714 raise NodeError('%s path is a %s not a file'
715 % (node2, type(node2)))
715 % (node2, type(node2)))
716 except NodeDoesNotExistError:
716 except NodeDoesNotExistError:
717 c.changeset_2 = EmptyChangeset(cs=diff2,
717 c.changeset_2 = EmptyChangeset(cs=diff2,
718 revision=c.changeset_2.revision,
718 revision=c.changeset_2.revision,
719 repo=c.db_repo_scm_instance)
719 repo=c.db_repo_scm_instance)
720 node2 = FileNode(f_path, '', changeset=c.changeset_2)
720 node2 = FileNode(f_path, '', changeset=c.changeset_2)
721 else:
721 else:
722 c.changeset_2 = EmptyChangeset(repo=c.db_repo_scm_instance)
722 c.changeset_2 = EmptyChangeset(repo=c.db_repo_scm_instance)
723 node2 = FileNode(f_path, '', changeset=c.changeset_2)
723 node2 = FileNode(f_path, '', changeset=c.changeset_2)
724 except ChangesetDoesNotExistError as e:
724 except ChangesetDoesNotExistError as e:
725 msg = _('Such revision does not exist for this repository')
725 msg = _('Such revision does not exist for this repository')
726 h.flash(msg, category='error')
726 h.flash(msg, category='error')
727 raise HTTPNotFound()
727 raise HTTPNotFound()
728 c.node1 = node1
728 c.node1 = node1
729 c.node2 = node2
729 c.node2 = node2
730 c.cs1 = c.changeset_1
730 c.cs1 = c.changeset_1
731 c.cs2 = c.changeset_2
731 c.cs2 = c.changeset_2
732
732
733 return render('files/diff_2way.html')
733 return render('files/diff_2way.html')
734
734
735 def _get_node_history(self, cs, f_path, changesets=None):
735 def _get_node_history(self, cs, f_path, changesets=None):
736 """
736 """
737 get changesets history for given node
737 get changesets history for given node
738
738
739 :param cs: changeset to calculate history
739 :param cs: changeset to calculate history
740 :param f_path: path for node to calculate history for
740 :param f_path: path for node to calculate history for
741 :param changesets: if passed don't calculate history and take
741 :param changesets: if passed don't calculate history and take
742 changesets defined in this list
742 changesets defined in this list
743 """
743 """
744 # calculate history based on tip
744 # calculate history based on tip
745 tip_cs = c.db_repo_scm_instance.get_changeset()
745 tip_cs = c.db_repo_scm_instance.get_changeset()
746 if changesets is None:
746 if changesets is None:
747 try:
747 try:
748 changesets = tip_cs.get_file_history(f_path)
748 changesets = tip_cs.get_file_history(f_path)
749 except (NodeDoesNotExistError, ChangesetError):
749 except (NodeDoesNotExistError, ChangesetError):
750 # this node is not present at tip !
750 # this node is not present at tip !
751 changesets = cs.get_file_history(f_path)
751 changesets = cs.get_file_history(f_path)
752 hist_l = []
752 hist_l = []
753
753
754 changesets_group = ([], _("Changesets"))
754 changesets_group = ([], _("Changesets"))
755 branches_group = ([], _("Branches"))
755 branches_group = ([], _("Branches"))
756 tags_group = ([], _("Tags"))
756 tags_group = ([], _("Tags"))
757 for chs in changesets:
757 for chs in changesets:
758 #_branch = '(%s)' % chs.branch if (cs.repository.alias == 'hg') else ''
758 # TODO: loop over chs.branches ... but that will not give all the bogus None branches for Git ...
759 _branch = chs.branch
759 _branch = chs.branch
760 n_desc = '%s (%s)' % (h.show_id(chs), _branch)
760 n_desc = '%s (%s)' % (h.show_id(chs), _branch)
761 changesets_group[0].append((chs.raw_id, n_desc,))
761 changesets_group[0].append((chs.raw_id, n_desc,))
762 hist_l.append(changesets_group)
762 hist_l.append(changesets_group)
763
763
764 for name, chs in c.db_repo_scm_instance.branches.items():
764 for name, chs in c.db_repo_scm_instance.branches.items():
765 branches_group[0].append((chs, name),)
765 branches_group[0].append((chs, name),)
766 hist_l.append(branches_group)
766 hist_l.append(branches_group)
767
767
768 for name, chs in c.db_repo_scm_instance.tags.items():
768 for name, chs in c.db_repo_scm_instance.tags.items():
769 tags_group[0].append((chs, name),)
769 tags_group[0].append((chs, name),)
770 hist_l.append(tags_group)
770 hist_l.append(tags_group)
771
771
772 return hist_l, changesets
772 return hist_l, changesets
773
773
774 @LoginRequired(allow_default_user=True)
774 @LoginRequired(allow_default_user=True)
775 @HasRepoPermissionLevelDecorator('read')
775 @HasRepoPermissionLevelDecorator('read')
776 @jsonify
776 @jsonify
777 def nodelist(self, repo_name, revision, f_path):
777 def nodelist(self, repo_name, revision, f_path):
778 if request.environ.get('HTTP_X_PARTIAL_XHR'):
778 if request.environ.get('HTTP_X_PARTIAL_XHR'):
779 cs = self.__get_cs(revision)
779 cs = self.__get_cs(revision)
780 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
780 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
781 flat=False)
781 flat=False)
782 return {'nodes': _d + _f}
782 return {'nodes': _d + _f}
@@ -1,728 +1,728 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.pullrequests
15 kallithea.controllers.pullrequests
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 pull requests controller for Kallithea for initializing pull requests
18 pull requests controller for Kallithea for initializing pull requests
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: May 7, 2012
22 :created_on: May 7, 2012
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import traceback
29 import traceback
30 import formencode
30 import formencode
31
31
32 from tg import request, tmpl_context as c
32 from tg import request, tmpl_context as c
33 from tg.i18n import ugettext as _
33 from tg.i18n import ugettext as _
34 from webob.exc import HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest
34 from webob.exc import HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest
35
35
36 from kallithea.config.routing import url
36 from kallithea.config.routing import url
37 from kallithea.lib import helpers as h
37 from kallithea.lib import helpers as h
38 from kallithea.lib import diffs
38 from kallithea.lib import diffs
39 from kallithea.lib.auth import LoginRequired, HasRepoPermissionLevelDecorator
39 from kallithea.lib.auth import LoginRequired, HasRepoPermissionLevelDecorator
40 from kallithea.lib.base import BaseRepoController, render, jsonify
40 from kallithea.lib.base import BaseRepoController, render, jsonify
41 from kallithea.lib.page import Page
41 from kallithea.lib.page import Page
42 from kallithea.lib.utils import action_logger
42 from kallithea.lib.utils import action_logger
43 from kallithea.lib.vcs.exceptions import EmptyRepositoryError, ChangesetDoesNotExistError
43 from kallithea.lib.vcs.exceptions import EmptyRepositoryError, ChangesetDoesNotExistError
44 from kallithea.lib.vcs.utils import safe_str
44 from kallithea.lib.vcs.utils import safe_str
45 from kallithea.lib.vcs.utils.hgcompat import unionrepo
45 from kallithea.lib.vcs.utils.hgcompat import unionrepo
46 from kallithea.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
46 from kallithea.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
47 PullRequestReviewer, Repository, User
47 PullRequestReviewer, Repository, User
48 from kallithea.model.pull_request import CreatePullRequestAction, CreatePullRequestIterationAction, PullRequestModel
48 from kallithea.model.pull_request import CreatePullRequestAction, CreatePullRequestIterationAction, PullRequestModel
49 from kallithea.model.meta import Session
49 from kallithea.model.meta import Session
50 from kallithea.model.repo import RepoModel
50 from kallithea.model.repo import RepoModel
51 from kallithea.model.comment import ChangesetCommentsModel
51 from kallithea.model.comment import ChangesetCommentsModel
52 from kallithea.model.changeset_status import ChangesetStatusModel
52 from kallithea.model.changeset_status import ChangesetStatusModel
53 from kallithea.model.forms import PullRequestForm, PullRequestPostForm
53 from kallithea.model.forms import PullRequestForm, PullRequestPostForm
54 from kallithea.lib.utils2 import safe_int
54 from kallithea.lib.utils2 import safe_int
55 from kallithea.controllers.changeset import _ignorews_url, _context_url, \
55 from kallithea.controllers.changeset import _ignorews_url, _context_url, \
56 create_comment
56 create_comment
57 from kallithea.controllers.compare import CompareController
57 from kallithea.controllers.compare import CompareController
58 from kallithea.lib.graphmod import graph_data
58 from kallithea.lib.graphmod import graph_data
59
59
60 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
61
61
62
62
63 def _get_reviewer(user_id):
63 def _get_reviewer(user_id):
64 """Look up user by ID and validate it as a potential reviewer."""
64 """Look up user by ID and validate it as a potential reviewer."""
65 try:
65 try:
66 user = User.get(int(user_id))
66 user = User.get(int(user_id))
67 except ValueError:
67 except ValueError:
68 user = None
68 user = None
69
69
70 if user is None or user.is_default_user:
70 if user is None or user.is_default_user:
71 h.flash(_('Invalid reviewer "%s" specified') % user_id, category='error')
71 h.flash(_('Invalid reviewer "%s" specified') % user_id, category='error')
72 raise HTTPBadRequest()
72 raise HTTPBadRequest()
73
73
74 return user
74 return user
75
75
76
76
77 class PullrequestsController(BaseRepoController):
77 class PullrequestsController(BaseRepoController):
78
78
79 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
79 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
80 """return a structure with repo's interesting changesets, suitable for
80 """return a structure with repo's interesting changesets, suitable for
81 the selectors in pullrequest.html
81 the selectors in pullrequest.html
82
82
83 rev: a revision that must be in the list somehow and selected by default
83 rev: a revision that must be in the list somehow and selected by default
84 branch: a branch that must be in the list and selected by default - even if closed
84 branch: a branch that must be in the list and selected by default - even if closed
85 branch_rev: a revision of which peers should be preferred and available."""
85 branch_rev: a revision of which peers should be preferred and available."""
86 # list named branches that has been merged to this named branch - it should probably merge back
86 # list named branches that has been merged to this named branch - it should probably merge back
87 peers = []
87 peers = []
88
88
89 if rev:
89 if rev:
90 rev = safe_str(rev)
90 rev = safe_str(rev)
91
91
92 if branch:
92 if branch:
93 branch = safe_str(branch)
93 branch = safe_str(branch)
94
94
95 if branch_rev:
95 if branch_rev:
96 branch_rev = safe_str(branch_rev)
96 branch_rev = safe_str(branch_rev)
97 # a revset not restricting to merge() would be better
97 # a revset not restricting to merge() would be better
98 # (especially because it would get the branch point)
98 # (especially because it would get the branch point)
99 # ... but is currently too expensive
99 # ... but is currently too expensive
100 # including branches of children could be nice too
100 # including branches of children could be nice too
101 peerbranches = set()
101 peerbranches = set()
102 for i in repo._repo.revs(
102 for i in repo._repo.revs(
103 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
103 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
104 branch_rev, branch_rev):
104 branch_rev, branch_rev):
105 abranch = repo.get_changeset(i).branch
105 for abranch in repo.get_changeset(i).branches:
106 if abranch not in peerbranches:
106 if abranch not in peerbranches:
107 n = 'branch:%s:%s' % (abranch, repo.get_changeset(abranch).raw_id)
107 n = 'branch:%s:%s' % (abranch, repo.get_changeset(abranch).raw_id)
108 peers.append((n, abranch))
108 peers.append((n, abranch))
109 peerbranches.add(abranch)
109 peerbranches.add(abranch)
110
110
111 selected = None
111 selected = None
112 tiprev = repo.tags.get('tip')
112 tiprev = repo.tags.get('tip')
113 tipbranch = None
113 tipbranch = None
114
114
115 branches = []
115 branches = []
116 for abranch, branchrev in repo.branches.iteritems():
116 for abranch, branchrev in repo.branches.iteritems():
117 n = 'branch:%s:%s' % (abranch, branchrev)
117 n = 'branch:%s:%s' % (abranch, branchrev)
118 desc = abranch
118 desc = abranch
119 if branchrev == tiprev:
119 if branchrev == tiprev:
120 tipbranch = abranch
120 tipbranch = abranch
121 desc = '%s (current tip)' % desc
121 desc = '%s (current tip)' % desc
122 branches.append((n, desc))
122 branches.append((n, desc))
123 if rev == branchrev:
123 if rev == branchrev:
124 selected = n
124 selected = n
125 if branch == abranch:
125 if branch == abranch:
126 if not rev:
126 if not rev:
127 selected = n
127 selected = n
128 branch = None
128 branch = None
129 if branch: # branch not in list - it is probably closed
129 if branch: # branch not in list - it is probably closed
130 branchrev = repo.closed_branches.get(branch)
130 branchrev = repo.closed_branches.get(branch)
131 if branchrev:
131 if branchrev:
132 n = 'branch:%s:%s' % (branch, branchrev)
132 n = 'branch:%s:%s' % (branch, branchrev)
133 branches.append((n, _('%s (closed)') % branch))
133 branches.append((n, _('%s (closed)') % branch))
134 selected = n
134 selected = n
135 branch = None
135 branch = None
136 if branch:
136 if branch:
137 log.debug('branch %r not found in %s', branch, repo)
137 log.debug('branch %r not found in %s', branch, repo)
138
138
139 bookmarks = []
139 bookmarks = []
140 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
140 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
141 n = 'book:%s:%s' % (bookmark, bookmarkrev)
141 n = 'book:%s:%s' % (bookmark, bookmarkrev)
142 bookmarks.append((n, bookmark))
142 bookmarks.append((n, bookmark))
143 if rev == bookmarkrev:
143 if rev == bookmarkrev:
144 selected = n
144 selected = n
145
145
146 tags = []
146 tags = []
147 for tag, tagrev in repo.tags.iteritems():
147 for tag, tagrev in repo.tags.iteritems():
148 if tag == 'tip':
148 if tag == 'tip':
149 continue
149 continue
150 n = 'tag:%s:%s' % (tag, tagrev)
150 n = 'tag:%s:%s' % (tag, tagrev)
151 tags.append((n, tag))
151 tags.append((n, tag))
152 # note: even if rev == tagrev, don't select the static tag - it must be chosen explicitly
152 # note: even if rev == tagrev, don't select the static tag - it must be chosen explicitly
153
153
154 # prio 1: rev was selected as existing entry above
154 # prio 1: rev was selected as existing entry above
155
155
156 # prio 2: create special entry for rev; rev _must_ be used
156 # prio 2: create special entry for rev; rev _must_ be used
157 specials = []
157 specials = []
158 if rev and selected is None:
158 if rev and selected is None:
159 selected = 'rev:%s:%s' % (rev, rev)
159 selected = 'rev:%s:%s' % (rev, rev)
160 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
160 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
161
161
162 # prio 3: most recent peer branch
162 # prio 3: most recent peer branch
163 if peers and not selected:
163 if peers and not selected:
164 selected = peers[0][0]
164 selected = peers[0][0]
165
165
166 # prio 4: tip revision
166 # prio 4: tip revision
167 if not selected:
167 if not selected:
168 if h.is_hg(repo):
168 if h.is_hg(repo):
169 if tipbranch:
169 if tipbranch:
170 selected = 'branch:%s:%s' % (tipbranch, tiprev)
170 selected = 'branch:%s:%s' % (tipbranch, tiprev)
171 else:
171 else:
172 selected = 'tag:null:' + repo.EMPTY_CHANGESET
172 selected = 'tag:null:' + repo.EMPTY_CHANGESET
173 tags.append((selected, 'null'))
173 tags.append((selected, 'null'))
174 else:
174 else:
175 if 'master' in repo.branches:
175 if 'master' in repo.branches:
176 selected = 'branch:master:%s' % repo.branches['master']
176 selected = 'branch:master:%s' % repo.branches['master']
177 else:
177 else:
178 k, v = repo.branches.items()[0]
178 k, v = repo.branches.items()[0]
179 selected = 'branch:%s:%s' % (k, v)
179 selected = 'branch:%s:%s' % (k, v)
180
180
181 groups = [(specials, _("Special")),
181 groups = [(specials, _("Special")),
182 (peers, _("Peer branches")),
182 (peers, _("Peer branches")),
183 (bookmarks, _("Bookmarks")),
183 (bookmarks, _("Bookmarks")),
184 (branches, _("Branches")),
184 (branches, _("Branches")),
185 (tags, _("Tags")),
185 (tags, _("Tags")),
186 ]
186 ]
187 return [g for g in groups if g[0]], selected
187 return [g for g in groups if g[0]], selected
188
188
189 def _get_is_allowed_change_status(self, pull_request):
189 def _get_is_allowed_change_status(self, pull_request):
190 if pull_request.is_closed():
190 if pull_request.is_closed():
191 return False
191 return False
192
192
193 owner = request.authuser.user_id == pull_request.owner_id
193 owner = request.authuser.user_id == pull_request.owner_id
194 reviewer = PullRequestReviewer.query() \
194 reviewer = PullRequestReviewer.query() \
195 .filter(PullRequestReviewer.pull_request == pull_request) \
195 .filter(PullRequestReviewer.pull_request == pull_request) \
196 .filter(PullRequestReviewer.user_id == request.authuser.user_id) \
196 .filter(PullRequestReviewer.user_id == request.authuser.user_id) \
197 .count() != 0
197 .count() != 0
198
198
199 return request.authuser.admin or owner or reviewer
199 return request.authuser.admin or owner or reviewer
200
200
201 @LoginRequired(allow_default_user=True)
201 @LoginRequired(allow_default_user=True)
202 @HasRepoPermissionLevelDecorator('read')
202 @HasRepoPermissionLevelDecorator('read')
203 def show_all(self, repo_name):
203 def show_all(self, repo_name):
204 c.from_ = request.GET.get('from_') or ''
204 c.from_ = request.GET.get('from_') or ''
205 c.closed = request.GET.get('closed') or ''
205 c.closed = request.GET.get('closed') or ''
206 p = safe_int(request.GET.get('page'), 1)
206 p = safe_int(request.GET.get('page'), 1)
207
207
208 q = PullRequest.query(include_closed=c.closed, sorted=True)
208 q = PullRequest.query(include_closed=c.closed, sorted=True)
209 if c.from_:
209 if c.from_:
210 q = q.filter_by(org_repo=c.db_repo)
210 q = q.filter_by(org_repo=c.db_repo)
211 else:
211 else:
212 q = q.filter_by(other_repo=c.db_repo)
212 q = q.filter_by(other_repo=c.db_repo)
213 c.pull_requests = q.all()
213 c.pull_requests = q.all()
214
214
215 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=100)
215 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=100)
216
216
217 return render('/pullrequests/pullrequest_show_all.html')
217 return render('/pullrequests/pullrequest_show_all.html')
218
218
219 @LoginRequired()
219 @LoginRequired()
220 def show_my(self):
220 def show_my(self):
221 c.closed = request.GET.get('closed') or ''
221 c.closed = request.GET.get('closed') or ''
222
222
223 c.my_pull_requests = PullRequest.query(
223 c.my_pull_requests = PullRequest.query(
224 include_closed=c.closed,
224 include_closed=c.closed,
225 sorted=True,
225 sorted=True,
226 ).filter_by(owner_id=request.authuser.user_id).all()
226 ).filter_by(owner_id=request.authuser.user_id).all()
227
227
228 c.participate_in_pull_requests = []
228 c.participate_in_pull_requests = []
229 c.participate_in_pull_requests_todo = []
229 c.participate_in_pull_requests_todo = []
230 done_status = set([ChangesetStatus.STATUS_APPROVED, ChangesetStatus.STATUS_REJECTED])
230 done_status = set([ChangesetStatus.STATUS_APPROVED, ChangesetStatus.STATUS_REJECTED])
231 for pr in PullRequest.query(
231 for pr in PullRequest.query(
232 include_closed=c.closed,
232 include_closed=c.closed,
233 reviewer_id=request.authuser.user_id,
233 reviewer_id=request.authuser.user_id,
234 sorted=True,
234 sorted=True,
235 ):
235 ):
236 status = pr.user_review_status(request.authuser.user_id) # very inefficient!!!
236 status = pr.user_review_status(request.authuser.user_id) # very inefficient!!!
237 if status in done_status:
237 if status in done_status:
238 c.participate_in_pull_requests.append(pr)
238 c.participate_in_pull_requests.append(pr)
239 else:
239 else:
240 c.participate_in_pull_requests_todo.append(pr)
240 c.participate_in_pull_requests_todo.append(pr)
241
241
242 return render('/pullrequests/pullrequest_show_my.html')
242 return render('/pullrequests/pullrequest_show_my.html')
243
243
244 @LoginRequired()
244 @LoginRequired()
245 @HasRepoPermissionLevelDecorator('read')
245 @HasRepoPermissionLevelDecorator('read')
246 def index(self):
246 def index(self):
247 org_repo = c.db_repo
247 org_repo = c.db_repo
248 org_scm_instance = org_repo.scm_instance
248 org_scm_instance = org_repo.scm_instance
249 try:
249 try:
250 org_scm_instance.get_changeset()
250 org_scm_instance.get_changeset()
251 except EmptyRepositoryError as e:
251 except EmptyRepositoryError as e:
252 h.flash(h.literal(_('There are no changesets yet')),
252 h.flash(h.literal(_('There are no changesets yet')),
253 category='warning')
253 category='warning')
254 raise HTTPFound(location=url('summary_home', repo_name=org_repo.repo_name))
254 raise HTTPFound(location=url('summary_home', repo_name=org_repo.repo_name))
255
255
256 org_rev = request.GET.get('rev_end')
256 org_rev = request.GET.get('rev_end')
257 # rev_start is not directly useful - its parent could however be used
257 # rev_start is not directly useful - its parent could however be used
258 # as default for other and thus give a simple compare view
258 # as default for other and thus give a simple compare view
259 rev_start = request.GET.get('rev_start')
259 rev_start = request.GET.get('rev_start')
260 other_rev = None
260 other_rev = None
261 if rev_start:
261 if rev_start:
262 starters = org_repo.get_changeset(rev_start).parents
262 starters = org_repo.get_changeset(rev_start).parents
263 if starters:
263 if starters:
264 other_rev = starters[0].raw_id
264 other_rev = starters[0].raw_id
265 else:
265 else:
266 other_rev = org_repo.scm_instance.EMPTY_CHANGESET
266 other_rev = org_repo.scm_instance.EMPTY_CHANGESET
267 branch = request.GET.get('branch')
267 branch = request.GET.get('branch')
268
268
269 c.cs_repos = [(org_repo.repo_name, org_repo.repo_name)]
269 c.cs_repos = [(org_repo.repo_name, org_repo.repo_name)]
270 c.default_cs_repo = org_repo.repo_name
270 c.default_cs_repo = org_repo.repo_name
271 c.cs_refs, c.default_cs_ref = self._get_repo_refs(org_scm_instance, rev=org_rev, branch=branch)
271 c.cs_refs, c.default_cs_ref = self._get_repo_refs(org_scm_instance, rev=org_rev, branch=branch)
272
272
273 default_cs_ref_type, default_cs_branch, default_cs_rev = c.default_cs_ref.split(':')
273 default_cs_ref_type, default_cs_branch, default_cs_rev = c.default_cs_ref.split(':')
274 if default_cs_ref_type != 'branch':
274 if default_cs_ref_type != 'branch':
275 default_cs_branch = org_repo.get_changeset(default_cs_rev).branch
275 default_cs_branch = org_repo.get_changeset(default_cs_rev).branch
276
276
277 # add org repo to other so we can open pull request against peer branches on itself
277 # add org repo to other so we can open pull request against peer branches on itself
278 c.a_repos = [(org_repo.repo_name, '%s (self)' % org_repo.repo_name)]
278 c.a_repos = [(org_repo.repo_name, '%s (self)' % org_repo.repo_name)]
279
279
280 if org_repo.parent:
280 if org_repo.parent:
281 # add parent of this fork also and select it.
281 # add parent of this fork also and select it.
282 # use the same branch on destination as on source, if available.
282 # use the same branch on destination as on source, if available.
283 c.a_repos.append((org_repo.parent.repo_name, '%s (parent)' % org_repo.parent.repo_name))
283 c.a_repos.append((org_repo.parent.repo_name, '%s (parent)' % org_repo.parent.repo_name))
284 c.a_repo = org_repo.parent
284 c.a_repo = org_repo.parent
285 c.a_refs, c.default_a_ref = self._get_repo_refs(
285 c.a_refs, c.default_a_ref = self._get_repo_refs(
286 org_repo.parent.scm_instance, branch=default_cs_branch, rev=other_rev)
286 org_repo.parent.scm_instance, branch=default_cs_branch, rev=other_rev)
287
287
288 else:
288 else:
289 c.a_repo = org_repo
289 c.a_repo = org_repo
290 c.a_refs, c.default_a_ref = self._get_repo_refs(org_scm_instance, rev=other_rev)
290 c.a_refs, c.default_a_ref = self._get_repo_refs(org_scm_instance, rev=other_rev)
291
291
292 # gather forks and add to this list ... even though it is rare to
292 # gather forks and add to this list ... even though it is rare to
293 # request forks to pull from their parent
293 # request forks to pull from their parent
294 for fork in org_repo.forks:
294 for fork in org_repo.forks:
295 c.a_repos.append((fork.repo_name, fork.repo_name))
295 c.a_repos.append((fork.repo_name, fork.repo_name))
296
296
297 return render('/pullrequests/pullrequest.html')
297 return render('/pullrequests/pullrequest.html')
298
298
299 @LoginRequired()
299 @LoginRequired()
300 @HasRepoPermissionLevelDecorator('read')
300 @HasRepoPermissionLevelDecorator('read')
301 @jsonify
301 @jsonify
302 def repo_info(self, repo_name):
302 def repo_info(self, repo_name):
303 repo = c.db_repo
303 repo = c.db_repo
304 refs, selected_ref = self._get_repo_refs(repo.scm_instance)
304 refs, selected_ref = self._get_repo_refs(repo.scm_instance)
305 return {
305 return {
306 'description': repo.description.split('\n', 1)[0],
306 'description': repo.description.split('\n', 1)[0],
307 'selected_ref': selected_ref,
307 'selected_ref': selected_ref,
308 'refs': refs,
308 'refs': refs,
309 }
309 }
310
310
311 @LoginRequired()
311 @LoginRequired()
312 @HasRepoPermissionLevelDecorator('read')
312 @HasRepoPermissionLevelDecorator('read')
313 def create(self, repo_name):
313 def create(self, repo_name):
314 repo = c.db_repo
314 repo = c.db_repo
315 try:
315 try:
316 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
316 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
317 except formencode.Invalid as errors:
317 except formencode.Invalid as errors:
318 log.error(traceback.format_exc())
318 log.error(traceback.format_exc())
319 log.error(str(errors))
319 log.error(str(errors))
320 msg = _('Error creating pull request: %s') % errors.msg
320 msg = _('Error creating pull request: %s') % errors.msg
321 h.flash(msg, 'error')
321 h.flash(msg, 'error')
322 raise HTTPBadRequest
322 raise HTTPBadRequest
323
323
324 # heads up: org and other might seem backward here ...
324 # heads up: org and other might seem backward here ...
325 org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name
325 org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name
326 org_repo = Repository.guess_instance(_form['org_repo'])
326 org_repo = Repository.guess_instance(_form['org_repo'])
327
327
328 other_ref = _form['other_ref'] # will have symbolic name and head revision
328 other_ref = _form['other_ref'] # will have symbolic name and head revision
329 other_repo = Repository.guess_instance(_form['other_repo'])
329 other_repo = Repository.guess_instance(_form['other_repo'])
330
330
331 reviewers = []
331 reviewers = []
332
332
333 title = _form['pullrequest_title']
333 title = _form['pullrequest_title']
334 description = _form['pullrequest_desc'].strip()
334 description = _form['pullrequest_desc'].strip()
335 owner = User.get(request.authuser.user_id)
335 owner = User.get(request.authuser.user_id)
336
336
337 try:
337 try:
338 cmd = CreatePullRequestAction(org_repo, other_repo, org_ref, other_ref, title, description, owner, reviewers)
338 cmd = CreatePullRequestAction(org_repo, other_repo, org_ref, other_ref, title, description, owner, reviewers)
339 except CreatePullRequestAction.ValidationError as e:
339 except CreatePullRequestAction.ValidationError as e:
340 h.flash(str(e), category='error', logf=log.error)
340 h.flash(str(e), category='error', logf=log.error)
341 raise HTTPNotFound
341 raise HTTPNotFound
342
342
343 try:
343 try:
344 pull_request = cmd.execute()
344 pull_request = cmd.execute()
345 Session().commit()
345 Session().commit()
346 except Exception:
346 except Exception:
347 h.flash(_('Error occurred while creating pull request'),
347 h.flash(_('Error occurred while creating pull request'),
348 category='error')
348 category='error')
349 log.error(traceback.format_exc())
349 log.error(traceback.format_exc())
350 raise HTTPFound(location=url('pullrequest_home', repo_name=repo_name))
350 raise HTTPFound(location=url('pullrequest_home', repo_name=repo_name))
351
351
352 h.flash(_('Successfully opened new pull request'),
352 h.flash(_('Successfully opened new pull request'),
353 category='success')
353 category='success')
354 raise HTTPFound(location=pull_request.url())
354 raise HTTPFound(location=pull_request.url())
355
355
356 def create_new_iteration(self, old_pull_request, new_rev, title, description, reviewers):
356 def create_new_iteration(self, old_pull_request, new_rev, title, description, reviewers):
357 owner = User.get(request.authuser.user_id)
357 owner = User.get(request.authuser.user_id)
358 new_org_rev = self._get_ref_rev(old_pull_request.org_repo, 'rev', new_rev)
358 new_org_rev = self._get_ref_rev(old_pull_request.org_repo, 'rev', new_rev)
359 new_other_rev = self._get_ref_rev(old_pull_request.other_repo, old_pull_request.other_ref_parts[0], old_pull_request.other_ref_parts[1])
359 new_other_rev = self._get_ref_rev(old_pull_request.other_repo, old_pull_request.other_ref_parts[0], old_pull_request.other_ref_parts[1])
360 try:
360 try:
361 cmd = CreatePullRequestIterationAction(old_pull_request, new_org_rev, new_other_rev, title, description, owner, reviewers)
361 cmd = CreatePullRequestIterationAction(old_pull_request, new_org_rev, new_other_rev, title, description, owner, reviewers)
362 except CreatePullRequestAction.ValidationError as e:
362 except CreatePullRequestAction.ValidationError as e:
363 h.flash(str(e), category='error', logf=log.error)
363 h.flash(str(e), category='error', logf=log.error)
364 raise HTTPNotFound
364 raise HTTPNotFound
365
365
366 try:
366 try:
367 pull_request = cmd.execute()
367 pull_request = cmd.execute()
368 Session().commit()
368 Session().commit()
369 except Exception:
369 except Exception:
370 h.flash(_('Error occurred while creating pull request'),
370 h.flash(_('Error occurred while creating pull request'),
371 category='error')
371 category='error')
372 log.error(traceback.format_exc())
372 log.error(traceback.format_exc())
373 raise HTTPFound(location=old_pull_request.url())
373 raise HTTPFound(location=old_pull_request.url())
374
374
375 h.flash(_('New pull request iteration created'),
375 h.flash(_('New pull request iteration created'),
376 category='success')
376 category='success')
377 raise HTTPFound(location=pull_request.url())
377 raise HTTPFound(location=pull_request.url())
378
378
379 # pullrequest_post for PR editing
379 # pullrequest_post for PR editing
380 @LoginRequired()
380 @LoginRequired()
381 @HasRepoPermissionLevelDecorator('read')
381 @HasRepoPermissionLevelDecorator('read')
382 def post(self, repo_name, pull_request_id):
382 def post(self, repo_name, pull_request_id):
383 pull_request = PullRequest.get_or_404(pull_request_id)
383 pull_request = PullRequest.get_or_404(pull_request_id)
384 if pull_request.is_closed():
384 if pull_request.is_closed():
385 raise HTTPForbidden()
385 raise HTTPForbidden()
386 assert pull_request.other_repo.repo_name == repo_name
386 assert pull_request.other_repo.repo_name == repo_name
387 # only owner or admin can update it
387 # only owner or admin can update it
388 owner = pull_request.owner_id == request.authuser.user_id
388 owner = pull_request.owner_id == request.authuser.user_id
389 repo_admin = h.HasRepoPermissionLevel('admin')(c.repo_name)
389 repo_admin = h.HasRepoPermissionLevel('admin')(c.repo_name)
390 if not (h.HasPermissionAny('hg.admin')() or repo_admin or owner):
390 if not (h.HasPermissionAny('hg.admin')() or repo_admin or owner):
391 raise HTTPForbidden()
391 raise HTTPForbidden()
392
392
393 _form = PullRequestPostForm()().to_python(request.POST)
393 _form = PullRequestPostForm()().to_python(request.POST)
394
394
395 cur_reviewers = set(pull_request.get_reviewer_users())
395 cur_reviewers = set(pull_request.get_reviewer_users())
396 new_reviewers = set(_get_reviewer(s) for s in _form['review_members'])
396 new_reviewers = set(_get_reviewer(s) for s in _form['review_members'])
397 old_reviewers = set(_get_reviewer(s) for s in _form['org_review_members'])
397 old_reviewers = set(_get_reviewer(s) for s in _form['org_review_members'])
398
398
399 other_added = cur_reviewers - old_reviewers
399 other_added = cur_reviewers - old_reviewers
400 other_removed = old_reviewers - cur_reviewers
400 other_removed = old_reviewers - cur_reviewers
401
401
402 if other_added:
402 if other_added:
403 h.flash(_('Meanwhile, the following reviewers have been added: %s') %
403 h.flash(_('Meanwhile, the following reviewers have been added: %s') %
404 (', '.join(u.username for u in other_added)),
404 (', '.join(u.username for u in other_added)),
405 category='warning')
405 category='warning')
406 if other_removed:
406 if other_removed:
407 h.flash(_('Meanwhile, the following reviewers have been removed: %s') %
407 h.flash(_('Meanwhile, the following reviewers have been removed: %s') %
408 (', '.join(u.username for u in other_removed)),
408 (', '.join(u.username for u in other_removed)),
409 category='warning')
409 category='warning')
410
410
411 if _form['updaterev']:
411 if _form['updaterev']:
412 return self.create_new_iteration(pull_request,
412 return self.create_new_iteration(pull_request,
413 _form['updaterev'],
413 _form['updaterev'],
414 _form['pullrequest_title'],
414 _form['pullrequest_title'],
415 _form['pullrequest_desc'],
415 _form['pullrequest_desc'],
416 new_reviewers)
416 new_reviewers)
417
417
418 added_reviewers = new_reviewers - old_reviewers - cur_reviewers
418 added_reviewers = new_reviewers - old_reviewers - cur_reviewers
419 removed_reviewers = (old_reviewers - new_reviewers) & cur_reviewers
419 removed_reviewers = (old_reviewers - new_reviewers) & cur_reviewers
420
420
421 old_description = pull_request.description
421 old_description = pull_request.description
422 pull_request.title = _form['pullrequest_title']
422 pull_request.title = _form['pullrequest_title']
423 pull_request.description = _form['pullrequest_desc'].strip() or _('No description')
423 pull_request.description = _form['pullrequest_desc'].strip() or _('No description')
424 pull_request.owner = User.get_by_username(_form['owner'])
424 pull_request.owner = User.get_by_username(_form['owner'])
425 user = User.get(request.authuser.user_id)
425 user = User.get(request.authuser.user_id)
426
426
427 PullRequestModel().mention_from_description(user, pull_request, old_description)
427 PullRequestModel().mention_from_description(user, pull_request, old_description)
428 PullRequestModel().add_reviewers(user, pull_request, added_reviewers)
428 PullRequestModel().add_reviewers(user, pull_request, added_reviewers)
429 PullRequestModel().remove_reviewers(user, pull_request, removed_reviewers)
429 PullRequestModel().remove_reviewers(user, pull_request, removed_reviewers)
430
430
431 Session().commit()
431 Session().commit()
432 h.flash(_('Pull request updated'), category='success')
432 h.flash(_('Pull request updated'), category='success')
433
433
434 raise HTTPFound(location=pull_request.url())
434 raise HTTPFound(location=pull_request.url())
435
435
436 @LoginRequired()
436 @LoginRequired()
437 @HasRepoPermissionLevelDecorator('read')
437 @HasRepoPermissionLevelDecorator('read')
438 @jsonify
438 @jsonify
439 def delete(self, repo_name, pull_request_id):
439 def delete(self, repo_name, pull_request_id):
440 pull_request = PullRequest.get_or_404(pull_request_id)
440 pull_request = PullRequest.get_or_404(pull_request_id)
441 # only owner can delete it !
441 # only owner can delete it !
442 if pull_request.owner_id == request.authuser.user_id:
442 if pull_request.owner_id == request.authuser.user_id:
443 PullRequestModel().delete(pull_request)
443 PullRequestModel().delete(pull_request)
444 Session().commit()
444 Session().commit()
445 h.flash(_('Successfully deleted pull request'),
445 h.flash(_('Successfully deleted pull request'),
446 category='success')
446 category='success')
447 raise HTTPFound(location=url('my_pullrequests'))
447 raise HTTPFound(location=url('my_pullrequests'))
448 raise HTTPForbidden()
448 raise HTTPForbidden()
449
449
450 @LoginRequired(allow_default_user=True)
450 @LoginRequired(allow_default_user=True)
451 @HasRepoPermissionLevelDecorator('read')
451 @HasRepoPermissionLevelDecorator('read')
452 def show(self, repo_name, pull_request_id, extra=None):
452 def show(self, repo_name, pull_request_id, extra=None):
453 repo_model = RepoModel()
453 repo_model = RepoModel()
454 c.users_array = repo_model.get_users_js()
454 c.users_array = repo_model.get_users_js()
455 c.pull_request = PullRequest.get_or_404(pull_request_id)
455 c.pull_request = PullRequest.get_or_404(pull_request_id)
456 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
456 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
457 cc_model = ChangesetCommentsModel()
457 cc_model = ChangesetCommentsModel()
458 cs_model = ChangesetStatusModel()
458 cs_model = ChangesetStatusModel()
459
459
460 # pull_requests repo_name we opened it against
460 # pull_requests repo_name we opened it against
461 # ie. other_repo must match
461 # ie. other_repo must match
462 if repo_name != c.pull_request.other_repo.repo_name:
462 if repo_name != c.pull_request.other_repo.repo_name:
463 raise HTTPNotFound
463 raise HTTPNotFound
464
464
465 # load compare data into template context
465 # load compare data into template context
466 c.cs_repo = c.pull_request.org_repo
466 c.cs_repo = c.pull_request.org_repo
467 (c.cs_ref_type,
467 (c.cs_ref_type,
468 c.cs_ref_name,
468 c.cs_ref_name,
469 c.cs_rev) = c.pull_request.org_ref.split(':')
469 c.cs_rev) = c.pull_request.org_ref.split(':')
470
470
471 c.a_repo = c.pull_request.other_repo
471 c.a_repo = c.pull_request.other_repo
472 (c.a_ref_type,
472 (c.a_ref_type,
473 c.a_ref_name,
473 c.a_ref_name,
474 c.a_rev) = c.pull_request.other_ref.split(':') # a_rev is ancestor
474 c.a_rev) = c.pull_request.other_ref.split(':') # a_rev is ancestor
475
475
476 org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
476 org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
477 try:
477 try:
478 c.cs_ranges = []
478 c.cs_ranges = []
479 for x in c.pull_request.revisions:
479 for x in c.pull_request.revisions:
480 c.cs_ranges.append(org_scm_instance.get_changeset(x))
480 c.cs_ranges.append(org_scm_instance.get_changeset(x))
481 except ChangesetDoesNotExistError:
481 except ChangesetDoesNotExistError:
482 c.cs_ranges = []
482 c.cs_ranges = []
483 h.flash(_('Revision %s not found in %s') % (x, c.cs_repo.repo_name),
483 h.flash(_('Revision %s not found in %s') % (x, c.cs_repo.repo_name),
484 'error')
484 'error')
485 c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
485 c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
486 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
486 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
487 c.jsdata = graph_data(org_scm_instance, revs)
487 c.jsdata = graph_data(org_scm_instance, revs)
488
488
489 c.is_range = False
489 c.is_range = False
490 try:
490 try:
491 if c.a_ref_type == 'rev': # this looks like a free range where target is ancestor
491 if c.a_ref_type == 'rev': # this looks like a free range where target is ancestor
492 cs_a = org_scm_instance.get_changeset(c.a_rev)
492 cs_a = org_scm_instance.get_changeset(c.a_rev)
493 root_parents = c.cs_ranges[0].parents
493 root_parents = c.cs_ranges[0].parents
494 c.is_range = cs_a in root_parents
494 c.is_range = cs_a in root_parents
495 #c.merge_root = len(root_parents) > 1 # a range starting with a merge might deserve a warning
495 #c.merge_root = len(root_parents) > 1 # a range starting with a merge might deserve a warning
496 except ChangesetDoesNotExistError: # probably because c.a_rev not found
496 except ChangesetDoesNotExistError: # probably because c.a_rev not found
497 pass
497 pass
498 except IndexError: # probably because c.cs_ranges is empty, probably because revisions are missing
498 except IndexError: # probably because c.cs_ranges is empty, probably because revisions are missing
499 pass
499 pass
500
500
501 avail_revs = set()
501 avail_revs = set()
502 avail_show = []
502 avail_show = []
503 c.cs_branch_name = c.cs_ref_name
503 c.cs_branch_name = c.cs_ref_name
504 c.a_branch_name = None
504 c.a_branch_name = None
505 other_scm_instance = c.a_repo.scm_instance
505 other_scm_instance = c.a_repo.scm_instance
506 c.update_msg = ""
506 c.update_msg = ""
507 c.update_msg_other = ""
507 c.update_msg_other = ""
508 try:
508 try:
509 if not c.cs_ranges:
509 if not c.cs_ranges:
510 c.update_msg = _('Error: changesets not found when displaying pull request from %s.') % c.cs_rev
510 c.update_msg = _('Error: changesets not found when displaying pull request from %s.') % c.cs_rev
511 elif org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor':
511 elif org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor':
512 if c.cs_ref_type != 'branch':
512 if c.cs_ref_type != 'branch':
513 c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ?
513 c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ?
514 c.a_branch_name = c.a_ref_name
514 c.a_branch_name = c.a_ref_name
515 if c.a_ref_type != 'branch':
515 if c.a_ref_type != 'branch':
516 try:
516 try:
517 c.a_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ?
517 c.a_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ?
518 except EmptyRepositoryError:
518 except EmptyRepositoryError:
519 c.a_branch_name = 'null' # not a branch name ... but close enough
519 c.a_branch_name = 'null' # not a branch name ... but close enough
520 # candidates: descendants of old head that are on the right branch
520 # candidates: descendants of old head that are on the right branch
521 # and not are the old head itself ...
521 # and not are the old head itself ...
522 # and nothing at all if old head is a descendant of target ref name
522 # and nothing at all if old head is a descendant of target ref name
523 if not c.is_range and other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, c.a_branch_name):
523 if not c.is_range and other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, c.a_branch_name):
524 c.update_msg = _('This pull request has already been merged to %s.') % c.a_branch_name
524 c.update_msg = _('This pull request has already been merged to %s.') % c.a_branch_name
525 elif c.pull_request.is_closed():
525 elif c.pull_request.is_closed():
526 c.update_msg = _('This pull request has been closed and can not be updated.')
526 c.update_msg = _('This pull request has been closed and can not be updated.')
527 else: # look for descendants of PR head on source branch in org repo
527 else: # look for descendants of PR head on source branch in org repo
528 avail_revs = org_scm_instance._repo.revs('%s:: & branch(%s)',
528 avail_revs = org_scm_instance._repo.revs('%s:: & branch(%s)',
529 revs[0], c.cs_branch_name)
529 revs[0], c.cs_branch_name)
530 if len(avail_revs) > 1: # more than just revs[0]
530 if len(avail_revs) > 1: # more than just revs[0]
531 # also show changesets that not are descendants but would be merged in
531 # also show changesets that not are descendants but would be merged in
532 targethead = other_scm_instance.get_changeset(c.a_branch_name).raw_id
532 targethead = other_scm_instance.get_changeset(c.a_branch_name).raw_id
533 if org_scm_instance.path != other_scm_instance.path:
533 if org_scm_instance.path != other_scm_instance.path:
534 # Note: org_scm_instance.path must come first so all
534 # Note: org_scm_instance.path must come first so all
535 # valid revision numbers are 100% org_scm compatible
535 # valid revision numbers are 100% org_scm compatible
536 # - both for avail_revs and for revset results
536 # - both for avail_revs and for revset results
537 hgrepo = unionrepo.unionrepository(org_scm_instance.baseui,
537 hgrepo = unionrepo.unionrepository(org_scm_instance.baseui,
538 org_scm_instance.path,
538 org_scm_instance.path,
539 other_scm_instance.path)
539 other_scm_instance.path)
540 else:
540 else:
541 hgrepo = org_scm_instance._repo
541 hgrepo = org_scm_instance._repo
542 show = set(hgrepo.revs('::%ld & !::parents(%s) & !::%s',
542 show = set(hgrepo.revs('::%ld & !::parents(%s) & !::%s',
543 avail_revs, revs[0], targethead))
543 avail_revs, revs[0], targethead))
544 c.update_msg = _('The following additional changes are available on %s:') % c.cs_branch_name
544 c.update_msg = _('The following additional changes are available on %s:') % c.cs_branch_name
545 else:
545 else:
546 show = set()
546 show = set()
547 avail_revs = set() # drop revs[0]
547 avail_revs = set() # drop revs[0]
548 c.update_msg = _('No additional changesets found for iterating on this pull request.')
548 c.update_msg = _('No additional changesets found for iterating on this pull request.')
549
549
550 # TODO: handle branch heads that not are tip-most
550 # TODO: handle branch heads that not are tip-most
551 brevs = org_scm_instance._repo.revs('%s - %ld - %s', c.cs_branch_name, avail_revs, revs[0])
551 brevs = org_scm_instance._repo.revs('%s - %ld - %s', c.cs_branch_name, avail_revs, revs[0])
552 if brevs:
552 if brevs:
553 # also show changesets that are on branch but neither ancestors nor descendants
553 # also show changesets that are on branch but neither ancestors nor descendants
554 show.update(org_scm_instance._repo.revs('::%ld - ::%ld - ::%s', brevs, avail_revs, c.a_branch_name))
554 show.update(org_scm_instance._repo.revs('::%ld - ::%ld - ::%s', brevs, avail_revs, c.a_branch_name))
555 show.add(revs[0]) # make sure graph shows this so we can see how they relate
555 show.add(revs[0]) # make sure graph shows this so we can see how they relate
556 c.update_msg_other = _('Note: Branch %s has another head: %s.') % (c.cs_branch_name,
556 c.update_msg_other = _('Note: Branch %s has another head: %s.') % (c.cs_branch_name,
557 h.short_id(org_scm_instance.get_changeset((max(brevs))).raw_id))
557 h.short_id(org_scm_instance.get_changeset((max(brevs))).raw_id))
558
558
559 avail_show = sorted(show, reverse=True)
559 avail_show = sorted(show, reverse=True)
560
560
561 elif org_scm_instance.alias == 'git':
561 elif org_scm_instance.alias == 'git':
562 c.cs_repo.scm_instance.get_changeset(c.cs_rev) # check it exists - raise ChangesetDoesNotExistError if not
562 c.cs_repo.scm_instance.get_changeset(c.cs_rev) # check it exists - raise ChangesetDoesNotExistError if not
563 c.update_msg = _("Git pull requests don't support iterating yet.")
563 c.update_msg = _("Git pull requests don't support iterating yet.")
564 except ChangesetDoesNotExistError:
564 except ChangesetDoesNotExistError:
565 c.update_msg = _('Error: some changesets not found when displaying pull request from %s.') % c.cs_rev
565 c.update_msg = _('Error: some changesets not found when displaying pull request from %s.') % c.cs_rev
566
566
567 c.avail_revs = avail_revs
567 c.avail_revs = avail_revs
568 c.avail_cs = [org_scm_instance.get_changeset(r) for r in avail_show]
568 c.avail_cs = [org_scm_instance.get_changeset(r) for r in avail_show]
569 c.avail_jsdata = graph_data(org_scm_instance, avail_show)
569 c.avail_jsdata = graph_data(org_scm_instance, avail_show)
570
570
571 raw_ids = [x.raw_id for x in c.cs_ranges]
571 raw_ids = [x.raw_id for x in c.cs_ranges]
572 c.cs_comments = c.cs_repo.get_comments(raw_ids)
572 c.cs_comments = c.cs_repo.get_comments(raw_ids)
573 c.cs_statuses = c.cs_repo.statuses(raw_ids)
573 c.cs_statuses = c.cs_repo.statuses(raw_ids)
574
574
575 ignore_whitespace = request.GET.get('ignorews') == '1'
575 ignore_whitespace = request.GET.get('ignorews') == '1'
576 line_context = safe_int(request.GET.get('context'), 3)
576 line_context = safe_int(request.GET.get('context'), 3)
577 c.ignorews_url = _ignorews_url
577 c.ignorews_url = _ignorews_url
578 c.context_url = _context_url
578 c.context_url = _context_url
579 fulldiff = request.GET.get('fulldiff')
579 fulldiff = request.GET.get('fulldiff')
580 diff_limit = None if fulldiff else self.cut_off_limit
580 diff_limit = None if fulldiff else self.cut_off_limit
581
581
582 # we swap org/other ref since we run a simple diff on one repo
582 # we swap org/other ref since we run a simple diff on one repo
583 log.debug('running diff between %s and %s in %s',
583 log.debug('running diff between %s and %s in %s',
584 c.a_rev, c.cs_rev, org_scm_instance.path)
584 c.a_rev, c.cs_rev, org_scm_instance.path)
585 try:
585 try:
586 raw_diff = diffs.get_diff(org_scm_instance, rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev),
586 raw_diff = diffs.get_diff(org_scm_instance, rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev),
587 ignore_whitespace=ignore_whitespace, context=line_context)
587 ignore_whitespace=ignore_whitespace, context=line_context)
588 except ChangesetDoesNotExistError:
588 except ChangesetDoesNotExistError:
589 raw_diff = _("The diff can't be shown - the PR revisions could not be found.")
589 raw_diff = _("The diff can't be shown - the PR revisions could not be found.")
590 diff_processor = diffs.DiffProcessor(raw_diff or '', diff_limit=diff_limit)
590 diff_processor = diffs.DiffProcessor(raw_diff or '', diff_limit=diff_limit)
591 c.limited_diff = diff_processor.limited_diff
591 c.limited_diff = diff_processor.limited_diff
592 c.file_diff_data = []
592 c.file_diff_data = []
593 c.lines_added = 0
593 c.lines_added = 0
594 c.lines_deleted = 0
594 c.lines_deleted = 0
595
595
596 for f in diff_processor.parsed:
596 for f in diff_processor.parsed:
597 st = f['stats']
597 st = f['stats']
598 c.lines_added += st['added']
598 c.lines_added += st['added']
599 c.lines_deleted += st['deleted']
599 c.lines_deleted += st['deleted']
600 filename = f['filename']
600 filename = f['filename']
601 fid = h.FID('', filename)
601 fid = h.FID('', filename)
602 html_diff = diffs.as_html(enable_comments=True, parsed_lines=[f])
602 html_diff = diffs.as_html(enable_comments=True, parsed_lines=[f])
603 c.file_diff_data.append((fid, None, f['operation'], f['old_filename'], filename, html_diff, st))
603 c.file_diff_data.append((fid, None, f['operation'], f['old_filename'], filename, html_diff, st))
604
604
605 # inline comments
605 # inline comments
606 c.inline_cnt = 0
606 c.inline_cnt = 0
607 c.inline_comments = cc_model.get_inline_comments(
607 c.inline_comments = cc_model.get_inline_comments(
608 c.db_repo.repo_id,
608 c.db_repo.repo_id,
609 pull_request=pull_request_id)
609 pull_request=pull_request_id)
610 # count inline comments
610 # count inline comments
611 for __, lines in c.inline_comments:
611 for __, lines in c.inline_comments:
612 for comments in lines.values():
612 for comments in lines.values():
613 c.inline_cnt += len(comments)
613 c.inline_cnt += len(comments)
614 # comments
614 # comments
615 c.comments = cc_model.get_comments(c.db_repo.repo_id, pull_request=pull_request_id)
615 c.comments = cc_model.get_comments(c.db_repo.repo_id, pull_request=pull_request_id)
616
616
617 # (badly named) pull-request status calculation based on reviewer votes
617 # (badly named) pull-request status calculation based on reviewer votes
618 (c.pull_request_reviewers,
618 (c.pull_request_reviewers,
619 c.pull_request_pending_reviewers,
619 c.pull_request_pending_reviewers,
620 c.current_voting_result,
620 c.current_voting_result,
621 ) = cs_model.calculate_pull_request_result(c.pull_request)
621 ) = cs_model.calculate_pull_request_result(c.pull_request)
622 c.changeset_statuses = ChangesetStatus.STATUSES
622 c.changeset_statuses = ChangesetStatus.STATUSES
623
623
624 c.is_ajax_preview = False
624 c.is_ajax_preview = False
625 c.ancestors = None # [c.a_rev] ... but that is shown in an other way
625 c.ancestors = None # [c.a_rev] ... but that is shown in an other way
626 return render('/pullrequests/pullrequest_show.html')
626 return render('/pullrequests/pullrequest_show.html')
627
627
628 @LoginRequired()
628 @LoginRequired()
629 @HasRepoPermissionLevelDecorator('read')
629 @HasRepoPermissionLevelDecorator('read')
630 @jsonify
630 @jsonify
631 def comment(self, repo_name, pull_request_id):
631 def comment(self, repo_name, pull_request_id):
632 pull_request = PullRequest.get_or_404(pull_request_id)
632 pull_request = PullRequest.get_or_404(pull_request_id)
633
633
634 status = request.POST.get('changeset_status')
634 status = request.POST.get('changeset_status')
635 close_pr = request.POST.get('save_close')
635 close_pr = request.POST.get('save_close')
636 delete = request.POST.get('save_delete')
636 delete = request.POST.get('save_delete')
637 f_path = request.POST.get('f_path')
637 f_path = request.POST.get('f_path')
638 line_no = request.POST.get('line')
638 line_no = request.POST.get('line')
639
639
640 if (status or close_pr or delete) and (f_path or line_no):
640 if (status or close_pr or delete) and (f_path or line_no):
641 # status votes and closing is only possible in general comments
641 # status votes and closing is only possible in general comments
642 raise HTTPBadRequest()
642 raise HTTPBadRequest()
643
643
644 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
644 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
645 if not allowed_to_change_status:
645 if not allowed_to_change_status:
646 if status or close_pr:
646 if status or close_pr:
647 h.flash(_('No permission to change pull request status'), 'error')
647 h.flash(_('No permission to change pull request status'), 'error')
648 raise HTTPForbidden()
648 raise HTTPForbidden()
649
649
650 if delete == "delete":
650 if delete == "delete":
651 if (pull_request.owner_id == request.authuser.user_id or
651 if (pull_request.owner_id == request.authuser.user_id or
652 h.HasPermissionAny('hg.admin')() or
652 h.HasPermissionAny('hg.admin')() or
653 h.HasRepoPermissionLevel('admin')(pull_request.org_repo.repo_name) or
653 h.HasRepoPermissionLevel('admin')(pull_request.org_repo.repo_name) or
654 h.HasRepoPermissionLevel('admin')(pull_request.other_repo.repo_name)
654 h.HasRepoPermissionLevel('admin')(pull_request.other_repo.repo_name)
655 ) and not pull_request.is_closed():
655 ) and not pull_request.is_closed():
656 PullRequestModel().delete(pull_request)
656 PullRequestModel().delete(pull_request)
657 Session().commit()
657 Session().commit()
658 h.flash(_('Successfully deleted pull request %s') % pull_request_id,
658 h.flash(_('Successfully deleted pull request %s') % pull_request_id,
659 category='success')
659 category='success')
660 return {
660 return {
661 'location': url('my_pullrequests'), # or repo pr list?
661 'location': url('my_pullrequests'), # or repo pr list?
662 }
662 }
663 raise HTTPFound(location=url('my_pullrequests')) # or repo pr list?
663 raise HTTPFound(location=url('my_pullrequests')) # or repo pr list?
664 raise HTTPForbidden()
664 raise HTTPForbidden()
665
665
666 text = request.POST.get('text', '').strip()
666 text = request.POST.get('text', '').strip()
667
667
668 comment = create_comment(
668 comment = create_comment(
669 text,
669 text,
670 status,
670 status,
671 pull_request_id=pull_request_id,
671 pull_request_id=pull_request_id,
672 f_path=f_path,
672 f_path=f_path,
673 line_no=line_no,
673 line_no=line_no,
674 closing_pr=close_pr,
674 closing_pr=close_pr,
675 )
675 )
676
676
677 action_logger(request.authuser,
677 action_logger(request.authuser,
678 'user_commented_pull_request:%s' % pull_request_id,
678 'user_commented_pull_request:%s' % pull_request_id,
679 c.db_repo, request.ip_addr)
679 c.db_repo, request.ip_addr)
680
680
681 if status:
681 if status:
682 ChangesetStatusModel().set_status(
682 ChangesetStatusModel().set_status(
683 c.db_repo.repo_id,
683 c.db_repo.repo_id,
684 status,
684 status,
685 request.authuser.user_id,
685 request.authuser.user_id,
686 comment,
686 comment,
687 pull_request=pull_request_id
687 pull_request=pull_request_id
688 )
688 )
689
689
690 if close_pr:
690 if close_pr:
691 PullRequestModel().close_pull_request(pull_request_id)
691 PullRequestModel().close_pull_request(pull_request_id)
692 action_logger(request.authuser,
692 action_logger(request.authuser,
693 'user_closed_pull_request:%s' % pull_request_id,
693 'user_closed_pull_request:%s' % pull_request_id,
694 c.db_repo, request.ip_addr)
694 c.db_repo, request.ip_addr)
695
695
696 Session().commit()
696 Session().commit()
697
697
698 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
698 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
699 raise HTTPFound(location=pull_request.url())
699 raise HTTPFound(location=pull_request.url())
700
700
701 data = {
701 data = {
702 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
702 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
703 }
703 }
704 if comment is not None:
704 if comment is not None:
705 c.comment = comment
705 c.comment = comment
706 data.update(comment.get_dict())
706 data.update(comment.get_dict())
707 data.update({'rendered_text':
707 data.update({'rendered_text':
708 render('changeset/changeset_comment_block.html')})
708 render('changeset/changeset_comment_block.html')})
709
709
710 return data
710 return data
711
711
712 @LoginRequired()
712 @LoginRequired()
713 @HasRepoPermissionLevelDecorator('read')
713 @HasRepoPermissionLevelDecorator('read')
714 @jsonify
714 @jsonify
715 def delete_comment(self, repo_name, comment_id):
715 def delete_comment(self, repo_name, comment_id):
716 co = ChangesetComment.get(comment_id)
716 co = ChangesetComment.get(comment_id)
717 if co.pull_request.is_closed():
717 if co.pull_request.is_closed():
718 # don't allow deleting comments on closed pull request
718 # don't allow deleting comments on closed pull request
719 raise HTTPForbidden()
719 raise HTTPForbidden()
720
720
721 owner = co.author_id == request.authuser.user_id
721 owner = co.author_id == request.authuser.user_id
722 repo_admin = h.HasRepoPermissionLevel('admin')(c.repo_name)
722 repo_admin = h.HasRepoPermissionLevel('admin')(c.repo_name)
723 if h.HasPermissionAny('hg.admin')() or repo_admin or owner:
723 if h.HasPermissionAny('hg.admin')() or repo_admin or owner:
724 ChangesetCommentsModel().delete(comment=co)
724 ChangesetCommentsModel().delete(comment=co)
725 Session().commit()
725 Session().commit()
726 return True
726 return True
727 else:
727 else:
728 raise HTTPForbidden()
728 raise HTTPForbidden()
@@ -1,1076 +1,1081 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.backends.base
3 vcs.backends.base
4 ~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~
5
5
6 Base for all available scm backends
6 Base for all available scm backends
7
7
8 :created_on: Apr 8, 2010
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
10 """
11
11
12 import datetime
12 import datetime
13 import itertools
13 import itertools
14
14
15 from kallithea.lib.vcs.utils import author_name, author_email, safe_unicode
15 from kallithea.lib.vcs.utils import author_name, author_email, safe_unicode
16 from kallithea.lib.vcs.utils.lazy import LazyProperty
16 from kallithea.lib.vcs.utils.lazy import LazyProperty
17 from kallithea.lib.vcs.utils.helpers import get_dict_for_attrs
17 from kallithea.lib.vcs.utils.helpers import get_dict_for_attrs
18 from kallithea.lib.vcs.conf import settings
18 from kallithea.lib.vcs.conf import settings
19
19
20 from kallithea.lib.vcs.exceptions import (
20 from kallithea.lib.vcs.exceptions import (
21 ChangesetError, EmptyRepositoryError, NodeAlreadyAddedError,
21 ChangesetError, EmptyRepositoryError, NodeAlreadyAddedError,
22 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
22 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
23 NodeDoesNotExistError, NodeNotChangedError, RepositoryError
23 NodeDoesNotExistError, NodeNotChangedError, RepositoryError
24 )
24 )
25
25
26
26
27 class BaseRepository(object):
27 class BaseRepository(object):
28 """
28 """
29 Base Repository for final backends
29 Base Repository for final backends
30
30
31 **Attributes**
31 **Attributes**
32
32
33 ``DEFAULT_BRANCH_NAME``
33 ``DEFAULT_BRANCH_NAME``
34 name of default branch (i.e. "trunk" for svn, "master" for git etc.
34 name of default branch (i.e. "trunk" for svn, "master" for git etc.
35
35
36 ``scm``
36 ``scm``
37 alias of scm, i.e. *git* or *hg*
37 alias of scm, i.e. *git* or *hg*
38
38
39 ``repo``
39 ``repo``
40 object from external api
40 object from external api
41
41
42 ``revisions``
42 ``revisions``
43 list of all available revisions' ids, in ascending order
43 list of all available revisions' ids, in ascending order
44
44
45 ``changesets``
45 ``changesets``
46 storage dict caching returned changesets
46 storage dict caching returned changesets
47
47
48 ``path``
48 ``path``
49 absolute path to the repository
49 absolute path to the repository
50
50
51 ``branches``
51 ``branches``
52 branches as list of changesets
52 branches as list of changesets
53
53
54 ``tags``
54 ``tags``
55 tags as list of changesets
55 tags as list of changesets
56 """
56 """
57 scm = None
57 scm = None
58 DEFAULT_BRANCH_NAME = None
58 DEFAULT_BRANCH_NAME = None
59 EMPTY_CHANGESET = '0' * 40
59 EMPTY_CHANGESET = '0' * 40
60
60
61 def __init__(self, repo_path, create=False, **kwargs):
61 def __init__(self, repo_path, create=False, **kwargs):
62 """
62 """
63 Initializes repository. Raises RepositoryError if repository could
63 Initializes repository. Raises RepositoryError if repository could
64 not be find at the given ``repo_path`` or directory at ``repo_path``
64 not be find at the given ``repo_path`` or directory at ``repo_path``
65 exists and ``create`` is set to True.
65 exists and ``create`` is set to True.
66
66
67 :param repo_path: local path of the repository
67 :param repo_path: local path of the repository
68 :param create=False: if set to True, would try to create repository.
68 :param create=False: if set to True, would try to create repository.
69 :param src_url=None: if set, should be proper url from which repository
69 :param src_url=None: if set, should be proper url from which repository
70 would be cloned; requires ``create`` parameter to be set to True -
70 would be cloned; requires ``create`` parameter to be set to True -
71 raises RepositoryError if src_url is set and create evaluates to
71 raises RepositoryError if src_url is set and create evaluates to
72 False
72 False
73 """
73 """
74 raise NotImplementedError
74 raise NotImplementedError
75
75
76 def __str__(self):
76 def __str__(self):
77 return '<%s at %s>' % (self.__class__.__name__, self.path)
77 return '<%s at %s>' % (self.__class__.__name__, self.path)
78
78
79 def __repr__(self):
79 def __repr__(self):
80 return self.__str__()
80 return self.__str__()
81
81
82 def __len__(self):
82 def __len__(self):
83 return self.count()
83 return self.count()
84
84
85 def __eq__(self, other):
85 def __eq__(self, other):
86 same_instance = isinstance(other, self.__class__)
86 same_instance = isinstance(other, self.__class__)
87 return same_instance and getattr(other, 'path', None) == self.path
87 return same_instance and getattr(other, 'path', None) == self.path
88
88
89 def __ne__(self, other):
89 def __ne__(self, other):
90 return not self.__eq__(other)
90 return not self.__eq__(other)
91
91
92 @LazyProperty
92 @LazyProperty
93 def alias(self):
93 def alias(self):
94 for k, v in settings.BACKENDS.items():
94 for k, v in settings.BACKENDS.items():
95 if v.split('.')[-1] == str(self.__class__.__name__):
95 if v.split('.')[-1] == str(self.__class__.__name__):
96 return k
96 return k
97
97
98 @LazyProperty
98 @LazyProperty
99 def name(self):
99 def name(self):
100 """
100 """
101 Return repository name (without group name)
101 Return repository name (without group name)
102 """
102 """
103 raise NotImplementedError
103 raise NotImplementedError
104
104
105 @property
105 @property
106 def name_unicode(self):
106 def name_unicode(self):
107 return safe_unicode(self.name)
107 return safe_unicode(self.name)
108
108
109 @LazyProperty
109 @LazyProperty
110 def owner(self):
110 def owner(self):
111 raise NotImplementedError
111 raise NotImplementedError
112
112
113 @LazyProperty
113 @LazyProperty
114 def description(self):
114 def description(self):
115 raise NotImplementedError
115 raise NotImplementedError
116
116
117 @LazyProperty
117 @LazyProperty
118 def size(self):
118 def size(self):
119 """
119 """
120 Returns combined size in bytes for all repository files
120 Returns combined size in bytes for all repository files
121 """
121 """
122
122
123 size = 0
123 size = 0
124 try:
124 try:
125 tip = self.get_changeset()
125 tip = self.get_changeset()
126 for topnode, dirs, files in tip.walk('/'):
126 for topnode, dirs, files in tip.walk('/'):
127 for f in files:
127 for f in files:
128 size += tip.get_file_size(f.path)
128 size += tip.get_file_size(f.path)
129
129
130 except RepositoryError as e:
130 except RepositoryError as e:
131 pass
131 pass
132 return size
132 return size
133
133
134 def is_valid(self):
134 def is_valid(self):
135 """
135 """
136 Validates repository.
136 Validates repository.
137 """
137 """
138 raise NotImplementedError
138 raise NotImplementedError
139
139
140 def is_empty(self):
140 def is_empty(self):
141 return self._empty
141 return self._empty
142
142
143 #==========================================================================
143 #==========================================================================
144 # CHANGESETS
144 # CHANGESETS
145 #==========================================================================
145 #==========================================================================
146
146
147 def get_changeset(self, revision=None):
147 def get_changeset(self, revision=None):
148 """
148 """
149 Returns instance of ``Changeset`` class. If ``revision`` is None, most
149 Returns instance of ``Changeset`` class. If ``revision`` is None, most
150 recent changeset is returned.
150 recent changeset is returned.
151
151
152 :raises ``EmptyRepositoryError``: if there are no revisions
152 :raises ``EmptyRepositoryError``: if there are no revisions
153 """
153 """
154 raise NotImplementedError
154 raise NotImplementedError
155
155
156 def __iter__(self):
156 def __iter__(self):
157 """
157 """
158 Allows Repository objects to be iterated.
158 Allows Repository objects to be iterated.
159
159
160 *Requires* implementation of ``__getitem__`` method.
160 *Requires* implementation of ``__getitem__`` method.
161 """
161 """
162 for revision in self.revisions:
162 for revision in self.revisions:
163 yield self.get_changeset(revision)
163 yield self.get_changeset(revision)
164
164
165 def get_changesets(self, start=None, end=None, start_date=None,
165 def get_changesets(self, start=None, end=None, start_date=None,
166 end_date=None, branch_name=None, reverse=False):
166 end_date=None, branch_name=None, reverse=False):
167 """
167 """
168 Returns iterator of ``BaseChangeset`` objects from start to end,
168 Returns iterator of ``BaseChangeset`` objects from start to end,
169 both inclusive.
169 both inclusive.
170
170
171 :param start: None or str
171 :param start: None or str
172 :param end: None or str
172 :param end: None or str
173 :param start_date:
173 :param start_date:
174 :param end_date:
174 :param end_date:
175 :param branch_name:
175 :param branch_name:
176 :param reversed:
176 :param reversed:
177 """
177 """
178 raise NotImplementedError
178 raise NotImplementedError
179
179
180 def __getslice__(self, i, j):
180 def __getslice__(self, i, j):
181 """
181 """
182 Returns a iterator of sliced repository
182 Returns a iterator of sliced repository
183 """
183 """
184 for rev in self.revisions[i:j]:
184 for rev in self.revisions[i:j]:
185 yield self.get_changeset(rev)
185 yield self.get_changeset(rev)
186
186
187 def __getitem__(self, key):
187 def __getitem__(self, key):
188 return self.get_changeset(key)
188 return self.get_changeset(key)
189
189
190 def count(self):
190 def count(self):
191 return len(self.revisions)
191 return len(self.revisions)
192
192
193 def tag(self, name, user, revision=None, message=None, date=None, **opts):
193 def tag(self, name, user, revision=None, message=None, date=None, **opts):
194 """
194 """
195 Creates and returns a tag for the given ``revision``.
195 Creates and returns a tag for the given ``revision``.
196
196
197 :param name: name for new tag
197 :param name: name for new tag
198 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
198 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
199 :param revision: changeset id for which new tag would be created
199 :param revision: changeset id for which new tag would be created
200 :param message: message of the tag's commit
200 :param message: message of the tag's commit
201 :param date: date of tag's commit
201 :param date: date of tag's commit
202
202
203 :raises TagAlreadyExistError: if tag with same name already exists
203 :raises TagAlreadyExistError: if tag with same name already exists
204 """
204 """
205 raise NotImplementedError
205 raise NotImplementedError
206
206
207 def remove_tag(self, name, user, message=None, date=None):
207 def remove_tag(self, name, user, message=None, date=None):
208 """
208 """
209 Removes tag with the given ``name``.
209 Removes tag with the given ``name``.
210
210
211 :param name: name of the tag to be removed
211 :param name: name of the tag to be removed
212 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
212 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
213 :param message: message of the tag's removal commit
213 :param message: message of the tag's removal commit
214 :param date: date of tag's removal commit
214 :param date: date of tag's removal commit
215
215
216 :raises TagDoesNotExistError: if tag with given name does not exists
216 :raises TagDoesNotExistError: if tag with given name does not exists
217 """
217 """
218 raise NotImplementedError
218 raise NotImplementedError
219
219
220 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
220 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
221 context=3):
221 context=3):
222 """
222 """
223 Returns (git like) *diff*, as plain text. Shows changes introduced by
223 Returns (git like) *diff*, as plain text. Shows changes introduced by
224 ``rev2`` since ``rev1``.
224 ``rev2`` since ``rev1``.
225
225
226 :param rev1: Entry point from which diff is shown. Can be
226 :param rev1: Entry point from which diff is shown. Can be
227 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
227 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
228 the changes since empty state of the repository until ``rev2``
228 the changes since empty state of the repository until ``rev2``
229 :param rev2: Until which revision changes should be shown.
229 :param rev2: Until which revision changes should be shown.
230 :param ignore_whitespace: If set to ``True``, would not show whitespace
230 :param ignore_whitespace: If set to ``True``, would not show whitespace
231 changes. Defaults to ``False``.
231 changes. Defaults to ``False``.
232 :param context: How many lines before/after changed lines should be
232 :param context: How many lines before/after changed lines should be
233 shown. Defaults to ``3``.
233 shown. Defaults to ``3``.
234 """
234 """
235 raise NotImplementedError
235 raise NotImplementedError
236
236
237 # ========== #
237 # ========== #
238 # COMMIT API #
238 # COMMIT API #
239 # ========== #
239 # ========== #
240
240
241 @LazyProperty
241 @LazyProperty
242 def in_memory_changeset(self):
242 def in_memory_changeset(self):
243 """
243 """
244 Returns ``InMemoryChangeset`` object for this repository.
244 Returns ``InMemoryChangeset`` object for this repository.
245 """
245 """
246 raise NotImplementedError
246 raise NotImplementedError
247
247
248 def add(self, filenode, **kwargs):
248 def add(self, filenode, **kwargs):
249 """
249 """
250 Commit api function that will add given ``FileNode`` into this
250 Commit api function that will add given ``FileNode`` into this
251 repository.
251 repository.
252
252
253 :raises ``NodeAlreadyExistsError``: if there is a file with same path
253 :raises ``NodeAlreadyExistsError``: if there is a file with same path
254 already in repository
254 already in repository
255 :raises ``NodeAlreadyAddedError``: if given node is already marked as
255 :raises ``NodeAlreadyAddedError``: if given node is already marked as
256 *added*
256 *added*
257 """
257 """
258 raise NotImplementedError
258 raise NotImplementedError
259
259
260 def remove(self, filenode, **kwargs):
260 def remove(self, filenode, **kwargs):
261 """
261 """
262 Commit api function that will remove given ``FileNode`` into this
262 Commit api function that will remove given ``FileNode`` into this
263 repository.
263 repository.
264
264
265 :raises ``EmptyRepositoryError``: if there are no changesets yet
265 :raises ``EmptyRepositoryError``: if there are no changesets yet
266 :raises ``NodeDoesNotExistError``: if there is no file with given path
266 :raises ``NodeDoesNotExistError``: if there is no file with given path
267 """
267 """
268 raise NotImplementedError
268 raise NotImplementedError
269
269
270 def commit(self, message, **kwargs):
270 def commit(self, message, **kwargs):
271 """
271 """
272 Persists current changes made on this repository and returns newly
272 Persists current changes made on this repository and returns newly
273 created changeset.
273 created changeset.
274
274
275 :raises ``NothingChangedError``: if no changes has been made
275 :raises ``NothingChangedError``: if no changes has been made
276 """
276 """
277 raise NotImplementedError
277 raise NotImplementedError
278
278
279 def get_state(self):
279 def get_state(self):
280 """
280 """
281 Returns dictionary with ``added``, ``changed`` and ``removed`` lists
281 Returns dictionary with ``added``, ``changed`` and ``removed`` lists
282 containing ``FileNode`` objects.
282 containing ``FileNode`` objects.
283 """
283 """
284 raise NotImplementedError
284 raise NotImplementedError
285
285
286 def get_config_value(self, section, name, config_file=None):
286 def get_config_value(self, section, name, config_file=None):
287 """
287 """
288 Returns configuration value for a given [``section``] and ``name``.
288 Returns configuration value for a given [``section``] and ``name``.
289
289
290 :param section: Section we want to retrieve value from
290 :param section: Section we want to retrieve value from
291 :param name: Name of configuration we want to retrieve
291 :param name: Name of configuration we want to retrieve
292 :param config_file: A path to file which should be used to retrieve
292 :param config_file: A path to file which should be used to retrieve
293 configuration from (might also be a list of file paths)
293 configuration from (might also be a list of file paths)
294 """
294 """
295 raise NotImplementedError
295 raise NotImplementedError
296
296
297 def get_user_name(self, config_file=None):
297 def get_user_name(self, config_file=None):
298 """
298 """
299 Returns user's name from global configuration file.
299 Returns user's name from global configuration file.
300
300
301 :param config_file: A path to file which should be used to retrieve
301 :param config_file: A path to file which should be used to retrieve
302 configuration from (might also be a list of file paths)
302 configuration from (might also be a list of file paths)
303 """
303 """
304 raise NotImplementedError
304 raise NotImplementedError
305
305
306 def get_user_email(self, config_file=None):
306 def get_user_email(self, config_file=None):
307 """
307 """
308 Returns user's email from global configuration file.
308 Returns user's email from global configuration file.
309
309
310 :param config_file: A path to file which should be used to retrieve
310 :param config_file: A path to file which should be used to retrieve
311 configuration from (might also be a list of file paths)
311 configuration from (might also be a list of file paths)
312 """
312 """
313 raise NotImplementedError
313 raise NotImplementedError
314
314
315 # =========== #
315 # =========== #
316 # WORKDIR API #
316 # WORKDIR API #
317 # =========== #
317 # =========== #
318
318
319 @LazyProperty
319 @LazyProperty
320 def workdir(self):
320 def workdir(self):
321 """
321 """
322 Returns ``Workdir`` instance for this repository.
322 Returns ``Workdir`` instance for this repository.
323 """
323 """
324 raise NotImplementedError
324 raise NotImplementedError
325
325
326
326
327 class BaseChangeset(object):
327 class BaseChangeset(object):
328 """
328 """
329 Each backend should implement it's changeset representation.
329 Each backend should implement it's changeset representation.
330
330
331 **Attributes**
331 **Attributes**
332
332
333 ``repository``
333 ``repository``
334 repository object within which changeset exists
334 repository object within which changeset exists
335
335
336 ``id``
336 ``id``
337 may be ``raw_id`` or i.e. for mercurial's tip just ``tip``
337 may be ``raw_id`` or i.e. for mercurial's tip just ``tip``
338
338
339 ``raw_id``
339 ``raw_id``
340 raw changeset representation (i.e. full 40 length sha for git
340 raw changeset representation (i.e. full 40 length sha for git
341 backend)
341 backend)
342
342
343 ``short_id``
343 ``short_id``
344 shortened (if apply) version of ``raw_id``; it would be simple
344 shortened (if apply) version of ``raw_id``; it would be simple
345 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
345 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
346 as ``raw_id`` for subversion
346 as ``raw_id`` for subversion
347
347
348 ``revision``
348 ``revision``
349 revision number as integer
349 revision number as integer
350
350
351 ``files``
351 ``files``
352 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
352 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
353
353
354 ``dirs``
354 ``dirs``
355 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
355 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
356
356
357 ``nodes``
357 ``nodes``
358 combined list of ``Node`` objects
358 combined list of ``Node`` objects
359
359
360 ``author``
360 ``author``
361 author of the changeset, as unicode
361 author of the changeset, as unicode
362
362
363 ``message``
363 ``message``
364 message of the changeset, as unicode
364 message of the changeset, as unicode
365
365
366 ``parents``
366 ``parents``
367 list of parent changesets
367 list of parent changesets
368
368
369 ``last``
369 ``last``
370 ``True`` if this is last changeset in repository, ``False``
370 ``True`` if this is last changeset in repository, ``False``
371 otherwise; trying to access this attribute while there is no
371 otherwise; trying to access this attribute while there is no
372 changesets would raise ``EmptyRepositoryError``
372 changesets would raise ``EmptyRepositoryError``
373 """
373 """
374 def __str__(self):
374 def __str__(self):
375 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
375 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
376 self.short_id)
376 self.short_id)
377
377
378 def __repr__(self):
378 def __repr__(self):
379 return self.__str__()
379 return self.__str__()
380
380
381 def __unicode__(self):
381 def __unicode__(self):
382 return u'%s:%s' % (self.revision, self.short_id)
382 return u'%s:%s' % (self.revision, self.short_id)
383
383
384 def __eq__(self, other):
384 def __eq__(self, other):
385 return self.raw_id == other.raw_id
385 return self.raw_id == other.raw_id
386
386
387 def __json__(self, with_file_list=False):
387 def __json__(self, with_file_list=False):
388 if with_file_list:
388 if with_file_list:
389 return dict(
389 return dict(
390 short_id=self.short_id,
390 short_id=self.short_id,
391 raw_id=self.raw_id,
391 raw_id=self.raw_id,
392 revision=self.revision,
392 revision=self.revision,
393 message=self.message,
393 message=self.message,
394 date=self.date,
394 date=self.date,
395 author=self.author,
395 author=self.author,
396 added=[safe_unicode(el.path) for el in self.added],
396 added=[safe_unicode(el.path) for el in self.added],
397 changed=[safe_unicode(el.path) for el in self.changed],
397 changed=[safe_unicode(el.path) for el in self.changed],
398 removed=[safe_unicode(el.path) for el in self.removed],
398 removed=[safe_unicode(el.path) for el in self.removed],
399 )
399 )
400 else:
400 else:
401 return dict(
401 return dict(
402 short_id=self.short_id,
402 short_id=self.short_id,
403 raw_id=self.raw_id,
403 raw_id=self.raw_id,
404 revision=self.revision,
404 revision=self.revision,
405 message=self.message,
405 message=self.message,
406 date=self.date,
406 date=self.date,
407 author=self.author,
407 author=self.author,
408 )
408 )
409
409
410 @LazyProperty
410 @LazyProperty
411 def last(self):
411 def last(self):
412 if self.repository is None:
412 if self.repository is None:
413 raise ChangesetError("Cannot check if it's most recent revision")
413 raise ChangesetError("Cannot check if it's most recent revision")
414 return self.raw_id == self.repository.revisions[-1]
414 return self.raw_id == self.repository.revisions[-1]
415
415
416 @LazyProperty
416 @LazyProperty
417 def parents(self):
417 def parents(self):
418 """
418 """
419 Returns list of parents changesets.
419 Returns list of parents changesets.
420 """
420 """
421 raise NotImplementedError
421 raise NotImplementedError
422
422
423 @LazyProperty
423 @LazyProperty
424 def children(self):
424 def children(self):
425 """
425 """
426 Returns list of children changesets.
426 Returns list of children changesets.
427 """
427 """
428 raise NotImplementedError
428 raise NotImplementedError
429
429
430 @LazyProperty
430 @LazyProperty
431 def id(self):
431 def id(self):
432 """
432 """
433 Returns string identifying this changeset.
433 Returns string identifying this changeset.
434 """
434 """
435 raise NotImplementedError
435 raise NotImplementedError
436
436
437 @LazyProperty
437 @LazyProperty
438 def raw_id(self):
438 def raw_id(self):
439 """
439 """
440 Returns raw string identifying this changeset.
440 Returns raw string identifying this changeset.
441 """
441 """
442 raise NotImplementedError
442 raise NotImplementedError
443
443
444 @LazyProperty
444 @LazyProperty
445 def short_id(self):
445 def short_id(self):
446 """
446 """
447 Returns shortened version of ``raw_id`` attribute, as string,
447 Returns shortened version of ``raw_id`` attribute, as string,
448 identifying this changeset, useful for web representation.
448 identifying this changeset, useful for web representation.
449 """
449 """
450 raise NotImplementedError
450 raise NotImplementedError
451
451
452 @LazyProperty
452 @LazyProperty
453 def revision(self):
453 def revision(self):
454 """
454 """
455 Returns integer identifying this changeset.
455 Returns integer identifying this changeset.
456
456
457 """
457 """
458 raise NotImplementedError
458 raise NotImplementedError
459
459
460 @LazyProperty
460 @LazyProperty
461 def committer(self):
461 def committer(self):
462 """
462 """
463 Returns Committer for given commit
463 Returns Committer for given commit
464 """
464 """
465
465
466 raise NotImplementedError
466 raise NotImplementedError
467
467
468 @LazyProperty
468 @LazyProperty
469 def committer_name(self):
469 def committer_name(self):
470 """
470 """
471 Returns Author name for given commit
471 Returns Author name for given commit
472 """
472 """
473
473
474 return author_name(self.committer)
474 return author_name(self.committer)
475
475
476 @LazyProperty
476 @LazyProperty
477 def committer_email(self):
477 def committer_email(self):
478 """
478 """
479 Returns Author email address for given commit
479 Returns Author email address for given commit
480 """
480 """
481
481
482 return author_email(self.committer)
482 return author_email(self.committer)
483
483
484 @LazyProperty
484 @LazyProperty
485 def author(self):
485 def author(self):
486 """
486 """
487 Returns Author for given commit
487 Returns Author for given commit
488 """
488 """
489
489
490 raise NotImplementedError
490 raise NotImplementedError
491
491
492 @LazyProperty
492 @LazyProperty
493 def author_name(self):
493 def author_name(self):
494 """
494 """
495 Returns Author name for given commit
495 Returns Author name for given commit
496 """
496 """
497
497
498 return author_name(self.author)
498 return author_name(self.author)
499
499
500 @LazyProperty
500 @LazyProperty
501 def author_email(self):
501 def author_email(self):
502 """
502 """
503 Returns Author email address for given commit
503 Returns Author email address for given commit
504 """
504 """
505
505
506 return author_email(self.author)
506 return author_email(self.author)
507
507
508 def get_file_mode(self, path):
508 def get_file_mode(self, path):
509 """
509 """
510 Returns stat mode of the file at the given ``path``.
510 Returns stat mode of the file at the given ``path``.
511 """
511 """
512 raise NotImplementedError
512 raise NotImplementedError
513
513
514 def get_file_content(self, path):
514 def get_file_content(self, path):
515 """
515 """
516 Returns content of the file at the given ``path``.
516 Returns content of the file at the given ``path``.
517 """
517 """
518 raise NotImplementedError
518 raise NotImplementedError
519
519
520 def get_file_size(self, path):
520 def get_file_size(self, path):
521 """
521 """
522 Returns size of the file at the given ``path``.
522 Returns size of the file at the given ``path``.
523 """
523 """
524 raise NotImplementedError
524 raise NotImplementedError
525
525
526 def get_file_changeset(self, path):
526 def get_file_changeset(self, path):
527 """
527 """
528 Returns last commit of the file at the given ``path``.
528 Returns last commit of the file at the given ``path``.
529 """
529 """
530 raise NotImplementedError
530 raise NotImplementedError
531
531
532 def get_file_history(self, path):
532 def get_file_history(self, path):
533 """
533 """
534 Returns history of file as reversed list of ``Changeset`` objects for
534 Returns history of file as reversed list of ``Changeset`` objects for
535 which file at given ``path`` has been modified.
535 which file at given ``path`` has been modified.
536 """
536 """
537 raise NotImplementedError
537 raise NotImplementedError
538
538
539 def get_nodes(self, path):
539 def get_nodes(self, path):
540 """
540 """
541 Returns combined ``DirNode`` and ``FileNode`` objects list representing
541 Returns combined ``DirNode`` and ``FileNode`` objects list representing
542 state of changeset at the given ``path``.
542 state of changeset at the given ``path``.
543
543
544 :raises ``ChangesetError``: if node at the given ``path`` is not
544 :raises ``ChangesetError``: if node at the given ``path`` is not
545 instance of ``DirNode``
545 instance of ``DirNode``
546 """
546 """
547 raise NotImplementedError
547 raise NotImplementedError
548
548
549 def get_node(self, path):
549 def get_node(self, path):
550 """
550 """
551 Returns ``Node`` object from the given ``path``.
551 Returns ``Node`` object from the given ``path``.
552
552
553 :raises ``NodeDoesNotExistError``: if there is no node at the given
553 :raises ``NodeDoesNotExistError``: if there is no node at the given
554 ``path``
554 ``path``
555 """
555 """
556 raise NotImplementedError
556 raise NotImplementedError
557
557
558 def fill_archive(self, stream=None, kind='tgz', prefix=None):
558 def fill_archive(self, stream=None, kind='tgz', prefix=None):
559 """
559 """
560 Fills up given stream.
560 Fills up given stream.
561
561
562 :param stream: file like object.
562 :param stream: file like object.
563 :param kind: one of following: ``zip``, ``tar``, ``tgz``
563 :param kind: one of following: ``zip``, ``tar``, ``tgz``
564 or ``tbz2``. Default: ``tgz``.
564 or ``tbz2``. Default: ``tgz``.
565 :param prefix: name of root directory in archive.
565 :param prefix: name of root directory in archive.
566 Default is repository name and changeset's raw_id joined with dash.
566 Default is repository name and changeset's raw_id joined with dash.
567
567
568 repo-tip.<kind>
568 repo-tip.<kind>
569 """
569 """
570
570
571 raise NotImplementedError
571 raise NotImplementedError
572
572
573 def get_chunked_archive(self, **kwargs):
573 def get_chunked_archive(self, **kwargs):
574 """
574 """
575 Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
575 Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
576
576
577 :param chunk_size: extra parameter which controls size of returned
577 :param chunk_size: extra parameter which controls size of returned
578 chunks. Default:8k.
578 chunks. Default:8k.
579 """
579 """
580
580
581 chunk_size = kwargs.pop('chunk_size', 8192)
581 chunk_size = kwargs.pop('chunk_size', 8192)
582 stream = kwargs.get('stream')
582 stream = kwargs.get('stream')
583 self.fill_archive(**kwargs)
583 self.fill_archive(**kwargs)
584 while True:
584 while True:
585 data = stream.read(chunk_size)
585 data = stream.read(chunk_size)
586 if not data:
586 if not data:
587 break
587 break
588 yield data
588 yield data
589
589
590 @LazyProperty
590 @LazyProperty
591 def root(self):
591 def root(self):
592 """
592 """
593 Returns ``RootNode`` object for this changeset.
593 Returns ``RootNode`` object for this changeset.
594 """
594 """
595 return self.get_node('')
595 return self.get_node('')
596
596
597 def next(self, branch=None):
597 def next(self, branch=None):
598 """
598 """
599 Returns next changeset from current, if branch is gives it will return
599 Returns next changeset from current, if branch is gives it will return
600 next changeset belonging to this branch
600 next changeset belonging to this branch
601
601
602 :param branch: show changesets within the given named branch
602 :param branch: show changesets within the given named branch
603 """
603 """
604 raise NotImplementedError
604 raise NotImplementedError
605
605
606 def prev(self, branch=None):
606 def prev(self, branch=None):
607 """
607 """
608 Returns previous changeset from current, if branch is gives it will
608 Returns previous changeset from current, if branch is gives it will
609 return previous changeset belonging to this branch
609 return previous changeset belonging to this branch
610
610
611 :param branch: show changesets within the given named branch
611 :param branch: show changesets within the given named branch
612 """
612 """
613 raise NotImplementedError
613 raise NotImplementedError
614
614
615 @LazyProperty
615 @LazyProperty
616 def added(self):
616 def added(self):
617 """
617 """
618 Returns list of added ``FileNode`` objects.
618 Returns list of added ``FileNode`` objects.
619 """
619 """
620 raise NotImplementedError
620 raise NotImplementedError
621
621
622 @LazyProperty
622 @LazyProperty
623 def changed(self):
623 def changed(self):
624 """
624 """
625 Returns list of modified ``FileNode`` objects.
625 Returns list of modified ``FileNode`` objects.
626 """
626 """
627 raise NotImplementedError
627 raise NotImplementedError
628
628
629 @LazyProperty
629 @LazyProperty
630 def removed(self):
630 def removed(self):
631 """
631 """
632 Returns list of removed ``FileNode`` objects.
632 Returns list of removed ``FileNode`` objects.
633 """
633 """
634 raise NotImplementedError
634 raise NotImplementedError
635
635
636 @LazyProperty
636 @LazyProperty
637 def size(self):
637 def size(self):
638 """
638 """
639 Returns total number of bytes from contents of all filenodes.
639 Returns total number of bytes from contents of all filenodes.
640 """
640 """
641 return sum((node.size for node in self.get_filenodes_generator()))
641 return sum((node.size for node in self.get_filenodes_generator()))
642
642
643 def walk(self, topurl=''):
643 def walk(self, topurl=''):
644 """
644 """
645 Similar to os.walk method. Instead of filesystem it walks through
645 Similar to os.walk method. Instead of filesystem it walks through
646 changeset starting at given ``topurl``. Returns generator of tuples
646 changeset starting at given ``topurl``. Returns generator of tuples
647 (topnode, dirnodes, filenodes).
647 (topnode, dirnodes, filenodes).
648 """
648 """
649 topnode = self.get_node(topurl)
649 topnode = self.get_node(topurl)
650 yield (topnode, topnode.dirs, topnode.files)
650 yield (topnode, topnode.dirs, topnode.files)
651 for dirnode in topnode.dirs:
651 for dirnode in topnode.dirs:
652 for tup in self.walk(dirnode.path):
652 for tup in self.walk(dirnode.path):
653 yield tup
653 yield tup
654
654
655 def get_filenodes_generator(self):
655 def get_filenodes_generator(self):
656 """
656 """
657 Returns generator that yields *all* file nodes.
657 Returns generator that yields *all* file nodes.
658 """
658 """
659 for topnode, dirs, files in self.walk():
659 for topnode, dirs, files in self.walk():
660 for node in files:
660 for node in files:
661 yield node
661 yield node
662
662
663 def as_dict(self):
663 def as_dict(self):
664 """
664 """
665 Returns dictionary with changeset's attributes and their values.
665 Returns dictionary with changeset's attributes and their values.
666 """
666 """
667 data = get_dict_for_attrs(self, ['id', 'raw_id', 'short_id',
667 data = get_dict_for_attrs(self, ['id', 'raw_id', 'short_id',
668 'revision', 'date', 'message'])
668 'revision', 'date', 'message'])
669 data['author'] = {'name': self.author_name, 'email': self.author_email}
669 data['author'] = {'name': self.author_name, 'email': self.author_email}
670 data['added'] = [safe_unicode(node.path) for node in self.added]
670 data['added'] = [safe_unicode(node.path) for node in self.added]
671 data['changed'] = [safe_unicode(node.path) for node in self.changed]
671 data['changed'] = [safe_unicode(node.path) for node in self.changed]
672 data['removed'] = [safe_unicode(node.path) for node in self.removed]
672 data['removed'] = [safe_unicode(node.path) for node in self.removed]
673 return data
673 return data
674
674
675 @LazyProperty
675 @LazyProperty
676 def closesbranch(self):
676 def closesbranch(self):
677 return False
677 return False
678
678
679 @LazyProperty
679 @LazyProperty
680 def obsolete(self):
680 def obsolete(self):
681 return False
681 return False
682
682
683 @LazyProperty
683 @LazyProperty
684 def bumped(self):
684 def bumped(self):
685 return False
685 return False
686
686
687 @LazyProperty
687 @LazyProperty
688 def divergent(self):
688 def divergent(self):
689 return False
689 return False
690
690
691 @LazyProperty
691 @LazyProperty
692 def extinct(self):
692 def extinct(self):
693 return False
693 return False
694
694
695 @LazyProperty
695 @LazyProperty
696 def unstable(self):
696 def unstable(self):
697 return False
697 return False
698
698
699 @LazyProperty
699 @LazyProperty
700 def phase(self):
700 def phase(self):
701 return ''
701 return ''
702
702
703
703
704 class BaseWorkdir(object):
704 class BaseWorkdir(object):
705 """
705 """
706 Working directory representation of single repository.
706 Working directory representation of single repository.
707
707
708 :attribute: repository: repository object of working directory
708 :attribute: repository: repository object of working directory
709 """
709 """
710
710
711 def __init__(self, repository):
711 def __init__(self, repository):
712 self.repository = repository
712 self.repository = repository
713
713
714 def get_branch(self):
714 def get_branch(self):
715 """
715 """
716 Returns name of current branch.
716 Returns name of current branch.
717 """
717 """
718 raise NotImplementedError
718 raise NotImplementedError
719
719
720 def get_changeset(self):
720 def get_changeset(self):
721 """
721 """
722 Returns current changeset.
722 Returns current changeset.
723 """
723 """
724 raise NotImplementedError
724 raise NotImplementedError
725
725
726 def get_added(self):
726 def get_added(self):
727 """
727 """
728 Returns list of ``FileNode`` objects marked as *new* in working
728 Returns list of ``FileNode`` objects marked as *new* in working
729 directory.
729 directory.
730 """
730 """
731 raise NotImplementedError
731 raise NotImplementedError
732
732
733 def get_changed(self):
733 def get_changed(self):
734 """
734 """
735 Returns list of ``FileNode`` objects *changed* in working directory.
735 Returns list of ``FileNode`` objects *changed* in working directory.
736 """
736 """
737 raise NotImplementedError
737 raise NotImplementedError
738
738
739 def get_removed(self):
739 def get_removed(self):
740 """
740 """
741 Returns list of ``RemovedFileNode`` objects marked as *removed* in
741 Returns list of ``RemovedFileNode`` objects marked as *removed* in
742 working directory.
742 working directory.
743 """
743 """
744 raise NotImplementedError
744 raise NotImplementedError
745
745
746 def get_untracked(self):
746 def get_untracked(self):
747 """
747 """
748 Returns list of ``FileNode`` objects which are present within working
748 Returns list of ``FileNode`` objects which are present within working
749 directory however are not tracked by repository.
749 directory however are not tracked by repository.
750 """
750 """
751 raise NotImplementedError
751 raise NotImplementedError
752
752
753 def get_status(self):
753 def get_status(self):
754 """
754 """
755 Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
755 Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
756 lists.
756 lists.
757 """
757 """
758 raise NotImplementedError
758 raise NotImplementedError
759
759
760 def commit(self, message, **kwargs):
760 def commit(self, message, **kwargs):
761 """
761 """
762 Commits local (from working directory) changes and returns newly
762 Commits local (from working directory) changes and returns newly
763 created
763 created
764 ``Changeset``. Updates repository's ``revisions`` list.
764 ``Changeset``. Updates repository's ``revisions`` list.
765
765
766 :raises ``CommitError``: if any error occurs while committing
766 :raises ``CommitError``: if any error occurs while committing
767 """
767 """
768 raise NotImplementedError
768 raise NotImplementedError
769
769
770 def update(self, revision=None):
770 def update(self, revision=None):
771 """
771 """
772 Fetches content of the given revision and populates it within working
772 Fetches content of the given revision and populates it within working
773 directory.
773 directory.
774 """
774 """
775 raise NotImplementedError
775 raise NotImplementedError
776
776
777 def checkout_branch(self, branch=None):
777 def checkout_branch(self, branch=None):
778 """
778 """
779 Checks out ``branch`` or the backend's default branch.
779 Checks out ``branch`` or the backend's default branch.
780
780
781 Raises ``BranchDoesNotExistError`` if the branch does not exist.
781 Raises ``BranchDoesNotExistError`` if the branch does not exist.
782 """
782 """
783 raise NotImplementedError
783 raise NotImplementedError
784
784
785
785
786 class BaseInMemoryChangeset(object):
786 class BaseInMemoryChangeset(object):
787 """
787 """
788 Represents differences between repository's state (most recent head) and
788 Represents differences between repository's state (most recent head) and
789 changes made *in place*.
789 changes made *in place*.
790
790
791 **Attributes**
791 **Attributes**
792
792
793 ``repository``
793 ``repository``
794 repository object for this in-memory-changeset
794 repository object for this in-memory-changeset
795
795
796 ``added``
796 ``added``
797 list of ``FileNode`` objects marked as *added*
797 list of ``FileNode`` objects marked as *added*
798
798
799 ``changed``
799 ``changed``
800 list of ``FileNode`` objects marked as *changed*
800 list of ``FileNode`` objects marked as *changed*
801
801
802 ``removed``
802 ``removed``
803 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
803 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
804 *removed*
804 *removed*
805
805
806 ``parents``
806 ``parents``
807 list of ``Changeset`` representing parents of in-memory changeset.
807 list of ``Changeset`` representing parents of in-memory changeset.
808 Should always be 2-element sequence.
808 Should always be 2-element sequence.
809
809
810 """
810 """
811
811
812 def __init__(self, repository):
812 def __init__(self, repository):
813 self.repository = repository
813 self.repository = repository
814 self.added = []
814 self.added = []
815 self.changed = []
815 self.changed = []
816 self.removed = []
816 self.removed = []
817 self.parents = []
817 self.parents = []
818
818
819 def add(self, *filenodes):
819 def add(self, *filenodes):
820 """
820 """
821 Marks given ``FileNode`` objects as *to be committed*.
821 Marks given ``FileNode`` objects as *to be committed*.
822
822
823 :raises ``NodeAlreadyExistsError``: if node with same path exists at
823 :raises ``NodeAlreadyExistsError``: if node with same path exists at
824 latest changeset
824 latest changeset
825 :raises ``NodeAlreadyAddedError``: if node with same path is already
825 :raises ``NodeAlreadyAddedError``: if node with same path is already
826 marked as *added*
826 marked as *added*
827 """
827 """
828 # Check if not already marked as *added* first
828 # Check if not already marked as *added* first
829 for node in filenodes:
829 for node in filenodes:
830 if node.path in (n.path for n in self.added):
830 if node.path in (n.path for n in self.added):
831 raise NodeAlreadyAddedError("Such FileNode %s is already "
831 raise NodeAlreadyAddedError("Such FileNode %s is already "
832 "marked for addition" % node.path)
832 "marked for addition" % node.path)
833 for node in filenodes:
833 for node in filenodes:
834 self.added.append(node)
834 self.added.append(node)
835
835
836 def change(self, *filenodes):
836 def change(self, *filenodes):
837 """
837 """
838 Marks given ``FileNode`` objects to be *changed* in next commit.
838 Marks given ``FileNode`` objects to be *changed* in next commit.
839
839
840 :raises ``EmptyRepositoryError``: if there are no changesets yet
840 :raises ``EmptyRepositoryError``: if there are no changesets yet
841 :raises ``NodeAlreadyExistsError``: if node with same path is already
841 :raises ``NodeAlreadyExistsError``: if node with same path is already
842 marked to be *changed*
842 marked to be *changed*
843 :raises ``NodeAlreadyRemovedError``: if node with same path is already
843 :raises ``NodeAlreadyRemovedError``: if node with same path is already
844 marked to be *removed*
844 marked to be *removed*
845 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
845 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
846 changeset
846 changeset
847 :raises ``NodeNotChangedError``: if node hasn't really be changed
847 :raises ``NodeNotChangedError``: if node hasn't really be changed
848 """
848 """
849 for node in filenodes:
849 for node in filenodes:
850 if node.path in (n.path for n in self.removed):
850 if node.path in (n.path for n in self.removed):
851 raise NodeAlreadyRemovedError("Node at %s is already marked "
851 raise NodeAlreadyRemovedError("Node at %s is already marked "
852 "as removed" % node.path)
852 "as removed" % node.path)
853 try:
853 try:
854 self.repository.get_changeset()
854 self.repository.get_changeset()
855 except EmptyRepositoryError:
855 except EmptyRepositoryError:
856 raise EmptyRepositoryError("Nothing to change - try to *add* new "
856 raise EmptyRepositoryError("Nothing to change - try to *add* new "
857 "nodes rather than changing them")
857 "nodes rather than changing them")
858 for node in filenodes:
858 for node in filenodes:
859 if node.path in (n.path for n in self.changed):
859 if node.path in (n.path for n in self.changed):
860 raise NodeAlreadyChangedError("Node at '%s' is already "
860 raise NodeAlreadyChangedError("Node at '%s' is already "
861 "marked as changed" % node.path)
861 "marked as changed" % node.path)
862 self.changed.append(node)
862 self.changed.append(node)
863
863
864 def remove(self, *filenodes):
864 def remove(self, *filenodes):
865 """
865 """
866 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
866 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
867 *removed* in next commit.
867 *removed* in next commit.
868
868
869 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
869 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
870 be *removed*
870 be *removed*
871 :raises ``NodeAlreadyChangedError``: if node has been already marked to
871 :raises ``NodeAlreadyChangedError``: if node has been already marked to
872 be *changed*
872 be *changed*
873 """
873 """
874 for node in filenodes:
874 for node in filenodes:
875 if node.path in (n.path for n in self.removed):
875 if node.path in (n.path for n in self.removed):
876 raise NodeAlreadyRemovedError("Node is already marked to "
876 raise NodeAlreadyRemovedError("Node is already marked to "
877 "for removal at %s" % node.path)
877 "for removal at %s" % node.path)
878 if node.path in (n.path for n in self.changed):
878 if node.path in (n.path for n in self.changed):
879 raise NodeAlreadyChangedError("Node is already marked to "
879 raise NodeAlreadyChangedError("Node is already marked to "
880 "be changed at %s" % node.path)
880 "be changed at %s" % node.path)
881 # We only mark node as *removed* - real removal is done by
881 # We only mark node as *removed* - real removal is done by
882 # commit method
882 # commit method
883 self.removed.append(node)
883 self.removed.append(node)
884
884
885 def reset(self):
885 def reset(self):
886 """
886 """
887 Resets this instance to initial state (cleans ``added``, ``changed``
887 Resets this instance to initial state (cleans ``added``, ``changed``
888 and ``removed`` lists).
888 and ``removed`` lists).
889 """
889 """
890 self.added = []
890 self.added = []
891 self.changed = []
891 self.changed = []
892 self.removed = []
892 self.removed = []
893 self.parents = []
893 self.parents = []
894
894
895 def get_ipaths(self):
895 def get_ipaths(self):
896 """
896 """
897 Returns generator of paths from nodes marked as added, changed or
897 Returns generator of paths from nodes marked as added, changed or
898 removed.
898 removed.
899 """
899 """
900 for node in itertools.chain(self.added, self.changed, self.removed):
900 for node in itertools.chain(self.added, self.changed, self.removed):
901 yield node.path
901 yield node.path
902
902
903 def get_paths(self):
903 def get_paths(self):
904 """
904 """
905 Returns list of paths from nodes marked as added, changed or removed.
905 Returns list of paths from nodes marked as added, changed or removed.
906 """
906 """
907 return list(self.get_ipaths())
907 return list(self.get_ipaths())
908
908
909 def check_integrity(self, parents=None):
909 def check_integrity(self, parents=None):
910 """
910 """
911 Checks in-memory changeset's integrity. Also, sets parents if not
911 Checks in-memory changeset's integrity. Also, sets parents if not
912 already set.
912 already set.
913
913
914 :raises CommitError: if any error occurs (i.e.
914 :raises CommitError: if any error occurs (i.e.
915 ``NodeDoesNotExistError``).
915 ``NodeDoesNotExistError``).
916 """
916 """
917 if not self.parents:
917 if not self.parents:
918 parents = parents or []
918 parents = parents or []
919 if len(parents) == 0:
919 if len(parents) == 0:
920 try:
920 try:
921 parents = [self.repository.get_changeset(), None]
921 parents = [self.repository.get_changeset(), None]
922 except EmptyRepositoryError:
922 except EmptyRepositoryError:
923 parents = [None, None]
923 parents = [None, None]
924 elif len(parents) == 1:
924 elif len(parents) == 1:
925 parents += [None]
925 parents += [None]
926 self.parents = parents
926 self.parents = parents
927
927
928 # Local parents, only if not None
928 # Local parents, only if not None
929 parents = [p for p in self.parents if p]
929 parents = [p for p in self.parents if p]
930
930
931 # Check nodes marked as added
931 # Check nodes marked as added
932 for p in parents:
932 for p in parents:
933 for node in self.added:
933 for node in self.added:
934 try:
934 try:
935 p.get_node(node.path)
935 p.get_node(node.path)
936 except NodeDoesNotExistError:
936 except NodeDoesNotExistError:
937 pass
937 pass
938 else:
938 else:
939 raise NodeAlreadyExistsError("Node at %s already exists "
939 raise NodeAlreadyExistsError("Node at %s already exists "
940 "at %s" % (node.path, p))
940 "at %s" % (node.path, p))
941
941
942 # Check nodes marked as changed
942 # Check nodes marked as changed
943 missing = set(self.changed)
943 missing = set(self.changed)
944 not_changed = set(self.changed)
944 not_changed = set(self.changed)
945 if self.changed and not parents:
945 if self.changed and not parents:
946 raise NodeDoesNotExistError(str(self.changed[0].path))
946 raise NodeDoesNotExistError(str(self.changed[0].path))
947 for p in parents:
947 for p in parents:
948 for node in self.changed:
948 for node in self.changed:
949 try:
949 try:
950 old = p.get_node(node.path)
950 old = p.get_node(node.path)
951 missing.remove(node)
951 missing.remove(node)
952 # if content actually changed, remove node from unchanged
952 # if content actually changed, remove node from unchanged
953 if old.content != node.content:
953 if old.content != node.content:
954 not_changed.remove(node)
954 not_changed.remove(node)
955 except NodeDoesNotExistError:
955 except NodeDoesNotExistError:
956 pass
956 pass
957 if self.changed and missing:
957 if self.changed and missing:
958 raise NodeDoesNotExistError("Node at %s is missing "
958 raise NodeDoesNotExistError("Node at %s is missing "
959 "(parents: %s)" % (node.path, parents))
959 "(parents: %s)" % (node.path, parents))
960
960
961 if self.changed and not_changed:
961 if self.changed and not_changed:
962 raise NodeNotChangedError("Node at %s wasn't actually changed "
962 raise NodeNotChangedError("Node at %s wasn't actually changed "
963 "since parents' changesets: %s" % (not_changed.pop().path,
963 "since parents' changesets: %s" % (not_changed.pop().path,
964 parents)
964 parents)
965 )
965 )
966
966
967 # Check nodes marked as removed
967 # Check nodes marked as removed
968 if self.removed and not parents:
968 if self.removed and not parents:
969 raise NodeDoesNotExistError("Cannot remove node at %s as there "
969 raise NodeDoesNotExistError("Cannot remove node at %s as there "
970 "were no parents specified" % self.removed[0].path)
970 "were no parents specified" % self.removed[0].path)
971 really_removed = set()
971 really_removed = set()
972 for p in parents:
972 for p in parents:
973 for node in self.removed:
973 for node in self.removed:
974 try:
974 try:
975 p.get_node(node.path)
975 p.get_node(node.path)
976 really_removed.add(node)
976 really_removed.add(node)
977 except ChangesetError:
977 except ChangesetError:
978 pass
978 pass
979 not_removed = set(self.removed) - really_removed
979 not_removed = set(self.removed) - really_removed
980 if not_removed:
980 if not_removed:
981 raise NodeDoesNotExistError("Cannot remove node at %s from "
981 raise NodeDoesNotExistError("Cannot remove node at %s from "
982 "following parents: %s" % (not_removed[0], parents))
982 "following parents: %s" % (not_removed[0], parents))
983
983
984 def commit(self, message, author, parents=None, branch=None, date=None,
984 def commit(self, message, author, parents=None, branch=None, date=None,
985 **kwargs):
985 **kwargs):
986 """
986 """
987 Performs in-memory commit (doesn't check workdir in any way) and
987 Performs in-memory commit (doesn't check workdir in any way) and
988 returns newly created ``Changeset``. Updates repository's
988 returns newly created ``Changeset``. Updates repository's
989 ``revisions``.
989 ``revisions``.
990
990
991 .. note::
991 .. note::
992 While overriding this method each backend's should call
992 While overriding this method each backend's should call
993 ``self.check_integrity(parents)`` in the first place.
993 ``self.check_integrity(parents)`` in the first place.
994
994
995 :param message: message of the commit
995 :param message: message of the commit
996 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
996 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
997 :param parents: single parent or sequence of parents from which commit
997 :param parents: single parent or sequence of parents from which commit
998 would be derived
998 would be derived
999 :param date: ``datetime.datetime`` instance. Defaults to
999 :param date: ``datetime.datetime`` instance. Defaults to
1000 ``datetime.datetime.now()``.
1000 ``datetime.datetime.now()``.
1001 :param branch: branch name, as string. If none given, default backend's
1001 :param branch: branch name, as string. If none given, default backend's
1002 branch would be used.
1002 branch would be used.
1003
1003
1004 :raises ``CommitError``: if any error occurs while committing
1004 :raises ``CommitError``: if any error occurs while committing
1005 """
1005 """
1006 raise NotImplementedError
1006 raise NotImplementedError
1007
1007
1008
1008
1009 class EmptyChangeset(BaseChangeset):
1009 class EmptyChangeset(BaseChangeset):
1010 """
1010 """
1011 An dummy empty changeset. It's possible to pass hash when creating
1011 An dummy empty changeset. It's possible to pass hash when creating
1012 an EmptyChangeset
1012 an EmptyChangeset
1013 """
1013 """
1014
1014
1015 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1015 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1016 alias=None, revision=-1, message='', author='', date=None):
1016 alias=None, revision=-1, message='', author='', date=None):
1017 self._empty_cs = cs
1017 self._empty_cs = cs
1018 self.revision = revision
1018 self.revision = revision
1019 self.message = message
1019 self.message = message
1020 self.author = author
1020 self.author = author
1021 self.date = date or datetime.datetime.fromtimestamp(0)
1021 self.date = date or datetime.datetime.fromtimestamp(0)
1022 self.repository = repo
1022 self.repository = repo
1023 self.requested_revision = requested_revision
1023 self.requested_revision = requested_revision
1024 self.alias = alias
1024 self.alias = alias
1025
1025
1026 @LazyProperty
1026 @LazyProperty
1027 def raw_id(self):
1027 def raw_id(self):
1028 """
1028 """
1029 Returns raw string identifying this changeset, useful for web
1029 Returns raw string identifying this changeset, useful for web
1030 representation.
1030 representation.
1031 """
1031 """
1032
1032
1033 return self._empty_cs
1033 return self._empty_cs
1034
1034
1035 @LazyProperty
1035 @LazyProperty
1036 def branch(self):
1036 def branch(self):
1037 from kallithea.lib.vcs.backends import get_backend
1037 from kallithea.lib.vcs.backends import get_backend
1038 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1038 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1039
1039
1040 @LazyProperty
1040 @LazyProperty
1041 def branches(self):
1042 from kallithea.lib.vcs.backends import get_backend
1043 return [get_backend(self.alias).DEFAULT_BRANCH_NAME]
1044
1045 @LazyProperty
1041 def short_id(self):
1046 def short_id(self):
1042 return self.raw_id[:12]
1047 return self.raw_id[:12]
1043
1048
1044 def get_file_changeset(self, path):
1049 def get_file_changeset(self, path):
1045 return self
1050 return self
1046
1051
1047 def get_file_content(self, path):
1052 def get_file_content(self, path):
1048 return u''
1053 return u''
1049
1054
1050 def get_file_size(self, path):
1055 def get_file_size(self, path):
1051 return 0
1056 return 0
1052
1057
1053
1058
1054 class CollectionGenerator(object):
1059 class CollectionGenerator(object):
1055
1060
1056 def __init__(self, repo, revs):
1061 def __init__(self, repo, revs):
1057 self.repo = repo
1062 self.repo = repo
1058 self.revs = revs
1063 self.revs = revs
1059
1064
1060 def __len__(self):
1065 def __len__(self):
1061 return len(self.revs)
1066 return len(self.revs)
1062
1067
1063 def __iter__(self):
1068 def __iter__(self):
1064 for rev in self.revs:
1069 for rev in self.revs:
1065 yield self.repo.get_changeset(rev)
1070 yield self.repo.get_changeset(rev)
1066
1071
1067 def __getitem__(self, what):
1072 def __getitem__(self, what):
1068 """Return either a single element by index, or a sliced collection."""
1073 """Return either a single element by index, or a sliced collection."""
1069 if isinstance(what, slice):
1074 if isinstance(what, slice):
1070 return CollectionGenerator(self.repo, self.revs[what])
1075 return CollectionGenerator(self.repo, self.revs[what])
1071 else:
1076 else:
1072 # single item
1077 # single item
1073 return self.repo.get_changeset(self.revs[what])
1078 return self.repo.get_changeset(self.revs[what])
1074
1079
1075 def __repr__(self):
1080 def __repr__(self):
1076 return '<CollectionGenerator[len:%s]>' % (len(self))
1081 return '<CollectionGenerator[len:%s]>' % (len(self))
@@ -1,552 +1,558 b''
1 import re
1 import re
2 from itertools import chain
2 from itertools import chain
3 from dulwich import objects
3 from dulwich import objects
4 from subprocess import Popen, PIPE
4 from subprocess import Popen, PIPE
5
5
6 from kallithea.lib.vcs.conf import settings
6 from kallithea.lib.vcs.conf import settings
7 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
7 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
8 from kallithea.lib.vcs.exceptions import (
8 from kallithea.lib.vcs.exceptions import (
9 RepositoryError, ChangesetError, NodeDoesNotExistError, VCSError,
9 RepositoryError, ChangesetError, NodeDoesNotExistError, VCSError,
10 ChangesetDoesNotExistError, ImproperArchiveTypeError
10 ChangesetDoesNotExistError, ImproperArchiveTypeError
11 )
11 )
12 from kallithea.lib.vcs.nodes import (
12 from kallithea.lib.vcs.nodes import (
13 FileNode, DirNode, NodeKind, RootNode, SubModuleNode,
13 FileNode, DirNode, NodeKind, RootNode, SubModuleNode,
14 ChangedFileNodesGenerator, AddedFileNodesGenerator, RemovedFileNodesGenerator
14 ChangedFileNodesGenerator, AddedFileNodesGenerator, RemovedFileNodesGenerator
15 )
15 )
16 from kallithea.lib.vcs.utils import (
16 from kallithea.lib.vcs.utils import (
17 safe_unicode, safe_str, safe_int, date_fromtimestamp
17 safe_unicode, safe_str, safe_int, date_fromtimestamp
18 )
18 )
19 from kallithea.lib.vcs.utils.lazy import LazyProperty
19 from kallithea.lib.vcs.utils.lazy import LazyProperty
20
20
21
21
22 class GitChangeset(BaseChangeset):
22 class GitChangeset(BaseChangeset):
23 """
23 """
24 Represents state of the repository at single revision.
24 Represents state of the repository at single revision.
25 """
25 """
26
26
27 def __init__(self, repository, revision):
27 def __init__(self, repository, revision):
28 self._stat_modes = {}
28 self._stat_modes = {}
29 self.repository = repository
29 self.repository = repository
30 revision = safe_str(revision)
30 revision = safe_str(revision)
31 try:
31 try:
32 commit = self.repository._repo[revision]
32 commit = self.repository._repo[revision]
33 if isinstance(commit, objects.Tag):
33 if isinstance(commit, objects.Tag):
34 revision = safe_str(commit.object[1])
34 revision = safe_str(commit.object[1])
35 commit = self.repository._repo.get_object(commit.object[1])
35 commit = self.repository._repo.get_object(commit.object[1])
36 except KeyError:
36 except KeyError:
37 raise RepositoryError("Cannot get object with id %s" % revision)
37 raise RepositoryError("Cannot get object with id %s" % revision)
38 self.raw_id = revision
38 self.raw_id = revision
39 self.id = self.raw_id
39 self.id = self.raw_id
40 self.short_id = self.raw_id[:12]
40 self.short_id = self.raw_id[:12]
41 self._commit = commit
41 self._commit = commit
42 self._tree_id = commit.tree
42 self._tree_id = commit.tree
43 self._committer_property = 'committer'
43 self._committer_property = 'committer'
44 self._author_property = 'author'
44 self._author_property = 'author'
45 self._date_property = 'commit_time'
45 self._date_property = 'commit_time'
46 self._date_tz_property = 'commit_timezone'
46 self._date_tz_property = 'commit_timezone'
47 self.revision = repository.revisions.index(revision)
47 self.revision = repository.revisions.index(revision)
48
48
49 self.nodes = {}
49 self.nodes = {}
50 self._paths = {}
50 self._paths = {}
51
51
52 @LazyProperty
52 @LazyProperty
53 def bookmarks(self):
53 def bookmarks(self):
54 return ()
54 return ()
55
55
56 @LazyProperty
56 @LazyProperty
57 def message(self):
57 def message(self):
58 return safe_unicode(self._commit.message)
58 return safe_unicode(self._commit.message)
59
59
60 @LazyProperty
60 @LazyProperty
61 def committer(self):
61 def committer(self):
62 return safe_unicode(getattr(self._commit, self._committer_property))
62 return safe_unicode(getattr(self._commit, self._committer_property))
63
63
64 @LazyProperty
64 @LazyProperty
65 def author(self):
65 def author(self):
66 return safe_unicode(getattr(self._commit, self._author_property))
66 return safe_unicode(getattr(self._commit, self._author_property))
67
67
68 @LazyProperty
68 @LazyProperty
69 def date(self):
69 def date(self):
70 return date_fromtimestamp(getattr(self._commit, self._date_property),
70 return date_fromtimestamp(getattr(self._commit, self._date_property),
71 getattr(self._commit, self._date_tz_property))
71 getattr(self._commit, self._date_tz_property))
72
72
73 @LazyProperty
73 @LazyProperty
74 def _timestamp(self):
74 def _timestamp(self):
75 return getattr(self._commit, self._date_property)
75 return getattr(self._commit, self._date_property)
76
76
77 @LazyProperty
77 @LazyProperty
78 def status(self):
78 def status(self):
79 """
79 """
80 Returns modified, added, removed, deleted files for current changeset
80 Returns modified, added, removed, deleted files for current changeset
81 """
81 """
82 return self.changed, self.added, self.removed
82 return self.changed, self.added, self.removed
83
83
84 @LazyProperty
84 @LazyProperty
85 def tags(self):
85 def tags(self):
86 _tags = []
86 _tags = []
87 for tname, tsha in self.repository.tags.iteritems():
87 for tname, tsha in self.repository.tags.iteritems():
88 if tsha == self.raw_id:
88 if tsha == self.raw_id:
89 _tags.append(tname)
89 _tags.append(tname)
90 return _tags
90 return _tags
91
91
92 @LazyProperty
92 @LazyProperty
93 def branch(self):
93 def branch(self):
94
94 # Note: This function will return one branch name for the changeset -
95 # that might not make sense in Git where branches() is a better match
96 # for the basic model
95 heads = self.repository._heads(reverse=False)
97 heads = self.repository._heads(reverse=False)
96
97 ref = heads.get(self.raw_id)
98 ref = heads.get(self.raw_id)
98 if ref:
99 if ref:
99 return safe_unicode(ref)
100 return safe_unicode(ref)
100
101
102 @LazyProperty
103 def branches(self):
104 heads = self.repository._heads(reverse=True)
105 return [b for b in heads if heads[b] == self.raw_id] # FIXME: Inefficient ... and returning None!
106
101 def _fix_path(self, path):
107 def _fix_path(self, path):
102 """
108 """
103 Paths are stored without trailing slash so we need to get rid off it if
109 Paths are stored without trailing slash so we need to get rid off it if
104 needed.
110 needed.
105 """
111 """
106 if path.endswith('/'):
112 if path.endswith('/'):
107 path = path.rstrip('/')
113 path = path.rstrip('/')
108 return path
114 return path
109
115
110 def _get_id_for_path(self, path):
116 def _get_id_for_path(self, path):
111 path = safe_str(path)
117 path = safe_str(path)
112 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
118 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
113 if path not in self._paths:
119 if path not in self._paths:
114 path = path.strip('/')
120 path = path.strip('/')
115 # set root tree
121 # set root tree
116 tree = self.repository._repo[self._tree_id]
122 tree = self.repository._repo[self._tree_id]
117 if path == '':
123 if path == '':
118 self._paths[''] = tree.id
124 self._paths[''] = tree.id
119 return tree.id
125 return tree.id
120 splitted = path.split('/')
126 splitted = path.split('/')
121 dirs, name = splitted[:-1], splitted[-1]
127 dirs, name = splitted[:-1], splitted[-1]
122 curdir = ''
128 curdir = ''
123
129
124 # initially extract things from root dir
130 # initially extract things from root dir
125 for item, stat, id in tree.iteritems():
131 for item, stat, id in tree.iteritems():
126 if curdir:
132 if curdir:
127 name = '/'.join((curdir, item))
133 name = '/'.join((curdir, item))
128 else:
134 else:
129 name = item
135 name = item
130 self._paths[name] = id
136 self._paths[name] = id
131 self._stat_modes[name] = stat
137 self._stat_modes[name] = stat
132
138
133 for dir in dirs:
139 for dir in dirs:
134 if curdir:
140 if curdir:
135 curdir = '/'.join((curdir, dir))
141 curdir = '/'.join((curdir, dir))
136 else:
142 else:
137 curdir = dir
143 curdir = dir
138 dir_id = None
144 dir_id = None
139 for item, stat, id in tree.iteritems():
145 for item, stat, id in tree.iteritems():
140 if dir == item:
146 if dir == item:
141 dir_id = id
147 dir_id = id
142 if dir_id:
148 if dir_id:
143 # Update tree
149 # Update tree
144 tree = self.repository._repo[dir_id]
150 tree = self.repository._repo[dir_id]
145 if not isinstance(tree, objects.Tree):
151 if not isinstance(tree, objects.Tree):
146 raise ChangesetError('%s is not a directory' % curdir)
152 raise ChangesetError('%s is not a directory' % curdir)
147 else:
153 else:
148 raise ChangesetError('%s have not been found' % curdir)
154 raise ChangesetError('%s have not been found' % curdir)
149
155
150 # cache all items from the given traversed tree
156 # cache all items from the given traversed tree
151 for item, stat, id in tree.iteritems():
157 for item, stat, id in tree.iteritems():
152 if curdir:
158 if curdir:
153 name = '/'.join((curdir, item))
159 name = '/'.join((curdir, item))
154 else:
160 else:
155 name = item
161 name = item
156 self._paths[name] = id
162 self._paths[name] = id
157 self._stat_modes[name] = stat
163 self._stat_modes[name] = stat
158 if path not in self._paths:
164 if path not in self._paths:
159 raise NodeDoesNotExistError("There is no file nor directory "
165 raise NodeDoesNotExistError("There is no file nor directory "
160 "at the given path '%s' at revision %s"
166 "at the given path '%s' at revision %s"
161 % (path, safe_str(self.short_id)))
167 % (path, safe_str(self.short_id)))
162 return self._paths[path]
168 return self._paths[path]
163
169
164 def _get_kind(self, path):
170 def _get_kind(self, path):
165 obj = self.repository._repo[self._get_id_for_path(path)]
171 obj = self.repository._repo[self._get_id_for_path(path)]
166 if isinstance(obj, objects.Blob):
172 if isinstance(obj, objects.Blob):
167 return NodeKind.FILE
173 return NodeKind.FILE
168 elif isinstance(obj, objects.Tree):
174 elif isinstance(obj, objects.Tree):
169 return NodeKind.DIR
175 return NodeKind.DIR
170
176
171 def _get_filectx(self, path):
177 def _get_filectx(self, path):
172 path = self._fix_path(path)
178 path = self._fix_path(path)
173 if self._get_kind(path) != NodeKind.FILE:
179 if self._get_kind(path) != NodeKind.FILE:
174 raise ChangesetError("File does not exist for revision %s at "
180 raise ChangesetError("File does not exist for revision %s at "
175 " '%s'" % (self.raw_id, path))
181 " '%s'" % (self.raw_id, path))
176 return path
182 return path
177
183
178 def _get_file_nodes(self):
184 def _get_file_nodes(self):
179 return chain(*(t[2] for t in self.walk()))
185 return chain(*(t[2] for t in self.walk()))
180
186
181 @LazyProperty
187 @LazyProperty
182 def parents(self):
188 def parents(self):
183 """
189 """
184 Returns list of parents changesets.
190 Returns list of parents changesets.
185 """
191 """
186 return [self.repository.get_changeset(parent)
192 return [self.repository.get_changeset(parent)
187 for parent in self._commit.parents]
193 for parent in self._commit.parents]
188
194
189 @LazyProperty
195 @LazyProperty
190 def children(self):
196 def children(self):
191 """
197 """
192 Returns list of children changesets.
198 Returns list of children changesets.
193 """
199 """
194 rev_filter = settings.GIT_REV_FILTER
200 rev_filter = settings.GIT_REV_FILTER
195 so, se = self.repository.run_git_command(
201 so, se = self.repository.run_git_command(
196 ['rev-list', rev_filter, '--children']
202 ['rev-list', rev_filter, '--children']
197 )
203 )
198
204
199 children = []
205 children = []
200 pat = re.compile(r'^%s' % self.raw_id)
206 pat = re.compile(r'^%s' % self.raw_id)
201 for l in so.splitlines():
207 for l in so.splitlines():
202 if pat.match(l):
208 if pat.match(l):
203 childs = l.split(' ')[1:]
209 childs = l.split(' ')[1:]
204 children.extend(childs)
210 children.extend(childs)
205 return [self.repository.get_changeset(cs) for cs in children]
211 return [self.repository.get_changeset(cs) for cs in children]
206
212
207 def next(self, branch=None):
213 def next(self, branch=None):
208 if branch and self.branch != branch:
214 if branch and self.branch != branch:
209 raise VCSError('Branch option used on changeset not belonging '
215 raise VCSError('Branch option used on changeset not belonging '
210 'to that branch')
216 'to that branch')
211
217
212 cs = self
218 cs = self
213 while True:
219 while True:
214 try:
220 try:
215 next_ = cs.revision + 1
221 next_ = cs.revision + 1
216 next_rev = cs.repository.revisions[next_]
222 next_rev = cs.repository.revisions[next_]
217 except IndexError:
223 except IndexError:
218 raise ChangesetDoesNotExistError
224 raise ChangesetDoesNotExistError
219 cs = cs.repository.get_changeset(next_rev)
225 cs = cs.repository.get_changeset(next_rev)
220
226
221 if not branch or branch == cs.branch:
227 if not branch or branch == cs.branch:
222 return cs
228 return cs
223
229
224 def prev(self, branch=None):
230 def prev(self, branch=None):
225 if branch and self.branch != branch:
231 if branch and self.branch != branch:
226 raise VCSError('Branch option used on changeset not belonging '
232 raise VCSError('Branch option used on changeset not belonging '
227 'to that branch')
233 'to that branch')
228
234
229 cs = self
235 cs = self
230 while True:
236 while True:
231 try:
237 try:
232 prev_ = cs.revision - 1
238 prev_ = cs.revision - 1
233 if prev_ < 0:
239 if prev_ < 0:
234 raise IndexError
240 raise IndexError
235 prev_rev = cs.repository.revisions[prev_]
241 prev_rev = cs.repository.revisions[prev_]
236 except IndexError:
242 except IndexError:
237 raise ChangesetDoesNotExistError
243 raise ChangesetDoesNotExistError
238 cs = cs.repository.get_changeset(prev_rev)
244 cs = cs.repository.get_changeset(prev_rev)
239
245
240 if not branch or branch == cs.branch:
246 if not branch or branch == cs.branch:
241 return cs
247 return cs
242
248
243 def diff(self, ignore_whitespace=True, context=3):
249 def diff(self, ignore_whitespace=True, context=3):
244 rev1 = self.parents[0] if self.parents else self.repository.EMPTY_CHANGESET
250 rev1 = self.parents[0] if self.parents else self.repository.EMPTY_CHANGESET
245 rev2 = self
251 rev2 = self
246 return ''.join(self.repository.get_diff(rev1, rev2,
252 return ''.join(self.repository.get_diff(rev1, rev2,
247 ignore_whitespace=ignore_whitespace,
253 ignore_whitespace=ignore_whitespace,
248 context=context))
254 context=context))
249
255
250 def get_file_mode(self, path):
256 def get_file_mode(self, path):
251 """
257 """
252 Returns stat mode of the file at the given ``path``.
258 Returns stat mode of the file at the given ``path``.
253 """
259 """
254 # ensure path is traversed
260 # ensure path is traversed
255 path = safe_str(path)
261 path = safe_str(path)
256 self._get_id_for_path(path)
262 self._get_id_for_path(path)
257 return self._stat_modes[path]
263 return self._stat_modes[path]
258
264
259 def get_file_content(self, path):
265 def get_file_content(self, path):
260 """
266 """
261 Returns content of the file at given ``path``.
267 Returns content of the file at given ``path``.
262 """
268 """
263 id = self._get_id_for_path(path)
269 id = self._get_id_for_path(path)
264 blob = self.repository._repo[id]
270 blob = self.repository._repo[id]
265 return blob.as_pretty_string()
271 return blob.as_pretty_string()
266
272
267 def get_file_size(self, path):
273 def get_file_size(self, path):
268 """
274 """
269 Returns size of the file at given ``path``.
275 Returns size of the file at given ``path``.
270 """
276 """
271 id = self._get_id_for_path(path)
277 id = self._get_id_for_path(path)
272 blob = self.repository._repo[id]
278 blob = self.repository._repo[id]
273 return blob.raw_length()
279 return blob.raw_length()
274
280
275 def get_file_changeset(self, path):
281 def get_file_changeset(self, path):
276 """
282 """
277 Returns last commit of the file at the given ``path``.
283 Returns last commit of the file at the given ``path``.
278 """
284 """
279 return self.get_file_history(path, limit=1)[0]
285 return self.get_file_history(path, limit=1)[0]
280
286
281 def get_file_history(self, path, limit=None):
287 def get_file_history(self, path, limit=None):
282 """
288 """
283 Returns history of file as reversed list of ``Changeset`` objects for
289 Returns history of file as reversed list of ``Changeset`` objects for
284 which file at given ``path`` has been modified.
290 which file at given ``path`` has been modified.
285
291
286 TODO: This function now uses os underlying 'git' and 'grep' commands
292 TODO: This function now uses os underlying 'git' and 'grep' commands
287 which is generally not good. Should be replaced with algorithm
293 which is generally not good. Should be replaced with algorithm
288 iterating commits.
294 iterating commits.
289 """
295 """
290 self._get_filectx(path)
296 self._get_filectx(path)
291 cs_id = safe_str(self.id)
297 cs_id = safe_str(self.id)
292 f_path = safe_str(path)
298 f_path = safe_str(path)
293
299
294 if limit is not None:
300 if limit is not None:
295 cmd = ['log', '-n', str(safe_int(limit, 0)),
301 cmd = ['log', '-n', str(safe_int(limit, 0)),
296 '--pretty=format:%H', '-s', cs_id, '--', f_path]
302 '--pretty=format:%H', '-s', cs_id, '--', f_path]
297
303
298 else:
304 else:
299 cmd = ['log',
305 cmd = ['log',
300 '--pretty=format:%H', '-s', cs_id, '--', f_path]
306 '--pretty=format:%H', '-s', cs_id, '--', f_path]
301 so, se = self.repository.run_git_command(cmd)
307 so, se = self.repository.run_git_command(cmd)
302 ids = re.findall(r'[0-9a-fA-F]{40}', so)
308 ids = re.findall(r'[0-9a-fA-F]{40}', so)
303 return [self.repository.get_changeset(sha) for sha in ids]
309 return [self.repository.get_changeset(sha) for sha in ids]
304
310
305 def get_file_history_2(self, path):
311 def get_file_history_2(self, path):
306 """
312 """
307 Returns history of file as reversed list of ``Changeset`` objects for
313 Returns history of file as reversed list of ``Changeset`` objects for
308 which file at given ``path`` has been modified.
314 which file at given ``path`` has been modified.
309
315
310 """
316 """
311 self._get_filectx(path)
317 self._get_filectx(path)
312 from dulwich.walk import Walker
318 from dulwich.walk import Walker
313 include = [self.id]
319 include = [self.id]
314 walker = Walker(self.repository._repo.object_store, include,
320 walker = Walker(self.repository._repo.object_store, include,
315 paths=[path], max_entries=1)
321 paths=[path], max_entries=1)
316 return [self.repository.get_changeset(sha)
322 return [self.repository.get_changeset(sha)
317 for sha in (x.commit.id for x in walker)]
323 for sha in (x.commit.id for x in walker)]
318
324
319 def get_file_annotate(self, path):
325 def get_file_annotate(self, path):
320 """
326 """
321 Returns a generator of four element tuples with
327 Returns a generator of four element tuples with
322 lineno, sha, changeset lazy loader and line
328 lineno, sha, changeset lazy loader and line
323
329
324 TODO: This function now uses os underlying 'git' command which is
330 TODO: This function now uses os underlying 'git' command which is
325 generally not good. Should be replaced with algorithm iterating
331 generally not good. Should be replaced with algorithm iterating
326 commits.
332 commits.
327 """
333 """
328 cmd = ['blame', '-l', '--root', '-r', self.id, '--', path]
334 cmd = ['blame', '-l', '--root', '-r', self.id, '--', path]
329 # -l ==> outputs long shas (and we need all 40 characters)
335 # -l ==> outputs long shas (and we need all 40 characters)
330 # --root ==> doesn't put '^' character for boundaries
336 # --root ==> doesn't put '^' character for boundaries
331 # -r sha ==> blames for the given revision
337 # -r sha ==> blames for the given revision
332 so, se = self.repository.run_git_command(cmd)
338 so, se = self.repository.run_git_command(cmd)
333
339
334 for i, blame_line in enumerate(so.split('\n')[:-1]):
340 for i, blame_line in enumerate(so.split('\n')[:-1]):
335 ln_no = i + 1
341 ln_no = i + 1
336 sha, line = re.split(r' ', blame_line, 1)
342 sha, line = re.split(r' ', blame_line, 1)
337 yield (ln_no, sha, lambda: self.repository.get_changeset(sha), line)
343 yield (ln_no, sha, lambda: self.repository.get_changeset(sha), line)
338
344
339 def fill_archive(self, stream=None, kind='tgz', prefix=None,
345 def fill_archive(self, stream=None, kind='tgz', prefix=None,
340 subrepos=False):
346 subrepos=False):
341 """
347 """
342 Fills up given stream.
348 Fills up given stream.
343
349
344 :param stream: file like object.
350 :param stream: file like object.
345 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
351 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
346 Default: ``tgz``.
352 Default: ``tgz``.
347 :param prefix: name of root directory in archive.
353 :param prefix: name of root directory in archive.
348 Default is repository name and changeset's raw_id joined with dash
354 Default is repository name and changeset's raw_id joined with dash
349 (``repo-tip.<KIND>``).
355 (``repo-tip.<KIND>``).
350 :param subrepos: include subrepos in this archive.
356 :param subrepos: include subrepos in this archive.
351
357
352 :raise ImproperArchiveTypeError: If given kind is wrong.
358 :raise ImproperArchiveTypeError: If given kind is wrong.
353 :raise VcsError: If given stream is None
359 :raise VcsError: If given stream is None
354
360
355 """
361 """
356 allowed_kinds = settings.ARCHIVE_SPECS.keys()
362 allowed_kinds = settings.ARCHIVE_SPECS.keys()
357 if kind not in allowed_kinds:
363 if kind not in allowed_kinds:
358 raise ImproperArchiveTypeError('Archive kind not supported use one'
364 raise ImproperArchiveTypeError('Archive kind not supported use one'
359 'of %s', allowed_kinds)
365 'of %s', allowed_kinds)
360
366
361 if prefix is None:
367 if prefix is None:
362 prefix = '%s-%s' % (self.repository.name, self.short_id)
368 prefix = '%s-%s' % (self.repository.name, self.short_id)
363 elif prefix.startswith('/'):
369 elif prefix.startswith('/'):
364 raise VCSError("Prefix cannot start with leading slash")
370 raise VCSError("Prefix cannot start with leading slash")
365 elif prefix.strip() == '':
371 elif prefix.strip() == '':
366 raise VCSError("Prefix cannot be empty")
372 raise VCSError("Prefix cannot be empty")
367
373
368 if kind == 'zip':
374 if kind == 'zip':
369 frmt = 'zip'
375 frmt = 'zip'
370 else:
376 else:
371 frmt = 'tar'
377 frmt = 'tar'
372 _git_path = settings.GIT_EXECUTABLE_PATH
378 _git_path = settings.GIT_EXECUTABLE_PATH
373 cmd = '%s archive --format=%s --prefix=%s/ %s' % (_git_path,
379 cmd = '%s archive --format=%s --prefix=%s/ %s' % (_git_path,
374 frmt, prefix, self.raw_id)
380 frmt, prefix, self.raw_id)
375 if kind == 'tgz':
381 if kind == 'tgz':
376 cmd += ' | gzip -9'
382 cmd += ' | gzip -9'
377 elif kind == 'tbz2':
383 elif kind == 'tbz2':
378 cmd += ' | bzip2 -9'
384 cmd += ' | bzip2 -9'
379
385
380 if stream is None:
386 if stream is None:
381 raise VCSError('You need to pass in a valid stream for filling'
387 raise VCSError('You need to pass in a valid stream for filling'
382 ' with archival data')
388 ' with archival data')
383 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
389 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
384 cwd=self.repository.path)
390 cwd=self.repository.path)
385
391
386 buffer_size = 1024 * 8
392 buffer_size = 1024 * 8
387 chunk = popen.stdout.read(buffer_size)
393 chunk = popen.stdout.read(buffer_size)
388 while chunk:
394 while chunk:
389 stream.write(chunk)
395 stream.write(chunk)
390 chunk = popen.stdout.read(buffer_size)
396 chunk = popen.stdout.read(buffer_size)
391 # Make sure all descriptors would be read
397 # Make sure all descriptors would be read
392 popen.communicate()
398 popen.communicate()
393
399
394 def get_nodes(self, path):
400 def get_nodes(self, path):
395 if self._get_kind(path) != NodeKind.DIR:
401 if self._get_kind(path) != NodeKind.DIR:
396 raise ChangesetError("Directory does not exist for revision %s at "
402 raise ChangesetError("Directory does not exist for revision %s at "
397 " '%s'" % (self.revision, path))
403 " '%s'" % (self.revision, path))
398 path = self._fix_path(path)
404 path = self._fix_path(path)
399 id = self._get_id_for_path(path)
405 id = self._get_id_for_path(path)
400 tree = self.repository._repo[id]
406 tree = self.repository._repo[id]
401 dirnodes = []
407 dirnodes = []
402 filenodes = []
408 filenodes = []
403 als = self.repository.alias
409 als = self.repository.alias
404 for name, stat, id in tree.iteritems():
410 for name, stat, id in tree.iteritems():
405 if objects.S_ISGITLINK(stat):
411 if objects.S_ISGITLINK(stat):
406 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
412 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
407 alias=als))
413 alias=als))
408 continue
414 continue
409
415
410 obj = self.repository._repo.get_object(id)
416 obj = self.repository._repo.get_object(id)
411 if path != '':
417 if path != '':
412 obj_path = '/'.join((path, name))
418 obj_path = '/'.join((path, name))
413 else:
419 else:
414 obj_path = name
420 obj_path = name
415 if obj_path not in self._stat_modes:
421 if obj_path not in self._stat_modes:
416 self._stat_modes[obj_path] = stat
422 self._stat_modes[obj_path] = stat
417 if isinstance(obj, objects.Tree):
423 if isinstance(obj, objects.Tree):
418 dirnodes.append(DirNode(obj_path, changeset=self))
424 dirnodes.append(DirNode(obj_path, changeset=self))
419 elif isinstance(obj, objects.Blob):
425 elif isinstance(obj, objects.Blob):
420 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
426 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
421 else:
427 else:
422 raise ChangesetError("Requested object should be Tree "
428 raise ChangesetError("Requested object should be Tree "
423 "or Blob, is %r" % type(obj))
429 "or Blob, is %r" % type(obj))
424 nodes = dirnodes + filenodes
430 nodes = dirnodes + filenodes
425 for node in nodes:
431 for node in nodes:
426 if node.path not in self.nodes:
432 if node.path not in self.nodes:
427 self.nodes[node.path] = node
433 self.nodes[node.path] = node
428 nodes.sort()
434 nodes.sort()
429 return nodes
435 return nodes
430
436
431 def get_node(self, path):
437 def get_node(self, path):
432 if isinstance(path, unicode):
438 if isinstance(path, unicode):
433 path = path.encode('utf-8')
439 path = path.encode('utf-8')
434 path = self._fix_path(path)
440 path = self._fix_path(path)
435 if path not in self.nodes:
441 if path not in self.nodes:
436 try:
442 try:
437 id_ = self._get_id_for_path(path)
443 id_ = self._get_id_for_path(path)
438 except ChangesetError:
444 except ChangesetError:
439 raise NodeDoesNotExistError("Cannot find one of parents' "
445 raise NodeDoesNotExistError("Cannot find one of parents' "
440 "directories for a given path: %s" % path)
446 "directories for a given path: %s" % path)
441
447
442 _GL = lambda m: m and objects.S_ISGITLINK(m)
448 _GL = lambda m: m and objects.S_ISGITLINK(m)
443 if _GL(self._stat_modes.get(path)):
449 if _GL(self._stat_modes.get(path)):
444 node = SubModuleNode(path, url=None, changeset=id_,
450 node = SubModuleNode(path, url=None, changeset=id_,
445 alias=self.repository.alias)
451 alias=self.repository.alias)
446 else:
452 else:
447 obj = self.repository._repo.get_object(id_)
453 obj = self.repository._repo.get_object(id_)
448
454
449 if isinstance(obj, objects.Tree):
455 if isinstance(obj, objects.Tree):
450 if path == '':
456 if path == '':
451 node = RootNode(changeset=self)
457 node = RootNode(changeset=self)
452 else:
458 else:
453 node = DirNode(path, changeset=self)
459 node = DirNode(path, changeset=self)
454 node._tree = obj
460 node._tree = obj
455 elif isinstance(obj, objects.Blob):
461 elif isinstance(obj, objects.Blob):
456 node = FileNode(path, changeset=self)
462 node = FileNode(path, changeset=self)
457 node._blob = obj
463 node._blob = obj
458 else:
464 else:
459 raise NodeDoesNotExistError("There is no file nor directory "
465 raise NodeDoesNotExistError("There is no file nor directory "
460 "at the given path '%s' at revision %s"
466 "at the given path '%s' at revision %s"
461 % (path, self.short_id))
467 % (path, self.short_id))
462 # cache node
468 # cache node
463 self.nodes[path] = node
469 self.nodes[path] = node
464 return self.nodes[path]
470 return self.nodes[path]
465
471
466 @LazyProperty
472 @LazyProperty
467 def affected_files(self):
473 def affected_files(self):
468 """
474 """
469 Gets a fast accessible file changes for given changeset
475 Gets a fast accessible file changes for given changeset
470 """
476 """
471 added, modified, deleted = self._changes_cache
477 added, modified, deleted = self._changes_cache
472 return list(added.union(modified).union(deleted))
478 return list(added.union(modified).union(deleted))
473
479
474 @LazyProperty
480 @LazyProperty
475 def _diff_name_status(self):
481 def _diff_name_status(self):
476 output = []
482 output = []
477 for parent in self.parents:
483 for parent in self.parents:
478 cmd = ['diff', '--name-status', parent.raw_id, self.raw_id,
484 cmd = ['diff', '--name-status', parent.raw_id, self.raw_id,
479 '--encoding=utf8']
485 '--encoding=utf8']
480 so, se = self.repository.run_git_command(cmd)
486 so, se = self.repository.run_git_command(cmd)
481 output.append(so.strip())
487 output.append(so.strip())
482 return '\n'.join(output)
488 return '\n'.join(output)
483
489
484 @LazyProperty
490 @LazyProperty
485 def _changes_cache(self):
491 def _changes_cache(self):
486 added = set()
492 added = set()
487 modified = set()
493 modified = set()
488 deleted = set()
494 deleted = set()
489 _r = self.repository._repo
495 _r = self.repository._repo
490
496
491 parents = self.parents
497 parents = self.parents
492 if not self.parents:
498 if not self.parents:
493 parents = [EmptyChangeset()]
499 parents = [EmptyChangeset()]
494 for parent in parents:
500 for parent in parents:
495 if isinstance(parent, EmptyChangeset):
501 if isinstance(parent, EmptyChangeset):
496 oid = None
502 oid = None
497 else:
503 else:
498 oid = _r[parent.raw_id].tree
504 oid = _r[parent.raw_id].tree
499 changes = _r.object_store.tree_changes(oid, _r[self.raw_id].tree)
505 changes = _r.object_store.tree_changes(oid, _r[self.raw_id].tree)
500 for (oldpath, newpath), (_, _), (_, _) in changes:
506 for (oldpath, newpath), (_, _), (_, _) in changes:
501 if newpath and oldpath:
507 if newpath and oldpath:
502 modified.add(newpath)
508 modified.add(newpath)
503 elif newpath and not oldpath:
509 elif newpath and not oldpath:
504 added.add(newpath)
510 added.add(newpath)
505 elif not newpath and oldpath:
511 elif not newpath and oldpath:
506 deleted.add(oldpath)
512 deleted.add(oldpath)
507 return added, modified, deleted
513 return added, modified, deleted
508
514
509 def _get_paths_for_status(self, status):
515 def _get_paths_for_status(self, status):
510 """
516 """
511 Returns sorted list of paths for given ``status``.
517 Returns sorted list of paths for given ``status``.
512
518
513 :param status: one of: *added*, *modified* or *deleted*
519 :param status: one of: *added*, *modified* or *deleted*
514 """
520 """
515 added, modified, deleted = self._changes_cache
521 added, modified, deleted = self._changes_cache
516 return sorted({
522 return sorted({
517 'added': list(added),
523 'added': list(added),
518 'modified': list(modified),
524 'modified': list(modified),
519 'deleted': list(deleted)}[status]
525 'deleted': list(deleted)}[status]
520 )
526 )
521
527
522 @LazyProperty
528 @LazyProperty
523 def added(self):
529 def added(self):
524 """
530 """
525 Returns list of added ``FileNode`` objects.
531 Returns list of added ``FileNode`` objects.
526 """
532 """
527 if not self.parents:
533 if not self.parents:
528 return list(self._get_file_nodes())
534 return list(self._get_file_nodes())
529 return AddedFileNodesGenerator([n for n in
535 return AddedFileNodesGenerator([n for n in
530 self._get_paths_for_status('added')], self)
536 self._get_paths_for_status('added')], self)
531
537
532 @LazyProperty
538 @LazyProperty
533 def changed(self):
539 def changed(self):
534 """
540 """
535 Returns list of modified ``FileNode`` objects.
541 Returns list of modified ``FileNode`` objects.
536 """
542 """
537 if not self.parents:
543 if not self.parents:
538 return []
544 return []
539 return ChangedFileNodesGenerator([n for n in
545 return ChangedFileNodesGenerator([n for n in
540 self._get_paths_for_status('modified')], self)
546 self._get_paths_for_status('modified')], self)
541
547
542 @LazyProperty
548 @LazyProperty
543 def removed(self):
549 def removed(self):
544 """
550 """
545 Returns list of removed ``FileNode`` objects.
551 Returns list of removed ``FileNode`` objects.
546 """
552 """
547 if not self.parents:
553 if not self.parents:
548 return []
554 return []
549 return RemovedFileNodesGenerator([n for n in
555 return RemovedFileNodesGenerator([n for n in
550 self._get_paths_for_status('deleted')], self)
556 self._get_paths_for_status('deleted')], self)
551
557
552 extra = {}
558 extra = {}
@@ -1,430 +1,434 b''
1 import os
1 import os
2 import posixpath
2 import posixpath
3
3
4 from kallithea.lib.vcs.conf import settings
4 from kallithea.lib.vcs.conf import settings
5 from kallithea.lib.vcs.backends.base import BaseChangeset
5 from kallithea.lib.vcs.backends.base import BaseChangeset
6 from kallithea.lib.vcs.exceptions import (
6 from kallithea.lib.vcs.exceptions import (
7 ChangesetDoesNotExistError, ChangesetError, ImproperArchiveTypeError,
7 ChangesetDoesNotExistError, ChangesetError, ImproperArchiveTypeError,
8 NodeDoesNotExistError, VCSError
8 NodeDoesNotExistError, VCSError
9 )
9 )
10 from kallithea.lib.vcs.nodes import (
10 from kallithea.lib.vcs.nodes import (
11 AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode,
11 AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode,
12 NodeKind, RemovedFileNodesGenerator, RootNode, SubModuleNode
12 NodeKind, RemovedFileNodesGenerator, RootNode, SubModuleNode
13 )
13 )
14 from kallithea.lib.vcs.utils import safe_str, safe_unicode, date_fromtimestamp
14 from kallithea.lib.vcs.utils import safe_str, safe_unicode, date_fromtimestamp
15 from kallithea.lib.vcs.utils.lazy import LazyProperty
15 from kallithea.lib.vcs.utils.lazy import LazyProperty
16 from kallithea.lib.vcs.utils.paths import get_dirs_for_path
16 from kallithea.lib.vcs.utils.paths import get_dirs_for_path
17 from kallithea.lib.vcs.utils.hgcompat import archival, hex
17 from kallithea.lib.vcs.utils.hgcompat import archival, hex
18
18
19 from mercurial import obsolete
19 from mercurial import obsolete
20
20
21
21
22 class MercurialChangeset(BaseChangeset):
22 class MercurialChangeset(BaseChangeset):
23 """
23 """
24 Represents state of the repository at the single revision.
24 Represents state of the repository at the single revision.
25 """
25 """
26
26
27 def __init__(self, repository, revision):
27 def __init__(self, repository, revision):
28 self.repository = repository
28 self.repository = repository
29 assert isinstance(revision, basestring), repr(revision)
29 assert isinstance(revision, basestring), repr(revision)
30 self.raw_id = revision
30 self.raw_id = revision
31 self._ctx = repository._repo[revision]
31 self._ctx = repository._repo[revision]
32 self.revision = self._ctx._rev
32 self.revision = self._ctx._rev
33 self.nodes = {}
33 self.nodes = {}
34
34
35 @LazyProperty
35 @LazyProperty
36 def tags(self):
36 def tags(self):
37 return map(safe_unicode, self._ctx.tags())
37 return map(safe_unicode, self._ctx.tags())
38
38
39 @LazyProperty
39 @LazyProperty
40 def branch(self):
40 def branch(self):
41 return safe_unicode(self._ctx.branch())
41 return safe_unicode(self._ctx.branch())
42
42
43 @LazyProperty
43 @LazyProperty
44 def branches(self):
45 return [safe_unicode(self._ctx.branch())]
46
47 @LazyProperty
44 def closesbranch(self):
48 def closesbranch(self):
45 return self._ctx.closesbranch()
49 return self._ctx.closesbranch()
46
50
47 @LazyProperty
51 @LazyProperty
48 def obsolete(self):
52 def obsolete(self):
49 return self._ctx.obsolete()
53 return self._ctx.obsolete()
50
54
51 @LazyProperty
55 @LazyProperty
52 def bumped(self):
56 def bumped(self):
53 return self._ctx.bumped()
57 return self._ctx.bumped()
54
58
55 @LazyProperty
59 @LazyProperty
56 def divergent(self):
60 def divergent(self):
57 return self._ctx.divergent()
61 return self._ctx.divergent()
58
62
59 @LazyProperty
63 @LazyProperty
60 def extinct(self):
64 def extinct(self):
61 return self._ctx.extinct()
65 return self._ctx.extinct()
62
66
63 @LazyProperty
67 @LazyProperty
64 def unstable(self):
68 def unstable(self):
65 return self._ctx.unstable()
69 return self._ctx.unstable()
66
70
67 @LazyProperty
71 @LazyProperty
68 def phase(self):
72 def phase(self):
69 if(self._ctx.phase() == 1):
73 if(self._ctx.phase() == 1):
70 return 'Draft'
74 return 'Draft'
71 elif(self._ctx.phase() == 2):
75 elif(self._ctx.phase() == 2):
72 return 'Secret'
76 return 'Secret'
73 else:
77 else:
74 return ''
78 return ''
75
79
76 @LazyProperty
80 @LazyProperty
77 def successors(self):
81 def successors(self):
78 successors = obsolete.successorssets(self._ctx._repo, self._ctx.node())
82 successors = obsolete.successorssets(self._ctx._repo, self._ctx.node())
79 if successors:
83 if successors:
80 # flatten the list here handles both divergent (len > 1)
84 # flatten the list here handles both divergent (len > 1)
81 # and the usual case (len = 1)
85 # and the usual case (len = 1)
82 successors = [hex(n)[:12] for sub in successors for n in sub if n != self._ctx.node()]
86 successors = [hex(n)[:12] for sub in successors for n in sub if n != self._ctx.node()]
83
87
84 return successors
88 return successors
85
89
86 @LazyProperty
90 @LazyProperty
87 def precursors(self):
91 def precursors(self):
88 precursors = set()
92 precursors = set()
89 nm = self._ctx._repo.changelog.nodemap
93 nm = self._ctx._repo.changelog.nodemap
90 for p in self._ctx._repo.obsstore.precursors.get(self._ctx.node(), ()):
94 for p in self._ctx._repo.obsstore.precursors.get(self._ctx.node(), ()):
91 pr = nm.get(p[0])
95 pr = nm.get(p[0])
92 if pr is not None:
96 if pr is not None:
93 precursors.add(hex(p[0])[:12])
97 precursors.add(hex(p[0])[:12])
94 return precursors
98 return precursors
95
99
96 @LazyProperty
100 @LazyProperty
97 def bookmarks(self):
101 def bookmarks(self):
98 return map(safe_unicode, self._ctx.bookmarks())
102 return map(safe_unicode, self._ctx.bookmarks())
99
103
100 @LazyProperty
104 @LazyProperty
101 def message(self):
105 def message(self):
102 return safe_unicode(self._ctx.description())
106 return safe_unicode(self._ctx.description())
103
107
104 @LazyProperty
108 @LazyProperty
105 def committer(self):
109 def committer(self):
106 return safe_unicode(self.author)
110 return safe_unicode(self.author)
107
111
108 @LazyProperty
112 @LazyProperty
109 def author(self):
113 def author(self):
110 return safe_unicode(self._ctx.user())
114 return safe_unicode(self._ctx.user())
111
115
112 @LazyProperty
116 @LazyProperty
113 def date(self):
117 def date(self):
114 return date_fromtimestamp(*self._ctx.date())
118 return date_fromtimestamp(*self._ctx.date())
115
119
116 @LazyProperty
120 @LazyProperty
117 def _timestamp(self):
121 def _timestamp(self):
118 return self._ctx.date()[0]
122 return self._ctx.date()[0]
119
123
120 @LazyProperty
124 @LazyProperty
121 def status(self):
125 def status(self):
122 """
126 """
123 Returns modified, added, removed, deleted files for current changeset
127 Returns modified, added, removed, deleted files for current changeset
124 """
128 """
125 return self.repository._repo.status(self._ctx.p1().node(),
129 return self.repository._repo.status(self._ctx.p1().node(),
126 self._ctx.node())
130 self._ctx.node())
127
131
128 @LazyProperty
132 @LazyProperty
129 def _file_paths(self):
133 def _file_paths(self):
130 return list(self._ctx)
134 return list(self._ctx)
131
135
132 @LazyProperty
136 @LazyProperty
133 def _dir_paths(self):
137 def _dir_paths(self):
134 p = list(set(get_dirs_for_path(*self._file_paths)))
138 p = list(set(get_dirs_for_path(*self._file_paths)))
135 p.insert(0, '')
139 p.insert(0, '')
136 return p
140 return p
137
141
138 @LazyProperty
142 @LazyProperty
139 def _paths(self):
143 def _paths(self):
140 return self._dir_paths + self._file_paths
144 return self._dir_paths + self._file_paths
141
145
142 @LazyProperty
146 @LazyProperty
143 def id(self):
147 def id(self):
144 if self.last:
148 if self.last:
145 return u'tip'
149 return u'tip'
146 return self.short_id
150 return self.short_id
147
151
148 @LazyProperty
152 @LazyProperty
149 def short_id(self):
153 def short_id(self):
150 return self.raw_id[:12]
154 return self.raw_id[:12]
151
155
152 @LazyProperty
156 @LazyProperty
153 def parents(self):
157 def parents(self):
154 """
158 """
155 Returns list of parents changesets.
159 Returns list of parents changesets.
156 """
160 """
157 return [self.repository.get_changeset(parent.rev())
161 return [self.repository.get_changeset(parent.rev())
158 for parent in self._ctx.parents() if parent.rev() >= 0]
162 for parent in self._ctx.parents() if parent.rev() >= 0]
159
163
160 @LazyProperty
164 @LazyProperty
161 def children(self):
165 def children(self):
162 """
166 """
163 Returns list of children changesets.
167 Returns list of children changesets.
164 """
168 """
165 return [self.repository.get_changeset(child.rev())
169 return [self.repository.get_changeset(child.rev())
166 for child in self._ctx.children() if child.rev() >= 0]
170 for child in self._ctx.children() if child.rev() >= 0]
167
171
168 def next(self, branch=None):
172 def next(self, branch=None):
169 if branch and self.branch != branch:
173 if branch and self.branch != branch:
170 raise VCSError('Branch option used on changeset not belonging '
174 raise VCSError('Branch option used on changeset not belonging '
171 'to that branch')
175 'to that branch')
172
176
173 cs = self
177 cs = self
174 while True:
178 while True:
175 try:
179 try:
176 next_ = cs.repository.revisions.index(cs.raw_id) + 1
180 next_ = cs.repository.revisions.index(cs.raw_id) + 1
177 next_rev = cs.repository.revisions[next_]
181 next_rev = cs.repository.revisions[next_]
178 except IndexError:
182 except IndexError:
179 raise ChangesetDoesNotExistError
183 raise ChangesetDoesNotExistError
180 cs = cs.repository.get_changeset(next_rev)
184 cs = cs.repository.get_changeset(next_rev)
181
185
182 if not branch or branch == cs.branch:
186 if not branch or branch == cs.branch:
183 return cs
187 return cs
184
188
185 def prev(self, branch=None):
189 def prev(self, branch=None):
186 if branch and self.branch != branch:
190 if branch and self.branch != branch:
187 raise VCSError('Branch option used on changeset not belonging '
191 raise VCSError('Branch option used on changeset not belonging '
188 'to that branch')
192 'to that branch')
189
193
190 cs = self
194 cs = self
191 while True:
195 while True:
192 try:
196 try:
193 prev_ = cs.repository.revisions.index(cs.raw_id) - 1
197 prev_ = cs.repository.revisions.index(cs.raw_id) - 1
194 if prev_ < 0:
198 if prev_ < 0:
195 raise IndexError
199 raise IndexError
196 prev_rev = cs.repository.revisions[prev_]
200 prev_rev = cs.repository.revisions[prev_]
197 except IndexError:
201 except IndexError:
198 raise ChangesetDoesNotExistError
202 raise ChangesetDoesNotExistError
199 cs = cs.repository.get_changeset(prev_rev)
203 cs = cs.repository.get_changeset(prev_rev)
200
204
201 if not branch or branch == cs.branch:
205 if not branch or branch == cs.branch:
202 return cs
206 return cs
203
207
204 def diff(self, ignore_whitespace=True, context=3):
208 def diff(self, ignore_whitespace=True, context=3):
205 return ''.join(self._ctx.diff(git=True,
209 return ''.join(self._ctx.diff(git=True,
206 ignore_whitespace=ignore_whitespace,
210 ignore_whitespace=ignore_whitespace,
207 context=context))
211 context=context))
208
212
209 def _fix_path(self, path):
213 def _fix_path(self, path):
210 """
214 """
211 Paths are stored without trailing slash so we need to get rid off it if
215 Paths are stored without trailing slash so we need to get rid off it if
212 needed. Also mercurial keeps filenodes as str so we need to decode
216 needed. Also mercurial keeps filenodes as str so we need to decode
213 from unicode to str
217 from unicode to str
214 """
218 """
215 if path.endswith('/'):
219 if path.endswith('/'):
216 path = path.rstrip('/')
220 path = path.rstrip('/')
217
221
218 return safe_str(path)
222 return safe_str(path)
219
223
220 def _get_kind(self, path):
224 def _get_kind(self, path):
221 path = self._fix_path(path)
225 path = self._fix_path(path)
222 if path in self._file_paths:
226 if path in self._file_paths:
223 return NodeKind.FILE
227 return NodeKind.FILE
224 elif path in self._dir_paths:
228 elif path in self._dir_paths:
225 return NodeKind.DIR
229 return NodeKind.DIR
226 else:
230 else:
227 raise ChangesetError("Node does not exist at the given path '%s'"
231 raise ChangesetError("Node does not exist at the given path '%s'"
228 % (path))
232 % (path))
229
233
230 def _get_filectx(self, path):
234 def _get_filectx(self, path):
231 path = self._fix_path(path)
235 path = self._fix_path(path)
232 if self._get_kind(path) != NodeKind.FILE:
236 if self._get_kind(path) != NodeKind.FILE:
233 raise ChangesetError("File does not exist for revision %s at "
237 raise ChangesetError("File does not exist for revision %s at "
234 " '%s'" % (self.raw_id, path))
238 " '%s'" % (self.raw_id, path))
235 return self._ctx.filectx(path)
239 return self._ctx.filectx(path)
236
240
237 def _extract_submodules(self):
241 def _extract_submodules(self):
238 """
242 """
239 returns a dictionary with submodule information from substate file
243 returns a dictionary with submodule information from substate file
240 of hg repository
244 of hg repository
241 """
245 """
242 return self._ctx.substate
246 return self._ctx.substate
243
247
244 def get_file_mode(self, path):
248 def get_file_mode(self, path):
245 """
249 """
246 Returns stat mode of the file at the given ``path``.
250 Returns stat mode of the file at the given ``path``.
247 """
251 """
248 fctx = self._get_filectx(path)
252 fctx = self._get_filectx(path)
249 if 'x' in fctx.flags():
253 if 'x' in fctx.flags():
250 return 0100755
254 return 0100755
251 else:
255 else:
252 return 0100644
256 return 0100644
253
257
254 def get_file_content(self, path):
258 def get_file_content(self, path):
255 """
259 """
256 Returns content of the file at given ``path``.
260 Returns content of the file at given ``path``.
257 """
261 """
258 fctx = self._get_filectx(path)
262 fctx = self._get_filectx(path)
259 return fctx.data()
263 return fctx.data()
260
264
261 def get_file_size(self, path):
265 def get_file_size(self, path):
262 """
266 """
263 Returns size of the file at given ``path``.
267 Returns size of the file at given ``path``.
264 """
268 """
265 fctx = self._get_filectx(path)
269 fctx = self._get_filectx(path)
266 return fctx.size()
270 return fctx.size()
267
271
268 def get_file_changeset(self, path):
272 def get_file_changeset(self, path):
269 """
273 """
270 Returns last commit of the file at the given ``path``.
274 Returns last commit of the file at the given ``path``.
271 """
275 """
272 return self.get_file_history(path, limit=1)[0]
276 return self.get_file_history(path, limit=1)[0]
273
277
274 def get_file_history(self, path, limit=None):
278 def get_file_history(self, path, limit=None):
275 """
279 """
276 Returns history of file as reversed list of ``Changeset`` objects for
280 Returns history of file as reversed list of ``Changeset`` objects for
277 which file at given ``path`` has been modified.
281 which file at given ``path`` has been modified.
278 """
282 """
279 fctx = self._get_filectx(path)
283 fctx = self._get_filectx(path)
280 hist = []
284 hist = []
281 cnt = 0
285 cnt = 0
282 for cs in reversed([x for x in fctx.filelog()]):
286 for cs in reversed([x for x in fctx.filelog()]):
283 cnt += 1
287 cnt += 1
284 hist.append(hex(fctx.filectx(cs).node()))
288 hist.append(hex(fctx.filectx(cs).node()))
285 if limit is not None and cnt == limit:
289 if limit is not None and cnt == limit:
286 break
290 break
287
291
288 return [self.repository.get_changeset(node) for node in hist]
292 return [self.repository.get_changeset(node) for node in hist]
289
293
290 def get_file_annotate(self, path):
294 def get_file_annotate(self, path):
291 """
295 """
292 Returns a generator of four element tuples with
296 Returns a generator of four element tuples with
293 lineno, sha, changeset lazy loader and line
297 lineno, sha, changeset lazy loader and line
294 """
298 """
295
299
296 fctx = self._get_filectx(path)
300 fctx = self._get_filectx(path)
297 for i, (aline, l) in enumerate(fctx.annotate(linenumber=False)):
301 for i, (aline, l) in enumerate(fctx.annotate(linenumber=False)):
298 ln_no = i + 1
302 ln_no = i + 1
299 try:
303 try:
300 fctx = aline.fctx
304 fctx = aline.fctx
301 except AttributeError: # aline.fctx was introduced in Mercurial 4.4
305 except AttributeError: # aline.fctx was introduced in Mercurial 4.4
302 fctx = aline[0]
306 fctx = aline[0]
303 sha = hex(fctx.node())
307 sha = hex(fctx.node())
304 yield (ln_no, sha, lambda: self.repository.get_changeset(sha), l)
308 yield (ln_no, sha, lambda: self.repository.get_changeset(sha), l)
305
309
306 def fill_archive(self, stream=None, kind='tgz', prefix=None,
310 def fill_archive(self, stream=None, kind='tgz', prefix=None,
307 subrepos=False):
311 subrepos=False):
308 """
312 """
309 Fills up given stream.
313 Fills up given stream.
310
314
311 :param stream: file like object.
315 :param stream: file like object.
312 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
316 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
313 Default: ``tgz``.
317 Default: ``tgz``.
314 :param prefix: name of root directory in archive.
318 :param prefix: name of root directory in archive.
315 Default is repository name and changeset's raw_id joined with dash
319 Default is repository name and changeset's raw_id joined with dash
316 (``repo-tip.<KIND>``).
320 (``repo-tip.<KIND>``).
317 :param subrepos: include subrepos in this archive.
321 :param subrepos: include subrepos in this archive.
318
322
319 :raise ImproperArchiveTypeError: If given kind is wrong.
323 :raise ImproperArchiveTypeError: If given kind is wrong.
320 :raise VcsError: If given stream is None
324 :raise VcsError: If given stream is None
321 """
325 """
322
326
323 allowed_kinds = settings.ARCHIVE_SPECS.keys()
327 allowed_kinds = settings.ARCHIVE_SPECS.keys()
324 if kind not in allowed_kinds:
328 if kind not in allowed_kinds:
325 raise ImproperArchiveTypeError('Archive kind not supported use one'
329 raise ImproperArchiveTypeError('Archive kind not supported use one'
326 'of %s', allowed_kinds)
330 'of %s', allowed_kinds)
327
331
328 if stream is None:
332 if stream is None:
329 raise VCSError('You need to pass in a valid stream for filling'
333 raise VCSError('You need to pass in a valid stream for filling'
330 ' with archival data')
334 ' with archival data')
331
335
332 if prefix is None:
336 if prefix is None:
333 prefix = '%s-%s' % (self.repository.name, self.short_id)
337 prefix = '%s-%s' % (self.repository.name, self.short_id)
334 elif prefix.startswith('/'):
338 elif prefix.startswith('/'):
335 raise VCSError("Prefix cannot start with leading slash")
339 raise VCSError("Prefix cannot start with leading slash")
336 elif prefix.strip() == '':
340 elif prefix.strip() == '':
337 raise VCSError("Prefix cannot be empty")
341 raise VCSError("Prefix cannot be empty")
338
342
339 archival.archive(self.repository._repo, stream, self.raw_id,
343 archival.archive(self.repository._repo, stream, self.raw_id,
340 kind, prefix=prefix, subrepos=subrepos)
344 kind, prefix=prefix, subrepos=subrepos)
341
345
342 def get_nodes(self, path):
346 def get_nodes(self, path):
343 """
347 """
344 Returns combined ``DirNode`` and ``FileNode`` objects list representing
348 Returns combined ``DirNode`` and ``FileNode`` objects list representing
345 state of changeset at the given ``path``. If node at the given ``path``
349 state of changeset at the given ``path``. If node at the given ``path``
346 is not instance of ``DirNode``, ChangesetError would be raised.
350 is not instance of ``DirNode``, ChangesetError would be raised.
347 """
351 """
348
352
349 if self._get_kind(path) != NodeKind.DIR:
353 if self._get_kind(path) != NodeKind.DIR:
350 raise ChangesetError("Directory does not exist for revision %s at "
354 raise ChangesetError("Directory does not exist for revision %s at "
351 " '%s'" % (self.revision, path))
355 " '%s'" % (self.revision, path))
352 path = self._fix_path(path)
356 path = self._fix_path(path)
353
357
354 filenodes = [FileNode(f, changeset=self) for f in self._file_paths
358 filenodes = [FileNode(f, changeset=self) for f in self._file_paths
355 if os.path.dirname(f) == path]
359 if os.path.dirname(f) == path]
356 dirs = path == '' and '' or [d for d in self._dir_paths
360 dirs = path == '' and '' or [d for d in self._dir_paths
357 if d and posixpath.dirname(d) == path]
361 if d and posixpath.dirname(d) == path]
358 dirnodes = [DirNode(d, changeset=self) for d in dirs
362 dirnodes = [DirNode(d, changeset=self) for d in dirs
359 if os.path.dirname(d) == path]
363 if os.path.dirname(d) == path]
360
364
361 als = self.repository.alias
365 als = self.repository.alias
362 for k, vals in self._extract_submodules().iteritems():
366 for k, vals in self._extract_submodules().iteritems():
363 #vals = url,rev,type
367 #vals = url,rev,type
364 loc = vals[0]
368 loc = vals[0]
365 cs = vals[1]
369 cs = vals[1]
366 dirnodes.append(SubModuleNode(k, url=loc, changeset=cs,
370 dirnodes.append(SubModuleNode(k, url=loc, changeset=cs,
367 alias=als))
371 alias=als))
368 nodes = dirnodes + filenodes
372 nodes = dirnodes + filenodes
369 # cache nodes
373 # cache nodes
370 for node in nodes:
374 for node in nodes:
371 self.nodes[node.path] = node
375 self.nodes[node.path] = node
372 nodes.sort()
376 nodes.sort()
373
377
374 return nodes
378 return nodes
375
379
376 def get_node(self, path):
380 def get_node(self, path):
377 """
381 """
378 Returns ``Node`` object from the given ``path``. If there is no node at
382 Returns ``Node`` object from the given ``path``. If there is no node at
379 the given ``path``, ``ChangesetError`` would be raised.
383 the given ``path``, ``ChangesetError`` would be raised.
380 """
384 """
381
385
382 path = self._fix_path(path)
386 path = self._fix_path(path)
383
387
384 if path not in self.nodes:
388 if path not in self.nodes:
385 if path in self._file_paths:
389 if path in self._file_paths:
386 node = FileNode(path, changeset=self)
390 node = FileNode(path, changeset=self)
387 elif path in self._dir_paths or path in self._dir_paths:
391 elif path in self._dir_paths or path in self._dir_paths:
388 if path == '':
392 if path == '':
389 node = RootNode(changeset=self)
393 node = RootNode(changeset=self)
390 else:
394 else:
391 node = DirNode(path, changeset=self)
395 node = DirNode(path, changeset=self)
392 else:
396 else:
393 raise NodeDoesNotExistError("There is no file nor directory "
397 raise NodeDoesNotExistError("There is no file nor directory "
394 "at the given path: '%s' at revision %s"
398 "at the given path: '%s' at revision %s"
395 % (path, self.short_id))
399 % (path, self.short_id))
396 # cache node
400 # cache node
397 self.nodes[path] = node
401 self.nodes[path] = node
398 return self.nodes[path]
402 return self.nodes[path]
399
403
400 @LazyProperty
404 @LazyProperty
401 def affected_files(self):
405 def affected_files(self):
402 """
406 """
403 Gets a fast accessible file changes for given changeset
407 Gets a fast accessible file changes for given changeset
404 """
408 """
405 return self._ctx.files()
409 return self._ctx.files()
406
410
407 @property
411 @property
408 def added(self):
412 def added(self):
409 """
413 """
410 Returns list of added ``FileNode`` objects.
414 Returns list of added ``FileNode`` objects.
411 """
415 """
412 return AddedFileNodesGenerator([n for n in self.status[1]], self)
416 return AddedFileNodesGenerator([n for n in self.status[1]], self)
413
417
414 @property
418 @property
415 def changed(self):
419 def changed(self):
416 """
420 """
417 Returns list of modified ``FileNode`` objects.
421 Returns list of modified ``FileNode`` objects.
418 """
422 """
419 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
423 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
420
424
421 @property
425 @property
422 def removed(self):
426 def removed(self):
423 """
427 """
424 Returns list of removed ``FileNode`` objects.
428 Returns list of removed ``FileNode`` objects.
425 """
429 """
426 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
430 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
427
431
428 @LazyProperty
432 @LazyProperty
429 def extra(self):
433 def extra(self):
430 return self._ctx.extra()
434 return self._ctx.extra()
@@ -1,119 +1,121 b''
1 ## Render changelog table with id 'changesets' with the range of changesets,
1 ## Render changelog table with id 'changesets' with the range of changesets,
2 ## statuses, and comments.
2 ## statuses, and comments.
3 ## Optionally, pass a js snippet to run whenever a table resize is triggered.
3 ## Optionally, pass a js snippet to run whenever a table resize is triggered.
4 <%def name="changelog(repo_name, cs_range, cs_statuses, cs_comments, show_checkbox=False, show_branch=True, show_index=False, resize_js='')">
4 <%def name="changelog(repo_name, cs_range, cs_statuses, cs_comments, show_checkbox=False, show_branch=True, show_index=False, resize_js='')">
5 <% num_cs = len(cs_range) %>
5 <% num_cs = len(cs_range) %>
6 <table class="table" id="changesets">
6 <table class="table" id="changesets">
7 <tbody>
7 <tbody>
8 %for cnt,cs in enumerate(cs_range):
8 %for cnt,cs in enumerate(cs_range):
9 <tr id="chg_${cnt+1}" class="${'mergerow' if len(cs.parents) > 1 else ''}">
9 <tr id="chg_${cnt+1}" class="${'mergerow' if len(cs.parents) > 1 else ''}">
10 %if show_checkbox:
10 %if show_checkbox:
11 <td class="checkbox-column">
11 <td class="checkbox-column">
12 ${h.checkbox(cs.raw_id,class_="changeset_range")}
12 ${h.checkbox(cs.raw_id,class_="changeset_range")}
13 </td>
13 </td>
14 %endif
14 %endif
15 %if show_index:
15 %if show_index:
16 <td class="changeset-logical-index">
16 <td class="changeset-logical-index">
17 <%
17 <%
18 index = num_cs - cnt
18 index = num_cs - cnt
19 if index == 1:
19 if index == 1:
20 title = _('First (oldest) changeset in this list')
20 title = _('First (oldest) changeset in this list')
21 elif index == num_cs:
21 elif index == num_cs:
22 title = _('Last (most recent) changeset in this list')
22 title = _('Last (most recent) changeset in this list')
23 else:
23 else:
24 title = _('Position in this list of changesets')
24 title = _('Position in this list of changesets')
25 %>
25 %>
26 <span data-toggle="tooltip" title="${title}">
26 <span data-toggle="tooltip" title="${title}">
27 ${index}
27 ${index}
28 </span>
28 </span>
29 </td>
29 </td>
30 %endif
30 %endif
31 <td class="status">
31 <td class="status">
32 %if cs_statuses.get(cs.raw_id):
32 %if cs_statuses.get(cs.raw_id):
33 %if cs_statuses.get(cs.raw_id)[2]:
33 %if cs_statuses.get(cs.raw_id)[2]:
34 <a data-toggle="tooltip"
34 <a data-toggle="tooltip"
35 title="${_('Changeset status: %s by %s\nClick to open associated pull request %s') % (cs_statuses.get(cs.raw_id)[1], cs_statuses.get(cs.raw_id)[5].username, cs_statuses.get(cs.raw_id)[4])}"
35 title="${_('Changeset status: %s by %s\nClick to open associated pull request %s') % (cs_statuses.get(cs.raw_id)[1], cs_statuses.get(cs.raw_id)[5].username, cs_statuses.get(cs.raw_id)[4])}"
36 href="${h.url('pullrequest_show',repo_name=cs_statuses.get(cs.raw_id)[3],pull_request_id=cs_statuses.get(cs.raw_id)[2])}">
36 href="${h.url('pullrequest_show',repo_name=cs_statuses.get(cs.raw_id)[3],pull_request_id=cs_statuses.get(cs.raw_id)[2])}">
37 <i class="icon-circle changeset-status-${cs_statuses.get(cs.raw_id)[0]}"></i>
37 <i class="icon-circle changeset-status-${cs_statuses.get(cs.raw_id)[0]}"></i>
38 </a>
38 </a>
39 %else:
39 %else:
40 <a data-toggle="tooltip"
40 <a data-toggle="tooltip"
41 title="${_('Changeset status: %s by %s') % (cs_statuses.get(cs.raw_id)[1], cs_statuses.get(cs.raw_id)[5].username)}"
41 title="${_('Changeset status: %s by %s') % (cs_statuses.get(cs.raw_id)[1], cs_statuses.get(cs.raw_id)[5].username)}"
42 href="${cs_comments[cs.raw_id][0].url()}">
42 href="${cs_comments[cs.raw_id][0].url()}">
43 <i class="icon-circle changeset-status-${cs_statuses.get(cs.raw_id)[0]}"></i>
43 <i class="icon-circle changeset-status-${cs_statuses.get(cs.raw_id)[0]}"></i>
44 </a>
44 </a>
45 %endif
45 %endif
46 %endif
46 %endif
47 </td>
47 </td>
48 <td class="author" data-toggle="tooltip" title="${cs.author}">
48 <td class="author" data-toggle="tooltip" title="${cs.author}">
49 ${h.gravatar(h.email_or_none(cs.author), size=16)}
49 ${h.gravatar(h.email_or_none(cs.author), size=16)}
50 <span class="user">${h.person(cs.author)}</span>
50 <span class="user">${h.person(cs.author)}</span>
51 </td>
51 </td>
52 <td class="hash">
52 <td class="hash">
53 ${h.link_to(h.show_id(cs),h.url('changeset_home',repo_name=repo_name,revision=cs.raw_id), class_='changeset_hash')}
53 ${h.link_to(h.show_id(cs),h.url('changeset_home',repo_name=repo_name,revision=cs.raw_id), class_='changeset_hash')}
54 </td>
54 </td>
55 <td class="date">
55 <td class="date">
56 <div data-toggle="tooltip" title="${h.fmt_date(cs.date)}">${h.age(cs.date,True)}</div>
56 <div data-toggle="tooltip" title="${h.fmt_date(cs.date)}">${h.age(cs.date,True)}</div>
57 </td>
57 </td>
58 <% message_lines = cs.message.splitlines() %>
58 <% message_lines = cs.message.splitlines() %>
59 %if len(message_lines) > 1:
59 %if len(message_lines) > 1:
60 <td class="expand_commit" title="${_('Expand commit message')}">
60 <td class="expand_commit" title="${_('Expand commit message')}">
61 <i class="icon-align-left"></i>
61 <i class="icon-align-left"></i>
62 </td>
62 </td>
63 %else:
63 %else:
64 <td class="expand_commit"></td>
64 <td class="expand_commit"></td>
65 %endif
65 %endif
66 <td class="mid">
66 <td class="mid">
67 <div class="log-container">
67 <div class="log-container">
68 <div class="message">
68 <div class="message">
69 <div class="message-firstline">${h.urlify_text(message_lines[0], c.repo_name,h.url('changeset_home',repo_name=repo_name,revision=cs.raw_id))}</div>
69 <div class="message-firstline">${h.urlify_text(message_lines[0], c.repo_name,h.url('changeset_home',repo_name=repo_name,revision=cs.raw_id))}</div>
70 %if len(message_lines) > 1:
70 %if len(message_lines) > 1:
71 <div class="message-full hidden">${h.urlify_text(cs.message, repo_name)}</div>
71 <div class="message-full hidden">${h.urlify_text(cs.message, repo_name)}</div>
72 %endif
72 %endif
73 </div>
73 </div>
74 <div class="extra-container">
74 <div class="extra-container">
75 %if cs_comments.get(cs.raw_id):
75 %if cs_comments.get(cs.raw_id):
76 <a class="comments-container comments-cnt" href="${cs_comments[cs.raw_id][0].url()}" data-toggle="tooltip" title="${_('%s comments') % len(cs_comments[cs.raw_id])}">${len(cs_comments[cs.raw_id])}<i class="icon-comment-discussion"></i>
76 <a class="comments-container comments-cnt" href="${cs_comments[cs.raw_id][0].url()}" data-toggle="tooltip" title="${_('%s comments') % len(cs_comments[cs.raw_id])}">${len(cs_comments[cs.raw_id])}<i class="icon-comment-discussion"></i>
77 </a>
77 </a>
78 %endif
78 %endif
79 %for book in cs.bookmarks:
79 %for book in cs.bookmarks:
80 <span class="label label-bookmark" title="${_('Bookmark %s') % book}">${h.link_to(book,h.url('changeset_home',repo_name=repo_name,revision=cs.raw_id))}</span>
80 <span class="label label-bookmark" title="${_('Bookmark %s') % book}">${h.link_to(book,h.url('changeset_home',repo_name=repo_name,revision=cs.raw_id))}</span>
81 %endfor
81 %endfor
82 %for tag in cs.tags:
82 %for tag in cs.tags:
83 <span class="label label-tag" title="${_('Tag %s') % tag}">${h.link_to(tag,h.url('changeset_home',repo_name=repo_name,revision=cs.raw_id))}</span>
83 <span class="label label-tag" title="${_('Tag %s') % tag}">${h.link_to(tag,h.url('changeset_home',repo_name=repo_name,revision=cs.raw_id))}</span>
84 %endfor
84 %endfor
85 %if cs.bumped:
85 %if cs.bumped:
86 <span class="label label-bumped" title="Bumped">Bumped</span>
86 <span class="label label-bumped" title="Bumped">Bumped</span>
87 %endif
87 %endif
88 %if cs.divergent:
88 %if cs.divergent:
89 <span class="label label-divergent" title="Divergent">Divergent</span>
89 <span class="label label-divergent" title="Divergent">Divergent</span>
90 %endif
90 %endif
91 %if cs.extinct:
91 %if cs.extinct:
92 <span class="label label-extinct" title="Extinct">Extinct</span>
92 <span class="label label-extinct" title="Extinct">Extinct</span>
93 %endif
93 %endif
94 %if cs.unstable:
94 %if cs.unstable:
95 <span class="label label-unstable" title="Unstable">Unstable</span>
95 <span class="label label-unstable" title="Unstable">Unstable</span>
96 %endif
96 %endif
97 %if cs.phase:
97 %if cs.phase:
98 <span class="label label-phase" title="Phase">${cs.phase}</span>
98 <span class="label label-phase" title="Phase">${cs.phase}</span>
99 %endif
99 %endif
100 %if show_branch and cs.branch:
100 %if show_branch:
101 <span class="label label-branch" title="${_('Branch %s' % cs.branch)}">${h.link_to(cs.branch,h.url('changelog_home',repo_name=repo_name,branch=cs.branch))}</span>
101 %for branch in cs.branches:
102 <span class="label label-branch" title="${_('Branch %s' % branch)}">${h.link_to(branch,h.url('changelog_home',repo_name=repo_name,branch=branch))}</span>
103 %endfor
102 %endif
104 %endif
103 </div>
105 </div>
104 </div>
106 </div>
105 </td>
107 </td>
106 </tr>
108 </tr>
107 %endfor
109 %endfor
108 </tbody>
110 </tbody>
109 </table>
111 </table>
110
112
111 <script type="text/javascript">
113 <script type="text/javascript">
112 $(document).ready(function() {
114 $(document).ready(function() {
113 $('#changesets .expand_commit').on('click',function(e){
115 $('#changesets .expand_commit').on('click',function(e){
114 $(this).next('.mid').find('.message > div').toggleClass('hidden');
116 $(this).next('.mid').find('.message > div').toggleClass('hidden');
115 ${resize_js};
117 ${resize_js};
116 });
118 });
117 });
119 });
118 </script>
120 </script>
119 </%def>
121 </%def>
@@ -1,207 +1,207 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2
2
3 <%inherit file="/base/base.html"/>
3 <%inherit file="/base/base.html"/>
4
4
5 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
5 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
6
6
7 <%block name="title">
7 <%block name="title">
8 ${_('%s Changeset') % c.repo_name} - ${h.show_id(c.changeset)}
8 ${_('%s Changeset') % c.repo_name} - ${h.show_id(c.changeset)}
9 </%block>
9 </%block>
10
10
11 <%def name="breadcrumbs_links()">
11 <%def name="breadcrumbs_links()">
12 ${_('Changeset')} - <span class='changeset_hash'>${h.show_id(c.changeset)}</span>
12 ${_('Changeset')} - <span class='changeset_hash'>${h.show_id(c.changeset)}</span>
13 </%def>
13 </%def>
14
14
15 <%block name="header_menu">
15 <%block name="header_menu">
16 ${self.menu('repositories')}
16 ${self.menu('repositories')}
17 </%block>
17 </%block>
18
18
19 <%def name="main()">
19 <%def name="main()">
20 ${self.repo_context_bar('changelog', c.changeset.raw_id)}
20 ${self.repo_context_bar('changelog', c.changeset.raw_id)}
21 <div class="panel panel-primary">
21 <div class="panel panel-primary">
22 <div class="panel-heading clearfix">
22 <div class="panel-heading clearfix">
23 ${self.breadcrumbs()}
23 ${self.breadcrumbs()}
24 </div>
24 </div>
25 <script>
25 <script>
26 var _USERS_AC_DATA = ${h.js(c.users_array)};
26 var _USERS_AC_DATA = ${h.js(c.users_array)};
27 AJAX_COMMENT_URL = ${h.js(url('changeset_comment',repo_name=c.repo_name,revision=c.changeset.raw_id))};
27 AJAX_COMMENT_URL = ${h.js(url('changeset_comment',repo_name=c.repo_name,revision=c.changeset.raw_id))};
28 AJAX_COMMENT_DELETE_URL = ${h.js(url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__'))};
28 AJAX_COMMENT_DELETE_URL = ${h.js(url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__'))};
29 </script>
29 </script>
30 <div class="panel-body">
30 <div class="panel-body">
31 <div class="panel panel-default">
31 <div class="panel panel-default">
32 <div class="panel-heading clearfix">
32 <div class="panel-heading clearfix">
33 ${self.parent_child_navigation()}
33 ${self.parent_child_navigation()}
34
34
35 <div class="pull-left" title="${_('Changeset status')}">
35 <div class="pull-left" title="${_('Changeset status')}">
36 %if c.statuses:
36 %if c.statuses:
37 <i class="icon-circle changeset-status-${c.statuses[0]}"></i>
37 <i class="icon-circle changeset-status-${c.statuses[0]}"></i>
38 [${h.changeset_status_lbl(c.statuses[0])}]
38 [${h.changeset_status_lbl(c.statuses[0])}]
39 %endif
39 %endif
40 </div>
40 </div>
41 <div class="diff-actions pull-left">
41 <div class="diff-actions pull-left">
42 <a href="${h.url('changeset_raw_home',repo_name=c.repo_name,revision=c.changeset.raw_id)}"
42 <a href="${h.url('changeset_raw_home',repo_name=c.repo_name,revision=c.changeset.raw_id)}"
43 data-toggle="tooltip"
43 data-toggle="tooltip"
44 title="${_('Raw diff')}"><i class="icon-diff"></i></a>
44 title="${_('Raw diff')}"><i class="icon-diff"></i></a>
45 <a href="${h.url('changeset_patch_home',repo_name=c.repo_name,revision=c.changeset.raw_id)}"
45 <a href="${h.url('changeset_patch_home',repo_name=c.repo_name,revision=c.changeset.raw_id)}"
46 data-toggle="tooltip"
46 data-toggle="tooltip"
47 title="${_('Patch diff')}"><i class="icon-file-powerpoint"></i></a>
47 title="${_('Patch diff')}"><i class="icon-file-powerpoint"></i></a>
48 <a href="${h.url('changeset_download_home',repo_name=c.repo_name,revision=c.changeset.raw_id,diff='download')}"
48 <a href="${h.url('changeset_download_home',repo_name=c.repo_name,revision=c.changeset.raw_id,diff='download')}"
49 data-toggle="tooltip"
49 data-toggle="tooltip"
50 title="${_('Download diff')}"><i class="icon-floppy"></i></a>
50 title="${_('Download diff')}"><i class="icon-floppy"></i></a>
51 ${c.ignorews_url(request.GET)}
51 ${c.ignorews_url(request.GET)}
52 ${c.context_url(request.GET)}
52 ${c.context_url(request.GET)}
53 </div>
53 </div>
54 </div>
54 </div>
55 <div class="panel-body">
55 <div class="panel-body">
56 <div class="form-group changeset_content_header clearfix">
56 <div class="form-group changeset_content_header clearfix">
57 <div class="pull-right">
57 <div class="pull-right">
58 <span class="logtags">
58 <span class="logtags">
59 %if len(c.changeset.parents)>1:
59 %if len(c.changeset.parents)>1:
60 <span class="label label-merge">${_('Merge')}</span>
60 <span class="label label-merge">${_('Merge')}</span>
61 %endif
61 %endif
62
62
63 %for book in c.changeset.bookmarks:
63 %for book in c.changeset.bookmarks:
64 <span class="label label-bookmark" title="${_('Bookmark %s') % book}">${h.link_to(book,h.url('changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}</span>
64 <span class="label label-bookmark" title="${_('Bookmark %s') % book}">${h.link_to(book,h.url('changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}</span>
65 %endfor
65 %endfor
66
66
67 %for tag in c.changeset.tags:
67 %for tag in c.changeset.tags:
68 <span class="label label-tag" title="${_('Tag %s') % tag}">${h.link_to(tag,h.url('changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}</span>
68 <span class="label label-tag" title="${_('Tag %s') % tag}">${h.link_to(tag,h.url('changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}</span>
69 %endfor
69 %endfor
70
70
71 %if c.changeset.branch:
71 %for branch in c.changeset.branches:
72 <span class="label label-branch" title="${_('Branch %s') % c.changeset.branch}">${h.link_to(c.changeset.branch,h.url('changelog_home',repo_name=c.repo_name,branch=c.changeset.branch))}</span>
72 <span class="label label-branch" title="${_('Branch %s') % branch}">${h.link_to(branch,h.url('changelog_home',repo_name=c.repo_name,branch=branch))}</span>
73 %endif
73 %endfor
74 </span>
74 </span>
75
75
76 <div class="changes">
76 <div class="changes">
77 % if (len(c.changeset.affected_files) <= c.affected_files_cut_off) or c.fulldiff:
77 % if (len(c.changeset.affected_files) <= c.affected_files_cut_off) or c.fulldiff:
78 <span class="label deleted" title="${_('Removed')}">${len(c.changeset.removed)}</span>
78 <span class="label deleted" title="${_('Removed')}">${len(c.changeset.removed)}</span>
79 <span class="label changed" title="${_('Changed')}">${len(c.changeset.changed)}</span>
79 <span class="label changed" title="${_('Changed')}">${len(c.changeset.changed)}</span>
80 <span class="label added" title="${_('Added')}">${len(c.changeset.added)}</span>
80 <span class="label added" title="${_('Added')}">${len(c.changeset.added)}</span>
81 % else:
81 % else:
82 <span class="label deleted" title="${_('Affected %s files') % len(c.changeset.affected_files)}">!</span>
82 <span class="label deleted" title="${_('Affected %s files') % len(c.changeset.affected_files)}">!</span>
83 <span class="label changed" title="${_('Affected %s files') % len(c.changeset.affected_files)}">!</span>
83 <span class="label changed" title="${_('Affected %s files') % len(c.changeset.affected_files)}">!</span>
84 <span class="label added" title="${_('Affected %s files') % len(c.changeset.affected_files)}">!</span>
84 <span class="label added" title="${_('Affected %s files') % len(c.changeset.affected_files)}">!</span>
85 % endif
85 % endif
86 </div>
86 </div>
87 </div>
87 </div>
88 <div class="pull-left">
88 <div class="pull-left">
89 <div class="author">
89 <div class="author">
90 ${h.gravatar_div(h.email_or_none(c.changeset.author), size=20)}
90 ${h.gravatar_div(h.email_or_none(c.changeset.author), size=20)}
91 <span><b>${h.person(c.changeset.author,'full_name_and_username')}</b> - ${h.age(c.changeset.date,True)} ${h.fmt_date(c.changeset.date)}</span><br/>
91 <span><b>${h.person(c.changeset.author,'full_name_and_username')}</b> - ${h.age(c.changeset.date,True)} ${h.fmt_date(c.changeset.date)}</span><br/>
92 <span>${h.email_or_none(c.changeset.author)}</span><br/>
92 <span>${h.email_or_none(c.changeset.author)}</span><br/>
93 </div>
93 </div>
94 <% rev = c.changeset.extra.get('source') %>
94 <% rev = c.changeset.extra.get('source') %>
95 %if rev:
95 %if rev:
96 <div>
96 <div>
97 ${_('Grafted from:')} ${h.link_to(h.short_id(rev),h.url('changeset_home',repo_name=c.repo_name,revision=rev), class_="changeset_hash")}
97 ${_('Grafted from:')} ${h.link_to(h.short_id(rev),h.url('changeset_home',repo_name=c.repo_name,revision=rev), class_="changeset_hash")}
98 </div>
98 </div>
99 %endif
99 %endif
100 <% rev = c.changeset.extra.get('transplant_source', '').encode('hex') %>
100 <% rev = c.changeset.extra.get('transplant_source', '').encode('hex') %>
101 %if rev:
101 %if rev:
102 <div>
102 <div>
103 ${_('Transplanted from:')} ${h.link_to(h.short_id(rev),h.url('changeset_home',repo_name=c.repo_name,revision=rev), class_="changeset_hash")}
103 ${_('Transplanted from:')} ${h.link_to(h.short_id(rev),h.url('changeset_home',repo_name=c.repo_name,revision=rev), class_="changeset_hash")}
104 </div>
104 </div>
105 %endif
105 %endif
106
106
107 % if hasattr(c.changeset, 'successors') and c.changeset.successors:
107 % if hasattr(c.changeset, 'successors') and c.changeset.successors:
108 <div class='successors'>
108 <div class='successors'>
109 <span class='successors_header'>${_('Replaced by:')} </span>
109 <span class='successors_header'>${_('Replaced by:')} </span>
110 % for i, s in enumerate(c.changeset.successors):
110 % for i, s in enumerate(c.changeset.successors):
111 <%
111 <%
112 comma = ""
112 comma = ""
113 if i != len(c.changeset.successors)-1:
113 if i != len(c.changeset.successors)-1:
114 comma = ", "
114 comma = ", "
115 %>
115 %>
116 <a class='successors_hash' href="${h.url('changeset_home',repo_name=c.repo_name, revision=s)}">${s}</a>${comma}
116 <a class='successors_hash' href="${h.url('changeset_home',repo_name=c.repo_name, revision=s)}">${s}</a>${comma}
117 % endfor
117 % endfor
118 </div>
118 </div>
119 % endif
119 % endif
120
120
121 % if hasattr(c.changeset, 'precursors') and c.changeset.precursors:
121 % if hasattr(c.changeset, 'precursors') and c.changeset.precursors:
122 <div class='precursors'>
122 <div class='precursors'>
123 <span class='precursors_header'>${_('Preceded by:')} </span>
123 <span class='precursors_header'>${_('Preceded by:')} </span>
124 % for i, s in enumerate(c.changeset.precursors):
124 % for i, s in enumerate(c.changeset.precursors):
125 <%
125 <%
126 comma = ""
126 comma = ""
127 if i != len(c.changeset.precursors)-1:
127 if i != len(c.changeset.precursors)-1:
128 comma = ", "
128 comma = ", "
129 %>
129 %>
130 <a class="precursors_hash" href="${h.url('changeset_home',repo_name=c.repo_name, revision=s)}">${s}</a>${comma}
130 <a class="precursors_hash" href="${h.url('changeset_home',repo_name=c.repo_name, revision=s)}">${s}</a>${comma}
131 % endfor
131 % endfor
132 </div>
132 </div>
133 % endif
133 % endif
134 </div>
134 </div>
135 </div>
135 </div>
136 <div class="form-group">${h.urlify_text(c.changeset.message, c.repo_name)}</div>
136 <div class="form-group">${h.urlify_text(c.changeset.message, c.repo_name)}</div>
137 <div>
137 <div>
138 <% a_rev, cs_rev, file_diff_data = c.changes[c.changeset.raw_id] %>
138 <% a_rev, cs_rev, file_diff_data = c.changes[c.changeset.raw_id] %>
139 % if c.limited_diff:
139 % if c.limited_diff:
140 ${ungettext('%s file changed', '%s files changed', len(file_diff_data)) % len(file_diff_data)}:
140 ${ungettext('%s file changed', '%s files changed', len(file_diff_data)) % len(file_diff_data)}:
141 % else:
141 % else:
142 ${ungettext('%s file changed with %s insertions and %s deletions', '%s files changed with %s insertions and %s deletions', len(file_diff_data)) % (len(file_diff_data), c.lines_added, c.lines_deleted)}:
142 ${ungettext('%s file changed with %s insertions and %s deletions', '%s files changed with %s insertions and %s deletions', len(file_diff_data)) % (len(file_diff_data), c.lines_added, c.lines_deleted)}:
143 %endif
143 %endif
144 </div>
144 </div>
145 <div class="cs_files">
145 <div class="cs_files">
146 %for fid, url_fid, op, a_path, path, diff, stats in file_diff_data:
146 %for fid, url_fid, op, a_path, path, diff, stats in file_diff_data:
147 <div class="cs_${op} clearfix">
147 <div class="cs_${op} clearfix">
148 <span class="node">
148 <span class="node">
149 <i class="icon-diff-${op}"></i>${h.link_to(h.safe_unicode(path), '#%s' % fid)}
149 <i class="icon-diff-${op}"></i>${h.link_to(h.safe_unicode(path), '#%s' % fid)}
150 </span>
150 </span>
151 <div class="changes">${h.fancy_file_stats(stats)}</div>
151 <div class="changes">${h.fancy_file_stats(stats)}</div>
152 </div>
152 </div>
153 %endfor
153 %endfor
154 %if c.limited_diff:
154 %if c.limited_diff:
155 <h5>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}">${_('Show full diff anyway')}</a></h5>
155 <h5>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}">${_('Show full diff anyway')}</a></h5>
156 %endif
156 %endif
157 </div>
157 </div>
158 <div class="comments-number">
158 <div class="comments-number">
159 ${comment.comment_count(c.inline_cnt, len(c.comments))}
159 ${comment.comment_count(c.inline_cnt, len(c.comments))}
160 </div>
160 </div>
161 </div>
161 </div>
162
162
163 </div>
163 </div>
164
164
165 ## diff block
165 ## diff block
166
166
167 <div class="commentable-diff">
167 <div class="commentable-diff">
168 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
168 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
169 ${diff_block.diff_block_js()}
169 ${diff_block.diff_block_js()}
170 <% a_rev, cs_rev, file_diff_data = c.changes[c.changeset.raw_id] %>
170 <% a_rev, cs_rev, file_diff_data = c.changes[c.changeset.raw_id] %>
171 ${diff_block.diff_block(c.repo_name, 'rev', a_rev, a_rev,
171 ${diff_block.diff_block(c.repo_name, 'rev', a_rev, a_rev,
172 c.repo_name, 'rev', cs_rev, cs_rev, file_diff_data)}
172 c.repo_name, 'rev', cs_rev, cs_rev, file_diff_data)}
173 % if c.limited_diff:
173 % if c.limited_diff:
174 <h4>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}">${_('Show full diff anyway')}</a></h4>
174 <h4>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}">${_('Show full diff anyway')}</a></h4>
175 % endif
175 % endif
176 </div>
176 </div>
177
177
178 ## template for inline comment form
178 ## template for inline comment form
179 ${comment.comment_inline_form()}
179 ${comment.comment_inline_form()}
180
180
181 ## render comments and inlines
181 ## render comments and inlines
182 ${comment.generate_comments()}
182 ${comment.generate_comments()}
183
183
184 ## main comment form and it status
184 ## main comment form and it status
185 ${comment.comments()}
185 ${comment.comments()}
186
186
187 </div>
187 </div>
188
188
189 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
189 ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
190 <script type="text/javascript">
190 <script type="text/javascript">
191 $(document).ready(function(){
191 $(document).ready(function(){
192 $('.code-difftable').on('click', '.add-bubble', function(e){
192 $('.code-difftable').on('click', '.add-bubble', function(e){
193 show_comment_form($(this));
193 show_comment_form($(this));
194 });
194 });
195
195
196 move_comments($(".comments .comments-list-chunk"));
196 move_comments($(".comments .comments-list-chunk"));
197
197
198 // hack: re-navigate to target after JS is done ... if a target is set and setting href thus won't reload
198 // hack: re-navigate to target after JS is done ... if a target is set and setting href thus won't reload
199 if (window.location.hash != "") {
199 if (window.location.hash != "") {
200 window.location.href = window.location.href;
200 window.location.href = window.location.href;
201 }
201 }
202 });
202 });
203
203
204 </script>
204 </script>
205
205
206 </div>
206 </div>
207 </%def>
207 </%def>
@@ -1,107 +1,107 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.html"/>
2 <%inherit file="/base/base.html"/>
3
3
4 <%block name="title">
4 <%block name="title">
5 ${_('%s Changesets') % c.repo_name} - ${h.show_id(c.cs_ranges[0])} &gt; ${h.show_id(c.cs_ranges[-1])}
5 ${_('%s Changesets') % c.repo_name} - ${h.show_id(c.cs_ranges[0])} &gt; ${h.show_id(c.cs_ranges[-1])}
6 </%block>
6 </%block>
7
7
8 <%def name="breadcrumbs_links()">
8 <%def name="breadcrumbs_links()">
9 ${_('Changesets')} -
9 ${_('Changesets')} -
10 ${h.link_to(h.show_id(c.cs_ranges[0]),h.url('changeset_home',repo_name=c.repo_name,revision=c.cs_ranges[0].raw_id))}
10 ${h.link_to(h.show_id(c.cs_ranges[0]),h.url('changeset_home',repo_name=c.repo_name,revision=c.cs_ranges[0].raw_id))}
11 <i class="icon-right"></i>
11 <i class="icon-right"></i>
12 ${h.link_to(h.show_id(c.cs_ranges[-1]),h.url('changeset_home',repo_name=c.repo_name,revision=c.cs_ranges[-1].raw_id))}
12 ${h.link_to(h.show_id(c.cs_ranges[-1]),h.url('changeset_home',repo_name=c.repo_name,revision=c.cs_ranges[-1].raw_id))}
13 </%def>
13 </%def>
14
14
15 <%block name="header_menu">
15 <%block name="header_menu">
16 ${self.menu('repositories')}
16 ${self.menu('repositories')}
17 </%block>
17 </%block>
18
18
19 <%def name="main()">
19 <%def name="main()">
20 ${self.repo_context_bar('changelog')}
20 ${self.repo_context_bar('changelog')}
21 <div class="panel panel-primary">
21 <div class="panel panel-primary">
22 <div class="panel-heading clearfix">
22 <div class="panel-heading clearfix">
23 <div class="pull-left">
23 <div class="pull-left">
24 ${self.breadcrumbs()}
24 ${self.breadcrumbs()}
25 </div>
25 </div>
26 <div class="pull-right">
26 <div class="pull-right">
27 <a href="${h.url('compare_url',repo_name=c.repo_name,org_ref_type='rev',org_ref_name=getattr(c.cs_ranges[0].parents[0] if c.cs_ranges[0].parents else h.EmptyChangeset(),'raw_id'),other_ref_type='rev',other_ref_name=c.cs_ranges[-1].raw_id)}" class="btn btn-default btn-sm"><i class="icon-git-compare"></i>Compare Revisions</a>
27 <a href="${h.url('compare_url',repo_name=c.repo_name,org_ref_type='rev',org_ref_name=getattr(c.cs_ranges[0].parents[0] if c.cs_ranges[0].parents else h.EmptyChangeset(),'raw_id'),other_ref_type='rev',other_ref_name=c.cs_ranges[-1].raw_id)}" class="btn btn-default btn-sm"><i class="icon-git-compare"></i>Compare Revisions</a>
28 </div>
28 </div>
29 </div>
29 </div>
30 <div class="panel-body">
30 <div class="panel-body">
31 <div>
31 <div>
32 <table class="table compare_view_commits">
32 <table class="table compare_view_commits">
33 %for cnt,cs in enumerate(c.cs_ranges):
33 %for cnt,cs in enumerate(c.cs_ranges):
34 <tr>
34 <tr>
35 %if c.visual.use_gravatar:
35 %if c.visual.use_gravatar:
36 <td>${h.gravatar_div(h.email_or_none(cs.author), size=14)}</td>
36 <td>${h.gravatar_div(h.email_or_none(cs.author), size=14)}</td>
37 %endif
37 %endif
38 <td>${h.link_to(h.short_id(cs.raw_id),h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id))}</td>
38 <td>${h.link_to(h.short_id(cs.raw_id),h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id))}</td>
39 <td class="author">${h.person(cs.author)}</td>
39 <td class="author">${h.person(cs.author)}</td>
40 <td><span data-toggle="tooltip" title="${h.age(cs.date)}">${cs.date}</span></td>
40 <td><span data-toggle="tooltip" title="${h.age(cs.date)}">${cs.date}</span></td>
41 <td>
41 <td>
42 %if c.statuses:
42 %if c.statuses:
43 <i class="icon-circle changeset-status-${c.statuses[cnt]}" title="${_('Changeset status: %s') % h.changeset_status_lbl(c.statuses[cnt])}"></i>
43 <i class="icon-circle changeset-status-${c.statuses[cnt]}" title="${_('Changeset status: %s') % h.changeset_status_lbl(c.statuses[cnt])}"></i>
44 %endif
44 %endif
45 </td>
45 </td>
46 <td><div class="message">${h.urlify_text(h.wrap_paragraphs(cs.message),c.repo_name)}</div></td>
46 <td><div class="message">${h.urlify_text(h.wrap_paragraphs(cs.message),c.repo_name)}</div></td>
47 </tr>
47 </tr>
48 %endfor
48 %endfor
49 </table>
49 </table>
50 <h4>${_('Files affected')}</h4>
50 <h4>${_('Files affected')}</h4>
51 <div class="cs_files">
51 <div class="cs_files">
52 %for cs in c.cs_ranges:
52 %for cs in c.cs_ranges:
53 <h6>${h.link_to(h.show_id(cs),h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id))}</h6>
53 <h6>${h.link_to(h.show_id(cs),h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id))}</h6>
54 <% a_rev, cs_rev, file_diff_data = c.changes[cs.raw_id] %>
54 <% a_rev, cs_rev, file_diff_data = c.changes[cs.raw_id] %>
55 %for fid, url_fid, op, a_path, path, diff, stats in file_diff_data:
55 %for fid, url_fid, op, a_path, path, diff, stats in file_diff_data:
56 <div class="cs_${op} clearfix">
56 <div class="cs_${op} clearfix">
57 <span class="node">
57 <span class="node">
58 <i class="icon-diff-${op}"></i>
58 <i class="icon-diff-${op}"></i>
59 ${h.link_to(h.safe_unicode(path), '#%s' % fid)}
59 ${h.link_to(h.safe_unicode(path), '#%s' % fid)}
60 </span>
60 </span>
61 <div class="changes">${h.fancy_file_stats(stats)}</div>
61 <div class="changes">${h.fancy_file_stats(stats)}</div>
62 </div>
62 </div>
63 %endfor
63 %endfor
64 %endfor
64 %endfor
65 </div>
65 </div>
66 </div>
66 </div>
67 </div>
67 </div>
68 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
68 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
69 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
69 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
70 ${diff_block.diff_block_js()}
70 ${diff_block.diff_block_js()}
71 %for cs in c.cs_ranges:
71 %for cs in c.cs_ranges:
72 <div class="panel-body">
72 <div class="panel-body">
73 ## diff block
73 ## diff block
74 <div class="h3">
74 <div class="h3">
75 ${h.gravatar_div(h.email_or_none(cs.author), size=20)}
75 ${h.gravatar_div(h.email_or_none(cs.author), size=20)}
76 <a data-toggle="tooltip" title="${cs.message}" href="${h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id)}">${h.show_id(cs)}</a>
76 <a data-toggle="tooltip" title="${cs.message}" href="${h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id)}">${h.show_id(cs)}</a>
77 <div class="right">
77 <div class="right">
78 <span class="logtags">
78 <span class="logtags">
79 %if len(cs.parents)>1:
79 %if len(cs.parents)>1:
80 <span class="label label-merge">${_('Merge')}</span>
80 <span class="label label-merge">${_('Merge')}</span>
81 %endif
81 %endif
82 %if h.is_hg(c.db_repo_scm_instance):
82 %if h.is_hg(c.db_repo_scm_instance):
83 %for book in cs.bookmarks:
83 %for book in cs.bookmarks:
84 <span class="label label-bookmark" title="${_('Bookmark %s') % book}">
84 <span class="label label-bookmark" title="${_('Bookmark %s') % book}">
85 ${h.link_to(book,h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id))}
85 ${h.link_to(book,h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id))}
86 </span>
86 </span>
87 %endfor
87 %endfor
88 %endif
88 %endif
89 %for tag in cs.tags:
89 %for tag in cs.tags:
90 <span class="label label-tag" title="${_('Tag %s') % tag}">
90 <span class="label label-tag" title="${_('Tag %s') % tag}">
91 ${h.link_to(tag,h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id))}</span>
91 ${h.link_to(tag,h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id))}</span>
92 %endfor
92 %endfor
93 %if cs.branch:
93 %for branch in cs.branches:
94 <span class="label label-branch" title="${_('Branch %s') % cs.branch}">
94 <span class="label label-branch" title="${_('Branch %s') % branch}">
95 ${h.link_to(cs.branch,h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id))}
95 ${h.link_to(branch,h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id))}
96 </span>
96 </span>
97 %endif
97 %endfor
98 </span>
98 </span>
99 </div>
99 </div>
100 </div>
100 </div>
101 <% a_rev, cs_rev, file_diff_data = c.changes[cs.raw_id] %>
101 <% a_rev, cs_rev, file_diff_data = c.changes[cs.raw_id] %>
102 ${diff_block.diff_block(c.repo_name, 'rev', a_rev, a_rev,
102 ${diff_block.diff_block(c.repo_name, 'rev', a_rev, a_rev,
103 c.repo_name, 'rev', cs_rev, cs_rev, file_diff_data)}
103 c.repo_name, 'rev', cs_rev, cs_rev, file_diff_data)}
104 </div>
104 </div>
105 %endfor
105 %endfor
106 </div>
106 </div>
107 </%def>
107 </%def>
@@ -1,108 +1,110 b''
1 import datetime
1 import datetime
2 from kallithea.lib import vcs
2 from kallithea.lib import vcs
3 from kallithea.lib.vcs.nodes import FileNode
3 from kallithea.lib.vcs.nodes import FileNode
4
4
5 from kallithea.tests.vcs.base import _BackendTestMixin
5 from kallithea.tests.vcs.base import _BackendTestMixin
6 from kallithea.tests.vcs.conf import SCM_TESTS
6 from kallithea.tests.vcs.conf import SCM_TESTS
7
7
8
8
9 class BranchesTestCaseMixin(_BackendTestMixin):
9 class BranchesTestCaseMixin(_BackendTestMixin):
10
10
11 @classmethod
11 @classmethod
12 def _get_commits(cls):
12 def _get_commits(cls):
13 commits = [
13 commits = [
14 {
14 {
15 'message': 'Initial commit',
15 'message': 'Initial commit',
16 'author': 'Joe Doe <joe.doe@example.com>',
16 'author': 'Joe Doe <joe.doe@example.com>',
17 'date': datetime.datetime(2010, 1, 1, 20),
17 'date': datetime.datetime(2010, 1, 1, 20),
18 'added': [
18 'added': [
19 FileNode('foobar', content='Foobar'),
19 FileNode('foobar', content='Foobar'),
20 FileNode('foobar2', content='Foobar II'),
20 FileNode('foobar2', content='Foobar II'),
21 FileNode('foo/bar/baz', content='baz here!'),
21 FileNode('foo/bar/baz', content='baz here!'),
22 ],
22 ],
23 },
23 },
24 {
24 {
25 'message': 'Changes...',
25 'message': 'Changes...',
26 'author': 'Jane Doe <jane.doe@example.com>',
26 'author': 'Jane Doe <jane.doe@example.com>',
27 'date': datetime.datetime(2010, 1, 1, 21),
27 'date': datetime.datetime(2010, 1, 1, 21),
28 'added': [
28 'added': [
29 FileNode('some/new.txt', content='news...'),
29 FileNode('some/new.txt', content='news...'),
30 ],
30 ],
31 'changed': [
31 'changed': [
32 FileNode('foobar', 'Foobar I'),
32 FileNode('foobar', 'Foobar I'),
33 ],
33 ],
34 'removed': [],
34 'removed': [],
35 },
35 },
36 ]
36 ]
37 return commits
37 return commits
38
38
39 def test_simple(self):
39 def test_simple(self):
40 tip = self.repo.get_changeset()
40 tip = self.repo.get_changeset()
41 assert tip.date == datetime.datetime(2010, 1, 1, 21)
41 assert tip.date == datetime.datetime(2010, 1, 1, 21)
42
42
43 def test_new_branch(self):
43 def test_new_branch(self):
44 # This check must not be removed to ensure the 'branches' LazyProperty
44 # This check must not be removed to ensure the 'branches' LazyProperty
45 # gets hit *before* the new 'foobar' branch got created:
45 # gets hit *before* the new 'foobar' branch got created:
46 assert 'foobar' not in self.repo.branches
46 assert 'foobar' not in self.repo.branches
47 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
47 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
48 content='Documentation\n'))
48 content='Documentation\n'))
49 foobar_tip = self.imc.commit(
49 foobar_tip = self.imc.commit(
50 message=u'New branch: foobar',
50 message=u'New branch: foobar',
51 author=u'joe',
51 author=u'joe',
52 branch='foobar',
52 branch='foobar',
53 )
53 )
54 assert 'foobar' in self.repo.branches
54 assert 'foobar' in self.repo.branches
55 assert foobar_tip.branch == 'foobar'
55 assert foobar_tip.branch == 'foobar'
56 assert foobar_tip.branches == ['foobar']
56
57
57 def test_new_head(self):
58 def test_new_head(self):
58 tip = self.repo.get_changeset()
59 tip = self.repo.get_changeset()
59 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
60 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
60 content='Documentation\n'))
61 content='Documentation\n'))
61 foobar_tip = self.imc.commit(
62 foobar_tip = self.imc.commit(
62 message=u'New branch: foobar',
63 message=u'New branch: foobar',
63 author=u'joe',
64 author=u'joe',
64 branch='foobar',
65 branch='foobar',
65 parents=[tip],
66 parents=[tip],
66 )
67 )
67 self.imc.change(vcs.nodes.FileNode('docs/index.txt',
68 self.imc.change(vcs.nodes.FileNode('docs/index.txt',
68 content='Documentation\nand more...\n'))
69 content='Documentation\nand more...\n'))
69 newtip = self.imc.commit(
70 newtip = self.imc.commit(
70 message=u'At default branch',
71 message=u'At default branch',
71 author=u'joe',
72 author=u'joe',
72 branch=foobar_tip.branch,
73 branch=foobar_tip.branch,
73 parents=[foobar_tip],
74 parents=[foobar_tip],
74 )
75 )
75
76
76 newest_tip = self.imc.commit(
77 newest_tip = self.imc.commit(
77 message=u'Merged with %s' % foobar_tip.raw_id,
78 message=u'Merged with %s' % foobar_tip.raw_id,
78 author=u'joe',
79 author=u'joe',
79 branch=self.backend_class.DEFAULT_BRANCH_NAME,
80 branch=self.backend_class.DEFAULT_BRANCH_NAME,
80 parents=[newtip, foobar_tip],
81 parents=[newtip, foobar_tip],
81 )
82 )
82
83
83 assert newest_tip.branch == self.backend_class.DEFAULT_BRANCH_NAME
84 assert newest_tip.branch == self.backend_class.DEFAULT_BRANCH_NAME
85 assert newest_tip.branches == [self.backend_class.DEFAULT_BRANCH_NAME]
84
86
85 def test_branch_with_slash_in_name(self):
87 def test_branch_with_slash_in_name(self):
86 self.imc.add(vcs.nodes.FileNode('extrafile', content='Some data\n'))
88 self.imc.add(vcs.nodes.FileNode('extrafile', content='Some data\n'))
87 self.imc.commit(u'Branch with a slash!', author=u'joe',
89 self.imc.commit(u'Branch with a slash!', author=u'joe',
88 branch='issue/123')
90 branch='issue/123')
89 assert 'issue/123' in self.repo.branches
91 assert 'issue/123' in self.repo.branches
90
92
91 def test_branch_with_slash_in_name_and_similar_without(self):
93 def test_branch_with_slash_in_name_and_similar_without(self):
92 self.imc.add(vcs.nodes.FileNode('extrafile', content='Some data\n'))
94 self.imc.add(vcs.nodes.FileNode('extrafile', content='Some data\n'))
93 self.imc.commit(u'Branch with a slash!', author=u'joe',
95 self.imc.commit(u'Branch with a slash!', author=u'joe',
94 branch='issue/123')
96 branch='issue/123')
95 self.imc.add(vcs.nodes.FileNode('extrafile II', content='Some data\n'))
97 self.imc.add(vcs.nodes.FileNode('extrafile II', content='Some data\n'))
96 self.imc.commit(u'Branch without a slash...', author=u'joe',
98 self.imc.commit(u'Branch without a slash...', author=u'joe',
97 branch='123')
99 branch='123')
98 assert 'issue/123' in self.repo.branches
100 assert 'issue/123' in self.repo.branches
99 assert '123' in self.repo.branches
101 assert '123' in self.repo.branches
100
102
101
103
102 # For each backend create test case class
104 # For each backend create test case class
103 for alias in SCM_TESTS:
105 for alias in SCM_TESTS:
104 attrs = {
106 attrs = {
105 'backend_alias': alias,
107 'backend_alias': alias,
106 }
108 }
107 cls_name = ''.join(('test %s branches' % alias).title().split())
109 cls_name = ''.join(('test %s branches' % alias).title().split())
108 globals()[cls_name] = type(cls_name, (BranchesTestCaseMixin,), attrs)
110 globals()[cls_name] = type(cls_name, (BranchesTestCaseMixin,), attrs)
@@ -1,390 +1,392 b''
1 # encoding: utf8
1 # encoding: utf8
2
2
3 import time
3 import time
4 import datetime
4 import datetime
5
5
6 import pytest
6 import pytest
7
7
8 from kallithea.lib import vcs
8 from kallithea.lib import vcs
9
9
10 from kallithea.lib.vcs.backends.base import BaseChangeset
10 from kallithea.lib.vcs.backends.base import BaseChangeset
11 from kallithea.lib.vcs.nodes import (
11 from kallithea.lib.vcs.nodes import (
12 FileNode, AddedFileNodesGenerator,
12 FileNode, AddedFileNodesGenerator,
13 ChangedFileNodesGenerator, RemovedFileNodesGenerator
13 ChangedFileNodesGenerator, RemovedFileNodesGenerator
14 )
14 )
15 from kallithea.lib.vcs.exceptions import (
15 from kallithea.lib.vcs.exceptions import (
16 BranchDoesNotExistError, ChangesetDoesNotExistError,
16 BranchDoesNotExistError, ChangesetDoesNotExistError,
17 RepositoryError, EmptyRepositoryError
17 RepositoryError, EmptyRepositoryError
18 )
18 )
19
19
20 from kallithea.tests.vcs.base import _BackendTestMixin
20 from kallithea.tests.vcs.base import _BackendTestMixin
21 from kallithea.tests.vcs.conf import SCM_TESTS, get_new_dir
21 from kallithea.tests.vcs.conf import SCM_TESTS, get_new_dir
22
22
23
23
24 class TestBaseChangeset(object):
24 class TestBaseChangeset(object):
25
25
26 def test_as_dict(self):
26 def test_as_dict(self):
27 changeset = BaseChangeset()
27 changeset = BaseChangeset()
28 changeset.id = 'ID'
28 changeset.id = 'ID'
29 changeset.raw_id = 'RAW_ID'
29 changeset.raw_id = 'RAW_ID'
30 changeset.short_id = 'SHORT_ID'
30 changeset.short_id = 'SHORT_ID'
31 changeset.revision = 1009
31 changeset.revision = 1009
32 changeset.date = datetime.datetime(2011, 1, 30, 1, 45)
32 changeset.date = datetime.datetime(2011, 1, 30, 1, 45)
33 changeset.message = 'Message of a commit'
33 changeset.message = 'Message of a commit'
34 changeset.author = 'Joe Doe <joe.doe@example.com>'
34 changeset.author = 'Joe Doe <joe.doe@example.com>'
35 changeset.added = [FileNode('foo/bar/baz'), FileNode(u'foobar'), FileNode(u'blΓ₯bΓ¦rgrΓΈd')]
35 changeset.added = [FileNode('foo/bar/baz'), FileNode(u'foobar'), FileNode(u'blΓ₯bΓ¦rgrΓΈd')]
36 changeset.changed = []
36 changeset.changed = []
37 changeset.removed = []
37 changeset.removed = []
38 assert changeset.as_dict() == {
38 assert changeset.as_dict() == {
39 'id': 'ID',
39 'id': 'ID',
40 'raw_id': 'RAW_ID',
40 'raw_id': 'RAW_ID',
41 'short_id': 'SHORT_ID',
41 'short_id': 'SHORT_ID',
42 'revision': 1009,
42 'revision': 1009,
43 'date': datetime.datetime(2011, 1, 30, 1, 45),
43 'date': datetime.datetime(2011, 1, 30, 1, 45),
44 'message': 'Message of a commit',
44 'message': 'Message of a commit',
45 'author': {
45 'author': {
46 'name': 'Joe Doe',
46 'name': 'Joe Doe',
47 'email': 'joe.doe@example.com',
47 'email': 'joe.doe@example.com',
48 },
48 },
49 'added': ['foo/bar/baz', 'foobar', u'bl\xe5b\xe6rgr\xf8d'],
49 'added': ['foo/bar/baz', 'foobar', u'bl\xe5b\xe6rgr\xf8d'],
50 'changed': [],
50 'changed': [],
51 'removed': [],
51 'removed': [],
52 }
52 }
53
53
54
54
55 class _ChangesetsWithCommitsTestCaseixin(_BackendTestMixin):
55 class _ChangesetsWithCommitsTestCaseixin(_BackendTestMixin):
56 recreate_repo_per_test = True
56 recreate_repo_per_test = True
57
57
58 @classmethod
58 @classmethod
59 def _get_commits(cls):
59 def _get_commits(cls):
60 start_date = datetime.datetime(2010, 1, 1, 20)
60 start_date = datetime.datetime(2010, 1, 1, 20)
61 for x in xrange(5):
61 for x in xrange(5):
62 yield {
62 yield {
63 'message': 'Commit %d' % x,
63 'message': 'Commit %d' % x,
64 'author': 'Joe Doe <joe.doe@example.com>',
64 'author': 'Joe Doe <joe.doe@example.com>',
65 'date': start_date + datetime.timedelta(hours=12 * x),
65 'date': start_date + datetime.timedelta(hours=12 * x),
66 'added': [
66 'added': [
67 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
67 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
68 ],
68 ],
69 }
69 }
70
70
71 def test_new_branch(self):
71 def test_new_branch(self):
72 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
72 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
73 content='Documentation\n'))
73 content='Documentation\n'))
74 foobar_tip = self.imc.commit(
74 foobar_tip = self.imc.commit(
75 message=u'New branch: foobar',
75 message=u'New branch: foobar',
76 author=u'joe',
76 author=u'joe',
77 branch='foobar',
77 branch='foobar',
78 )
78 )
79 assert 'foobar' in self.repo.branches
79 assert 'foobar' in self.repo.branches
80 assert foobar_tip.branch == 'foobar'
80 assert foobar_tip.branch == 'foobar'
81 assert foobar_tip.branches == ['foobar']
81 # 'foobar' should be the only branch that contains the new commit
82 # 'foobar' should be the only branch that contains the new commit
82 branch_tips = self.repo.branches.values()
83 branch_tips = self.repo.branches.values()
83 assert branch_tips.count(str(foobar_tip.raw_id)) == 1
84 assert branch_tips.count(str(foobar_tip.raw_id)) == 1
84
85
85 def test_new_head_in_default_branch(self):
86 def test_new_head_in_default_branch(self):
86 tip = self.repo.get_changeset()
87 tip = self.repo.get_changeset()
87 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
88 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
88 content='Documentation\n'))
89 content='Documentation\n'))
89 foobar_tip = self.imc.commit(
90 foobar_tip = self.imc.commit(
90 message=u'New branch: foobar',
91 message=u'New branch: foobar',
91 author=u'joe',
92 author=u'joe',
92 branch='foobar',
93 branch='foobar',
93 parents=[tip],
94 parents=[tip],
94 )
95 )
95 self.imc.change(vcs.nodes.FileNode('docs/index.txt',
96 self.imc.change(vcs.nodes.FileNode('docs/index.txt',
96 content='Documentation\nand more...\n'))
97 content='Documentation\nand more...\n'))
97 newtip = self.imc.commit(
98 newtip = self.imc.commit(
98 message=u'At default branch',
99 message=u'At default branch',
99 author=u'joe',
100 author=u'joe',
100 branch=foobar_tip.branch,
101 branch=foobar_tip.branch,
101 parents=[foobar_tip],
102 parents=[foobar_tip],
102 )
103 )
103
104
104 newest_tip = self.imc.commit(
105 newest_tip = self.imc.commit(
105 message=u'Merged with %s' % foobar_tip.raw_id,
106 message=u'Merged with %s' % foobar_tip.raw_id,
106 author=u'joe',
107 author=u'joe',
107 branch=self.backend_class.DEFAULT_BRANCH_NAME,
108 branch=self.backend_class.DEFAULT_BRANCH_NAME,
108 parents=[newtip, foobar_tip],
109 parents=[newtip, foobar_tip],
109 )
110 )
110
111
111 assert newest_tip.branch == self.backend_class.DEFAULT_BRANCH_NAME
112 assert newest_tip.branch == self.backend_class.DEFAULT_BRANCH_NAME
113 assert newest_tip.branches == [self.backend_class.DEFAULT_BRANCH_NAME]
112
114
113 def test_get_changesets_respects_branch_name(self):
115 def test_get_changesets_respects_branch_name(self):
114 tip = self.repo.get_changeset()
116 tip = self.repo.get_changeset()
115 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
117 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
116 content='Documentation\n'))
118 content='Documentation\n'))
117 doc_changeset = self.imc.commit(
119 doc_changeset = self.imc.commit(
118 message=u'New branch: docs',
120 message=u'New branch: docs',
119 author=u'joe',
121 author=u'joe',
120 branch='docs',
122 branch='docs',
121 )
123 )
122 self.imc.add(vcs.nodes.FileNode('newfile', content=''))
124 self.imc.add(vcs.nodes.FileNode('newfile', content=''))
123 self.imc.commit(
125 self.imc.commit(
124 message=u'Back in default branch',
126 message=u'Back in default branch',
125 author=u'joe',
127 author=u'joe',
126 parents=[tip],
128 parents=[tip],
127 )
129 )
128 default_branch_changesets = self.repo.get_changesets(
130 default_branch_changesets = self.repo.get_changesets(
129 branch_name=self.repo.DEFAULT_BRANCH_NAME)
131 branch_name=self.repo.DEFAULT_BRANCH_NAME)
130 assert doc_changeset not in default_branch_changesets
132 assert doc_changeset not in default_branch_changesets
131
133
132 def test_get_changeset_by_branch(self):
134 def test_get_changeset_by_branch(self):
133 for branch, sha in self.repo.branches.iteritems():
135 for branch, sha in self.repo.branches.iteritems():
134 assert sha == self.repo.get_changeset(branch).raw_id
136 assert sha == self.repo.get_changeset(branch).raw_id
135
137
136 def test_get_changeset_by_tag(self):
138 def test_get_changeset_by_tag(self):
137 for tag, sha in self.repo.tags.iteritems():
139 for tag, sha in self.repo.tags.iteritems():
138 assert sha == self.repo.get_changeset(tag).raw_id
140 assert sha == self.repo.get_changeset(tag).raw_id
139
141
140 def test_get_changeset_parents(self):
142 def test_get_changeset_parents(self):
141 for test_rev in [1, 2, 3]:
143 for test_rev in [1, 2, 3]:
142 sha = self.repo.get_changeset(test_rev-1)
144 sha = self.repo.get_changeset(test_rev-1)
143 assert [sha] == self.repo.get_changeset(test_rev).parents
145 assert [sha] == self.repo.get_changeset(test_rev).parents
144
146
145 def test_get_changeset_children(self):
147 def test_get_changeset_children(self):
146 for test_rev in [1, 2, 3]:
148 for test_rev in [1, 2, 3]:
147 sha = self.repo.get_changeset(test_rev+1)
149 sha = self.repo.get_changeset(test_rev+1)
148 assert [sha] == self.repo.get_changeset(test_rev).children
150 assert [sha] == self.repo.get_changeset(test_rev).children
149
151
150
152
151 class _ChangesetsTestCaseMixin(_BackendTestMixin):
153 class _ChangesetsTestCaseMixin(_BackendTestMixin):
152 recreate_repo_per_test = False
154 recreate_repo_per_test = False
153
155
154 @classmethod
156 @classmethod
155 def _get_commits(cls):
157 def _get_commits(cls):
156 start_date = datetime.datetime(2010, 1, 1, 20)
158 start_date = datetime.datetime(2010, 1, 1, 20)
157 for x in xrange(5):
159 for x in xrange(5):
158 yield {
160 yield {
159 'message': u'Commit %d' % x,
161 'message': u'Commit %d' % x,
160 'author': u'Joe Doe <joe.doe@example.com>',
162 'author': u'Joe Doe <joe.doe@example.com>',
161 'date': start_date + datetime.timedelta(hours=12 * x),
163 'date': start_date + datetime.timedelta(hours=12 * x),
162 'added': [
164 'added': [
163 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
165 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
164 ],
166 ],
165 }
167 }
166
168
167 def test_simple(self):
169 def test_simple(self):
168 tip = self.repo.get_changeset()
170 tip = self.repo.get_changeset()
169 assert tip.date == datetime.datetime(2010, 1, 3, 20)
171 assert tip.date == datetime.datetime(2010, 1, 3, 20)
170
172
171 def test_get_changesets_is_ordered_by_date(self):
173 def test_get_changesets_is_ordered_by_date(self):
172 changesets = list(self.repo.get_changesets())
174 changesets = list(self.repo.get_changesets())
173 ordered_by_date = sorted(changesets,
175 ordered_by_date = sorted(changesets,
174 key=lambda cs: cs.date)
176 key=lambda cs: cs.date)
175
177
176 assert changesets == ordered_by_date
178 assert changesets == ordered_by_date
177
179
178 def test_get_changesets_respects_start(self):
180 def test_get_changesets_respects_start(self):
179 second_id = self.repo.revisions[1]
181 second_id = self.repo.revisions[1]
180 changesets = list(self.repo.get_changesets(start=second_id))
182 changesets = list(self.repo.get_changesets(start=second_id))
181 assert len(changesets) == 4
183 assert len(changesets) == 4
182
184
183 def test_get_changesets_numerical_id_respects_start(self):
185 def test_get_changesets_numerical_id_respects_start(self):
184 second_id = 1
186 second_id = 1
185 changesets = list(self.repo.get_changesets(start=second_id))
187 changesets = list(self.repo.get_changesets(start=second_id))
186 assert len(changesets) == 4
188 assert len(changesets) == 4
187
189
188 def test_get_changesets_includes_start_changeset(self):
190 def test_get_changesets_includes_start_changeset(self):
189 second_id = self.repo.revisions[1]
191 second_id = self.repo.revisions[1]
190 changesets = list(self.repo.get_changesets(start=second_id))
192 changesets = list(self.repo.get_changesets(start=second_id))
191 assert changesets[0].raw_id == second_id
193 assert changesets[0].raw_id == second_id
192
194
193 def test_get_changesets_respects_end(self):
195 def test_get_changesets_respects_end(self):
194 second_id = self.repo.revisions[1]
196 second_id = self.repo.revisions[1]
195 changesets = list(self.repo.get_changesets(end=second_id))
197 changesets = list(self.repo.get_changesets(end=second_id))
196 assert changesets[-1].raw_id == second_id
198 assert changesets[-1].raw_id == second_id
197 assert len(changesets) == 2
199 assert len(changesets) == 2
198
200
199 def test_get_changesets_numerical_id_respects_end(self):
201 def test_get_changesets_numerical_id_respects_end(self):
200 second_id = 1
202 second_id = 1
201 changesets = list(self.repo.get_changesets(end=second_id))
203 changesets = list(self.repo.get_changesets(end=second_id))
202 assert changesets.index(changesets[-1]) == second_id
204 assert changesets.index(changesets[-1]) == second_id
203 assert len(changesets) == 2
205 assert len(changesets) == 2
204
206
205 def test_get_changesets_respects_both_start_and_end(self):
207 def test_get_changesets_respects_both_start_and_end(self):
206 second_id = self.repo.revisions[1]
208 second_id = self.repo.revisions[1]
207 third_id = self.repo.revisions[2]
209 third_id = self.repo.revisions[2]
208 changesets = list(self.repo.get_changesets(start=second_id,
210 changesets = list(self.repo.get_changesets(start=second_id,
209 end=third_id))
211 end=third_id))
210 assert len(changesets) == 2
212 assert len(changesets) == 2
211
213
212 def test_get_changesets_numerical_id_respects_both_start_and_end(self):
214 def test_get_changesets_numerical_id_respects_both_start_and_end(self):
213 changesets = list(self.repo.get_changesets(start=2, end=3))
215 changesets = list(self.repo.get_changesets(start=2, end=3))
214 assert len(changesets) == 2
216 assert len(changesets) == 2
215
217
216 def test_get_changesets_on_empty_repo_raises_EmptyRepository_error(self):
218 def test_get_changesets_on_empty_repo_raises_EmptyRepository_error(self):
217 Backend = self.get_backend()
219 Backend = self.get_backend()
218 repo_path = get_new_dir(str(time.time()))
220 repo_path = get_new_dir(str(time.time()))
219 repo = Backend(repo_path, create=True)
221 repo = Backend(repo_path, create=True)
220
222
221 with pytest.raises(EmptyRepositoryError):
223 with pytest.raises(EmptyRepositoryError):
222 list(repo.get_changesets(start='foobar'))
224 list(repo.get_changesets(start='foobar'))
223
225
224 def test_get_changesets_includes_end_changeset(self):
226 def test_get_changesets_includes_end_changeset(self):
225 second_id = self.repo.revisions[1]
227 second_id = self.repo.revisions[1]
226 changesets = list(self.repo.get_changesets(end=second_id))
228 changesets = list(self.repo.get_changesets(end=second_id))
227 assert changesets[-1].raw_id == second_id
229 assert changesets[-1].raw_id == second_id
228
230
229 def test_get_changesets_respects_start_date(self):
231 def test_get_changesets_respects_start_date(self):
230 start_date = datetime.datetime(2010, 2, 1)
232 start_date = datetime.datetime(2010, 2, 1)
231 for cs in self.repo.get_changesets(start_date=start_date):
233 for cs in self.repo.get_changesets(start_date=start_date):
232 assert cs.date >= start_date
234 assert cs.date >= start_date
233
235
234 def test_get_changesets_respects_end_date(self):
236 def test_get_changesets_respects_end_date(self):
235 start_date = datetime.datetime(2010, 1, 1)
237 start_date = datetime.datetime(2010, 1, 1)
236 end_date = datetime.datetime(2010, 2, 1)
238 end_date = datetime.datetime(2010, 2, 1)
237 for cs in self.repo.get_changesets(start_date=start_date,
239 for cs in self.repo.get_changesets(start_date=start_date,
238 end_date=end_date):
240 end_date=end_date):
239 assert cs.date >= start_date
241 assert cs.date >= start_date
240 assert cs.date <= end_date
242 assert cs.date <= end_date
241
243
242 def test_get_changesets_respects_start_date_and_end_date(self):
244 def test_get_changesets_respects_start_date_and_end_date(self):
243 end_date = datetime.datetime(2010, 2, 1)
245 end_date = datetime.datetime(2010, 2, 1)
244 for cs in self.repo.get_changesets(end_date=end_date):
246 for cs in self.repo.get_changesets(end_date=end_date):
245 assert cs.date <= end_date
247 assert cs.date <= end_date
246
248
247 def test_get_changesets_respects_reverse(self):
249 def test_get_changesets_respects_reverse(self):
248 changesets_id_list = [cs.raw_id for cs in
250 changesets_id_list = [cs.raw_id for cs in
249 self.repo.get_changesets(reverse=True)]
251 self.repo.get_changesets(reverse=True)]
250 assert changesets_id_list == list(reversed(self.repo.revisions))
252 assert changesets_id_list == list(reversed(self.repo.revisions))
251
253
252 def test_get_filenodes_generator(self):
254 def test_get_filenodes_generator(self):
253 tip = self.repo.get_changeset()
255 tip = self.repo.get_changeset()
254 filepaths = [node.path for node in tip.get_filenodes_generator()]
256 filepaths = [node.path for node in tip.get_filenodes_generator()]
255 assert filepaths == ['file_%d.txt' % x for x in xrange(5)]
257 assert filepaths == ['file_%d.txt' % x for x in xrange(5)]
256
258
257 def test_size(self):
259 def test_size(self):
258 tip = self.repo.get_changeset()
260 tip = self.repo.get_changeset()
259 size = 5 * len('Foobar N') # Size of 5 files
261 size = 5 * len('Foobar N') # Size of 5 files
260 assert tip.size == size
262 assert tip.size == size
261
263
262 def test_author(self):
264 def test_author(self):
263 tip = self.repo.get_changeset()
265 tip = self.repo.get_changeset()
264 assert tip.author == u'Joe Doe <joe.doe@example.com>'
266 assert tip.author == u'Joe Doe <joe.doe@example.com>'
265
267
266 def test_author_name(self):
268 def test_author_name(self):
267 tip = self.repo.get_changeset()
269 tip = self.repo.get_changeset()
268 assert tip.author_name == u'Joe Doe'
270 assert tip.author_name == u'Joe Doe'
269
271
270 def test_author_email(self):
272 def test_author_email(self):
271 tip = self.repo.get_changeset()
273 tip = self.repo.get_changeset()
272 assert tip.author_email == u'joe.doe@example.com'
274 assert tip.author_email == u'joe.doe@example.com'
273
275
274 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_start(self):
276 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_start(self):
275 with pytest.raises(ChangesetDoesNotExistError):
277 with pytest.raises(ChangesetDoesNotExistError):
276 list(self.repo.get_changesets(start='foobar'))
278 list(self.repo.get_changesets(start='foobar'))
277
279
278 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_end(self):
280 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_end(self):
279 with pytest.raises(ChangesetDoesNotExistError):
281 with pytest.raises(ChangesetDoesNotExistError):
280 list(self.repo.get_changesets(end='foobar'))
282 list(self.repo.get_changesets(end='foobar'))
281
283
282 def test_get_changesets_raise_branchdoesnotexist_for_wrong_branch_name(self):
284 def test_get_changesets_raise_branchdoesnotexist_for_wrong_branch_name(self):
283 with pytest.raises(BranchDoesNotExistError):
285 with pytest.raises(BranchDoesNotExistError):
284 list(self.repo.get_changesets(branch_name='foobar'))
286 list(self.repo.get_changesets(branch_name='foobar'))
285
287
286 def test_get_changesets_raise_repositoryerror_for_wrong_start_end(self):
288 def test_get_changesets_raise_repositoryerror_for_wrong_start_end(self):
287 start = self.repo.revisions[-1]
289 start = self.repo.revisions[-1]
288 end = self.repo.revisions[0]
290 end = self.repo.revisions[0]
289 with pytest.raises(RepositoryError):
291 with pytest.raises(RepositoryError):
290 list(self.repo.get_changesets(start=start, end=end))
292 list(self.repo.get_changesets(start=start, end=end))
291
293
292 def test_get_changesets_numerical_id_reversed(self):
294 def test_get_changesets_numerical_id_reversed(self):
293 with pytest.raises(RepositoryError):
295 with pytest.raises(RepositoryError):
294 [x for x in self.repo.get_changesets(start=3, end=2)]
296 [x for x in self.repo.get_changesets(start=3, end=2)]
295
297
296 def test_get_changesets_numerical_id_respects_both_start_and_end_last(self):
298 def test_get_changesets_numerical_id_respects_both_start_and_end_last(self):
297 with pytest.raises(RepositoryError):
299 with pytest.raises(RepositoryError):
298 last = len(self.repo.revisions)
300 last = len(self.repo.revisions)
299 list(self.repo.get_changesets(start=last-1, end=last-2))
301 list(self.repo.get_changesets(start=last-1, end=last-2))
300
302
301 def test_get_changesets_numerical_id_last_zero_error(self):
303 def test_get_changesets_numerical_id_last_zero_error(self):
302 with pytest.raises(RepositoryError):
304 with pytest.raises(RepositoryError):
303 last = len(self.repo.revisions)
305 last = len(self.repo.revisions)
304 list(self.repo.get_changesets(start=last-1, end=0))
306 list(self.repo.get_changesets(start=last-1, end=0))
305
307
306
308
307 class _ChangesetsChangesTestCaseMixin(_BackendTestMixin):
309 class _ChangesetsChangesTestCaseMixin(_BackendTestMixin):
308 recreate_repo_per_test = False
310 recreate_repo_per_test = False
309
311
310 @classmethod
312 @classmethod
311 def _get_commits(cls):
313 def _get_commits(cls):
312 return [
314 return [
313 {
315 {
314 'message': u'Initial',
316 'message': u'Initial',
315 'author': u'Joe Doe <joe.doe@example.com>',
317 'author': u'Joe Doe <joe.doe@example.com>',
316 'date': datetime.datetime(2010, 1, 1, 20),
318 'date': datetime.datetime(2010, 1, 1, 20),
317 'added': [
319 'added': [
318 FileNode('foo/bar', content='foo'),
320 FileNode('foo/bar', content='foo'),
319 FileNode('foo/baΕ‚', content='foo'),
321 FileNode('foo/baΕ‚', content='foo'),
320 FileNode('foobar', content='foo'),
322 FileNode('foobar', content='foo'),
321 FileNode('qwe', content='foo'),
323 FileNode('qwe', content='foo'),
322 ],
324 ],
323 },
325 },
324 {
326 {
325 'message': u'Massive changes',
327 'message': u'Massive changes',
326 'author': u'Joe Doe <joe.doe@example.com>',
328 'author': u'Joe Doe <joe.doe@example.com>',
327 'date': datetime.datetime(2010, 1, 1, 22),
329 'date': datetime.datetime(2010, 1, 1, 22),
328 'added': [FileNode('fallout', content='War never changes')],
330 'added': [FileNode('fallout', content='War never changes')],
329 'changed': [
331 'changed': [
330 FileNode('foo/bar', content='baz'),
332 FileNode('foo/bar', content='baz'),
331 FileNode('foobar', content='baz'),
333 FileNode('foobar', content='baz'),
332 ],
334 ],
333 'removed': [FileNode('qwe')],
335 'removed': [FileNode('qwe')],
334 },
336 },
335 ]
337 ]
336
338
337 def test_initial_commit(self):
339 def test_initial_commit(self):
338 changeset = self.repo.get_changeset(0)
340 changeset = self.repo.get_changeset(0)
339 assert sorted(list(changeset.added)) == sorted([
341 assert sorted(list(changeset.added)) == sorted([
340 changeset.get_node('foo/bar'),
342 changeset.get_node('foo/bar'),
341 changeset.get_node('foo/baΕ‚'),
343 changeset.get_node('foo/baΕ‚'),
342 changeset.get_node('foobar'),
344 changeset.get_node('foobar'),
343 changeset.get_node('qwe'),
345 changeset.get_node('qwe'),
344 ])
346 ])
345 assert list(changeset.changed) == []
347 assert list(changeset.changed) == []
346 assert list(changeset.removed) == []
348 assert list(changeset.removed) == []
347 assert u'foo/ba\u0142' in changeset.as_dict()['added']
349 assert u'foo/ba\u0142' in changeset.as_dict()['added']
348 assert u'foo/ba\u0142' in changeset.__json__(with_file_list=True)['added']
350 assert u'foo/ba\u0142' in changeset.__json__(with_file_list=True)['added']
349
351
350 def test_head_added(self):
352 def test_head_added(self):
351 changeset = self.repo.get_changeset()
353 changeset = self.repo.get_changeset()
352 assert isinstance(changeset.added, AddedFileNodesGenerator)
354 assert isinstance(changeset.added, AddedFileNodesGenerator)
353 assert list(changeset.added) == [
355 assert list(changeset.added) == [
354 changeset.get_node('fallout'),
356 changeset.get_node('fallout'),
355 ]
357 ]
356 assert isinstance(changeset.changed, ChangedFileNodesGenerator)
358 assert isinstance(changeset.changed, ChangedFileNodesGenerator)
357 assert list(changeset.changed) == [
359 assert list(changeset.changed) == [
358 changeset.get_node('foo/bar'),
360 changeset.get_node('foo/bar'),
359 changeset.get_node('foobar'),
361 changeset.get_node('foobar'),
360 ]
362 ]
361 assert isinstance(changeset.removed, RemovedFileNodesGenerator)
363 assert isinstance(changeset.removed, RemovedFileNodesGenerator)
362 assert len(changeset.removed) == 1
364 assert len(changeset.removed) == 1
363 assert list(changeset.removed)[0].path == 'qwe'
365 assert list(changeset.removed)[0].path == 'qwe'
364
366
365 def test_get_filemode(self):
367 def test_get_filemode(self):
366 changeset = self.repo.get_changeset()
368 changeset = self.repo.get_changeset()
367 assert 33188 == changeset.get_file_mode('foo/bar')
369 assert 33188 == changeset.get_file_mode('foo/bar')
368
370
369 def test_get_filemode_non_ascii(self):
371 def test_get_filemode_non_ascii(self):
370 changeset = self.repo.get_changeset()
372 changeset = self.repo.get_changeset()
371 assert 33188 == changeset.get_file_mode('foo/baΕ‚')
373 assert 33188 == changeset.get_file_mode('foo/baΕ‚')
372 assert 33188 == changeset.get_file_mode(u'foo/baΕ‚')
374 assert 33188 == changeset.get_file_mode(u'foo/baΕ‚')
373
375
374
376
375 # For each backend create test case class
377 # For each backend create test case class
376 for alias in SCM_TESTS:
378 for alias in SCM_TESTS:
377 attrs = {
379 attrs = {
378 'backend_alias': alias,
380 'backend_alias': alias,
379 }
381 }
380 # tests with additional commits
382 # tests with additional commits
381 cls_name = 'Test' + alias.title() + 'ChangesetsWithCommits'
383 cls_name = 'Test' + alias.title() + 'ChangesetsWithCommits'
382 globals()[cls_name] = type(cls_name, (_ChangesetsWithCommitsTestCaseixin,), attrs)
384 globals()[cls_name] = type(cls_name, (_ChangesetsWithCommitsTestCaseixin,), attrs)
383
385
384 # tests without additional commits
386 # tests without additional commits
385 cls_name = 'Test' + alias.title() + 'Changesets'
387 cls_name = 'Test' + alias.title() + 'Changesets'
386 globals()[cls_name] = type(cls_name, (_ChangesetsTestCaseMixin,), attrs)
388 globals()[cls_name] = type(cls_name, (_ChangesetsTestCaseMixin,), attrs)
387
389
388 # tests changes
390 # tests changes
389 cls_name = 'Test' + alias.title() + 'ChangesetsChanges'
391 cls_name = 'Test' + alias.title() + 'ChangesetsChanges'
390 globals()[cls_name] = type(cls_name, (_ChangesetsChangesTestCaseMixin,), attrs)
392 globals()[cls_name] = type(cls_name, (_ChangesetsChangesTestCaseMixin,), attrs)
@@ -1,866 +1,869 b''
1 import os
1 import os
2 import sys
2 import sys
3 import mock
3 import mock
4 import datetime
4 import datetime
5 import urllib2
5 import urllib2
6
6
7 import pytest
7 import pytest
8
8
9 from kallithea.lib.vcs.backends.git import GitRepository, GitChangeset
9 from kallithea.lib.vcs.backends.git import GitRepository, GitChangeset
10 from kallithea.lib.vcs.exceptions import RepositoryError, VCSError, NodeDoesNotExistError
10 from kallithea.lib.vcs.exceptions import RepositoryError, VCSError, NodeDoesNotExistError
11 from kallithea.lib.vcs.nodes import NodeKind, FileNode, DirNode, NodeState
11 from kallithea.lib.vcs.nodes import NodeKind, FileNode, DirNode, NodeState
12 from kallithea.model.scm import ScmModel
12 from kallithea.model.scm import ScmModel
13
13
14 from kallithea.tests.vcs.base import _BackendTestMixin
14 from kallithea.tests.vcs.base import _BackendTestMixin
15 from kallithea.tests.vcs.conf import TEST_GIT_REPO, TEST_GIT_REPO_CLONE, TESTS_TMP_PATH, get_new_dir
15 from kallithea.tests.vcs.conf import TEST_GIT_REPO, TEST_GIT_REPO_CLONE, TESTS_TMP_PATH, get_new_dir
16
16
17
17
18 class TestGitRepository(object):
18 class TestGitRepository(object):
19
19
20 def __check_for_existing_repo(self):
20 def __check_for_existing_repo(self):
21 if os.path.exists(TEST_GIT_REPO_CLONE):
21 if os.path.exists(TEST_GIT_REPO_CLONE):
22 pytest.fail('Cannot test git clone repo as location %s already '
22 pytest.fail('Cannot test git clone repo as location %s already '
23 'exists. You should manually remove it first.'
23 'exists. You should manually remove it first.'
24 % TEST_GIT_REPO_CLONE)
24 % TEST_GIT_REPO_CLONE)
25
25
26 def setup_method(self):
26 def setup_method(self):
27 self.repo = GitRepository(TEST_GIT_REPO)
27 self.repo = GitRepository(TEST_GIT_REPO)
28
28
29 def test_wrong_repo_path(self):
29 def test_wrong_repo_path(self):
30 wrong_repo_path = os.path.join(TESTS_TMP_PATH, 'errorrepo')
30 wrong_repo_path = os.path.join(TESTS_TMP_PATH, 'errorrepo')
31 with pytest.raises(RepositoryError):
31 with pytest.raises(RepositoryError):
32 GitRepository(wrong_repo_path)
32 GitRepository(wrong_repo_path)
33
33
34 def test_git_cmd_injection(self):
34 def test_git_cmd_injection(self):
35 repo_inject_path = TEST_GIT_REPO + '; echo "Cake";'
35 repo_inject_path = TEST_GIT_REPO + '; echo "Cake";'
36 with pytest.raises(urllib2.URLError):
36 with pytest.raises(urllib2.URLError):
37 # Should fail because URL will contain the parts after ; too
37 # Should fail because URL will contain the parts after ; too
38 GitRepository(get_new_dir('injection-repo'), src_url=repo_inject_path, update_after_clone=True, create=True)
38 GitRepository(get_new_dir('injection-repo'), src_url=repo_inject_path, update_after_clone=True, create=True)
39
39
40 with pytest.raises(RepositoryError):
40 with pytest.raises(RepositoryError):
41 # Should fail on direct clone call, which as of this writing does not happen outside of class
41 # Should fail on direct clone call, which as of this writing does not happen outside of class
42 clone_fail_repo = GitRepository(get_new_dir('injection-repo'), create=True)
42 clone_fail_repo = GitRepository(get_new_dir('injection-repo'), create=True)
43 clone_fail_repo.clone(repo_inject_path, update_after_clone=True,)
43 clone_fail_repo.clone(repo_inject_path, update_after_clone=True,)
44
44
45 # Verify correct quoting of evil characters that should work on posix file systems
45 # Verify correct quoting of evil characters that should work on posix file systems
46 if sys.platform == 'win32':
46 if sys.platform == 'win32':
47 # windows does not allow '"' in dir names
47 # windows does not allow '"' in dir names
48 # and some versions of the git client don't like ` and '
48 # and some versions of the git client don't like ` and '
49 tricky_path = get_new_dir("tricky-path-repo-$")
49 tricky_path = get_new_dir("tricky-path-repo-$")
50 else:
50 else:
51 tricky_path = get_new_dir("tricky-path-repo-$'\"`")
51 tricky_path = get_new_dir("tricky-path-repo-$'\"`")
52 successfully_cloned = GitRepository(tricky_path, src_url=TEST_GIT_REPO, update_after_clone=True, create=True)
52 successfully_cloned = GitRepository(tricky_path, src_url=TEST_GIT_REPO, update_after_clone=True, create=True)
53 # Repo should have been created
53 # Repo should have been created
54 assert not successfully_cloned._repo.bare
54 assert not successfully_cloned._repo.bare
55
55
56 if sys.platform == 'win32':
56 if sys.platform == 'win32':
57 # windows does not allow '"' in dir names
57 # windows does not allow '"' in dir names
58 # and some versions of the git client don't like ` and '
58 # and some versions of the git client don't like ` and '
59 tricky_path_2 = get_new_dir("tricky-path-2-repo-$")
59 tricky_path_2 = get_new_dir("tricky-path-2-repo-$")
60 else:
60 else:
61 tricky_path_2 = get_new_dir("tricky-path-2-repo-$'\"`")
61 tricky_path_2 = get_new_dir("tricky-path-2-repo-$'\"`")
62 successfully_cloned2 = GitRepository(tricky_path_2, src_url=tricky_path, bare=True, create=True)
62 successfully_cloned2 = GitRepository(tricky_path_2, src_url=tricky_path, bare=True, create=True)
63 # Repo should have been created and thus used correct quoting for clone
63 # Repo should have been created and thus used correct quoting for clone
64 assert successfully_cloned2._repo.bare
64 assert successfully_cloned2._repo.bare
65
65
66 # Should pass because URL has been properly quoted
66 # Should pass because URL has been properly quoted
67 successfully_cloned.pull(tricky_path_2)
67 successfully_cloned.pull(tricky_path_2)
68 successfully_cloned2.fetch(tricky_path)
68 successfully_cloned2.fetch(tricky_path)
69
69
70 def test_repo_create_with_spaces_in_path(self):
70 def test_repo_create_with_spaces_in_path(self):
71 repo_path = get_new_dir("path with spaces")
71 repo_path = get_new_dir("path with spaces")
72 repo = GitRepository(repo_path, src_url=None, bare=True, create=True)
72 repo = GitRepository(repo_path, src_url=None, bare=True, create=True)
73 # Repo should have been created
73 # Repo should have been created
74 assert repo._repo.bare
74 assert repo._repo.bare
75
75
76 def test_repo_clone(self):
76 def test_repo_clone(self):
77 self.__check_for_existing_repo()
77 self.__check_for_existing_repo()
78 repo = GitRepository(TEST_GIT_REPO)
78 repo = GitRepository(TEST_GIT_REPO)
79 repo_clone = GitRepository(TEST_GIT_REPO_CLONE,
79 repo_clone = GitRepository(TEST_GIT_REPO_CLONE,
80 src_url=TEST_GIT_REPO, create=True, update_after_clone=True)
80 src_url=TEST_GIT_REPO, create=True, update_after_clone=True)
81 assert len(repo.revisions) == len(repo_clone.revisions)
81 assert len(repo.revisions) == len(repo_clone.revisions)
82 # Checking hashes of changesets should be enough
82 # Checking hashes of changesets should be enough
83 for changeset in repo.get_changesets():
83 for changeset in repo.get_changesets():
84 raw_id = changeset.raw_id
84 raw_id = changeset.raw_id
85 assert raw_id == repo_clone.get_changeset(raw_id).raw_id
85 assert raw_id == repo_clone.get_changeset(raw_id).raw_id
86
86
87 def test_repo_clone_with_spaces_in_path(self):
87 def test_repo_clone_with_spaces_in_path(self):
88 repo_path = get_new_dir("path with spaces")
88 repo_path = get_new_dir("path with spaces")
89 successfully_cloned = GitRepository(repo_path, src_url=TEST_GIT_REPO, update_after_clone=True, create=True)
89 successfully_cloned = GitRepository(repo_path, src_url=TEST_GIT_REPO, update_after_clone=True, create=True)
90 # Repo should have been created
90 # Repo should have been created
91 assert not successfully_cloned._repo.bare
91 assert not successfully_cloned._repo.bare
92
92
93 successfully_cloned.pull(TEST_GIT_REPO)
93 successfully_cloned.pull(TEST_GIT_REPO)
94 self.repo.fetch(repo_path)
94 self.repo.fetch(repo_path)
95
95
96 def test_repo_clone_without_create(self):
96 def test_repo_clone_without_create(self):
97 with pytest.raises(RepositoryError):
97 with pytest.raises(RepositoryError):
98 GitRepository(TEST_GIT_REPO_CLONE + '_wo_create', src_url=TEST_GIT_REPO)
98 GitRepository(TEST_GIT_REPO_CLONE + '_wo_create', src_url=TEST_GIT_REPO)
99
99
100 def test_repo_clone_with_update(self):
100 def test_repo_clone_with_update(self):
101 repo = GitRepository(TEST_GIT_REPO)
101 repo = GitRepository(TEST_GIT_REPO)
102 clone_path = TEST_GIT_REPO_CLONE + '_with_update'
102 clone_path = TEST_GIT_REPO_CLONE + '_with_update'
103 repo_clone = GitRepository(clone_path,
103 repo_clone = GitRepository(clone_path,
104 create=True, src_url=TEST_GIT_REPO, update_after_clone=True)
104 create=True, src_url=TEST_GIT_REPO, update_after_clone=True)
105 assert len(repo.revisions) == len(repo_clone.revisions)
105 assert len(repo.revisions) == len(repo_clone.revisions)
106
106
107 # check if current workdir was updated
107 # check if current workdir was updated
108 fpath = os.path.join(clone_path, 'MANIFEST.in')
108 fpath = os.path.join(clone_path, 'MANIFEST.in')
109 assert os.path.isfile(fpath) == True, 'Repo was cloned and updated but file %s could not be found' % fpath
109 assert os.path.isfile(fpath) == True, 'Repo was cloned and updated but file %s could not be found' % fpath
110
110
111 def test_repo_clone_without_update(self):
111 def test_repo_clone_without_update(self):
112 repo = GitRepository(TEST_GIT_REPO)
112 repo = GitRepository(TEST_GIT_REPO)
113 clone_path = TEST_GIT_REPO_CLONE + '_without_update'
113 clone_path = TEST_GIT_REPO_CLONE + '_without_update'
114 repo_clone = GitRepository(clone_path,
114 repo_clone = GitRepository(clone_path,
115 create=True, src_url=TEST_GIT_REPO, update_after_clone=False)
115 create=True, src_url=TEST_GIT_REPO, update_after_clone=False)
116 assert len(repo.revisions) == len(repo_clone.revisions)
116 assert len(repo.revisions) == len(repo_clone.revisions)
117 # check if current workdir was *NOT* updated
117 # check if current workdir was *NOT* updated
118 fpath = os.path.join(clone_path, 'MANIFEST.in')
118 fpath = os.path.join(clone_path, 'MANIFEST.in')
119 # Make sure it's not bare repo
119 # Make sure it's not bare repo
120 assert not repo_clone._repo.bare
120 assert not repo_clone._repo.bare
121 assert os.path.isfile(fpath) == False, 'Repo was cloned and updated but file %s was found' % fpath
121 assert os.path.isfile(fpath) == False, 'Repo was cloned and updated but file %s was found' % fpath
122
122
123 def test_repo_clone_into_bare_repo(self):
123 def test_repo_clone_into_bare_repo(self):
124 repo = GitRepository(TEST_GIT_REPO)
124 repo = GitRepository(TEST_GIT_REPO)
125 clone_path = TEST_GIT_REPO_CLONE + '_bare.git'
125 clone_path = TEST_GIT_REPO_CLONE + '_bare.git'
126 repo_clone = GitRepository(clone_path, create=True,
126 repo_clone = GitRepository(clone_path, create=True,
127 src_url=repo.path, bare=True)
127 src_url=repo.path, bare=True)
128 assert repo_clone._repo.bare
128 assert repo_clone._repo.bare
129
129
130 def test_create_repo_is_not_bare_by_default(self):
130 def test_create_repo_is_not_bare_by_default(self):
131 repo = GitRepository(get_new_dir('not-bare-by-default'), create=True)
131 repo = GitRepository(get_new_dir('not-bare-by-default'), create=True)
132 assert not repo._repo.bare
132 assert not repo._repo.bare
133
133
134 def test_create_bare_repo(self):
134 def test_create_bare_repo(self):
135 repo = GitRepository(get_new_dir('bare-repo'), create=True, bare=True)
135 repo = GitRepository(get_new_dir('bare-repo'), create=True, bare=True)
136 assert repo._repo.bare
136 assert repo._repo.bare
137
137
138 def test_revisions(self):
138 def test_revisions(self):
139 # there are 112 revisions (by now)
139 # there are 112 revisions (by now)
140 # so we can assume they would be available from now on
140 # so we can assume they would be available from now on
141 subset = set([
141 subset = set([
142 'c1214f7e79e02fc37156ff215cd71275450cffc3',
142 'c1214f7e79e02fc37156ff215cd71275450cffc3',
143 '38b5fe81f109cb111f549bfe9bb6b267e10bc557',
143 '38b5fe81f109cb111f549bfe9bb6b267e10bc557',
144 'fa6600f6848800641328adbf7811fd2372c02ab2',
144 'fa6600f6848800641328adbf7811fd2372c02ab2',
145 '102607b09cdd60e2793929c4f90478be29f85a17',
145 '102607b09cdd60e2793929c4f90478be29f85a17',
146 '49d3fd156b6f7db46313fac355dca1a0b94a0017',
146 '49d3fd156b6f7db46313fac355dca1a0b94a0017',
147 '2d1028c054665b962fa3d307adfc923ddd528038',
147 '2d1028c054665b962fa3d307adfc923ddd528038',
148 'd7e0d30fbcae12c90680eb095a4f5f02505ce501',
148 'd7e0d30fbcae12c90680eb095a4f5f02505ce501',
149 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
149 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
150 'dd80b0f6cf5052f17cc738c2951c4f2070200d7f',
150 'dd80b0f6cf5052f17cc738c2951c4f2070200d7f',
151 '8430a588b43b5d6da365400117c89400326e7992',
151 '8430a588b43b5d6da365400117c89400326e7992',
152 'd955cd312c17b02143c04fa1099a352b04368118',
152 'd955cd312c17b02143c04fa1099a352b04368118',
153 'f67b87e5c629c2ee0ba58f85197e423ff28d735b',
153 'f67b87e5c629c2ee0ba58f85197e423ff28d735b',
154 'add63e382e4aabc9e1afdc4bdc24506c269b7618',
154 'add63e382e4aabc9e1afdc4bdc24506c269b7618',
155 'f298fe1189f1b69779a4423f40b48edf92a703fc',
155 'f298fe1189f1b69779a4423f40b48edf92a703fc',
156 'bd9b619eb41994cac43d67cf4ccc8399c1125808',
156 'bd9b619eb41994cac43d67cf4ccc8399c1125808',
157 '6e125e7c890379446e98980d8ed60fba87d0f6d1',
157 '6e125e7c890379446e98980d8ed60fba87d0f6d1',
158 'd4a54db9f745dfeba6933bf5b1e79e15d0af20bd',
158 'd4a54db9f745dfeba6933bf5b1e79e15d0af20bd',
159 '0b05e4ed56c802098dfc813cbe779b2f49e92500',
159 '0b05e4ed56c802098dfc813cbe779b2f49e92500',
160 '191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e',
160 '191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e',
161 '45223f8f114c64bf4d6f853e3c35a369a6305520',
161 '45223f8f114c64bf4d6f853e3c35a369a6305520',
162 'ca1eb7957a54bce53b12d1a51b13452f95bc7c7e',
162 'ca1eb7957a54bce53b12d1a51b13452f95bc7c7e',
163 'f5ea29fc42ef67a2a5a7aecff10e1566699acd68',
163 'f5ea29fc42ef67a2a5a7aecff10e1566699acd68',
164 '27d48942240f5b91dfda77accd2caac94708cc7d',
164 '27d48942240f5b91dfda77accd2caac94708cc7d',
165 '622f0eb0bafd619d2560c26f80f09e3b0b0d78af',
165 '622f0eb0bafd619d2560c26f80f09e3b0b0d78af',
166 'e686b958768ee96af8029fe19c6050b1a8dd3b2b'])
166 'e686b958768ee96af8029fe19c6050b1a8dd3b2b'])
167 assert subset.issubset(set(self.repo.revisions))
167 assert subset.issubset(set(self.repo.revisions))
168
168
169 def test_slicing(self):
169 def test_slicing(self):
170 # 4 1 5 10 95
170 # 4 1 5 10 95
171 for sfrom, sto, size in [(0, 4, 4), (1, 2, 1), (10, 15, 5),
171 for sfrom, sto, size in [(0, 4, 4), (1, 2, 1), (10, 15, 5),
172 (10, 20, 10), (5, 100, 95)]:
172 (10, 20, 10), (5, 100, 95)]:
173 revs = list(self.repo[sfrom:sto])
173 revs = list(self.repo[sfrom:sto])
174 assert len(revs) == size
174 assert len(revs) == size
175 assert revs[0] == self.repo.get_changeset(sfrom)
175 assert revs[0] == self.repo.get_changeset(sfrom)
176 assert revs[-1] == self.repo.get_changeset(sto - 1)
176 assert revs[-1] == self.repo.get_changeset(sto - 1)
177
177
178 def test_branches(self):
178 def test_branches(self):
179 # TODO: Need more tests here
179 # TODO: Need more tests here
180 # Removed (those are 'remotes' branches for cloned repo)
180 # Removed (those are 'remotes' branches for cloned repo)
181 #assert 'master' in self.repo.branches
181 #assert 'master' in self.repo.branches
182 #assert 'gittree' in self.repo.branches
182 #assert 'gittree' in self.repo.branches
183 #assert 'web-branch' in self.repo.branches
183 #assert 'web-branch' in self.repo.branches
184 for name, id in self.repo.branches.items():
184 for name, id in self.repo.branches.items():
185 assert isinstance(self.repo.get_changeset(id), GitChangeset)
185 assert isinstance(self.repo.get_changeset(id), GitChangeset)
186
186
187 def test_tags(self):
187 def test_tags(self):
188 # TODO: Need more tests here
188 # TODO: Need more tests here
189 assert 'v0.1.1' in self.repo.tags
189 assert 'v0.1.1' in self.repo.tags
190 assert 'v0.1.2' in self.repo.tags
190 assert 'v0.1.2' in self.repo.tags
191 for name, id in self.repo.tags.items():
191 for name, id in self.repo.tags.items():
192 assert isinstance(self.repo.get_changeset(id), GitChangeset)
192 assert isinstance(self.repo.get_changeset(id), GitChangeset)
193
193
194 def _test_single_changeset_cache(self, revision):
194 def _test_single_changeset_cache(self, revision):
195 chset = self.repo.get_changeset(revision)
195 chset = self.repo.get_changeset(revision)
196 assert revision in self.repo.changesets
196 assert revision in self.repo.changesets
197 assert chset is self.repo.changesets[revision]
197 assert chset is self.repo.changesets[revision]
198
198
199 def test_initial_changeset(self):
199 def test_initial_changeset(self):
200 id = self.repo.revisions[0]
200 id = self.repo.revisions[0]
201 init_chset = self.repo.get_changeset(id)
201 init_chset = self.repo.get_changeset(id)
202 assert init_chset.message == 'initial import\n'
202 assert init_chset.message == 'initial import\n'
203 assert init_chset.author == 'Marcin Kuzminski <marcin@python-blog.com>'
203 assert init_chset.author == 'Marcin Kuzminski <marcin@python-blog.com>'
204 for path in ('vcs/__init__.py',
204 for path in ('vcs/__init__.py',
205 'vcs/backends/BaseRepository.py',
205 'vcs/backends/BaseRepository.py',
206 'vcs/backends/__init__.py'):
206 'vcs/backends/__init__.py'):
207 assert isinstance(init_chset.get_node(path), FileNode)
207 assert isinstance(init_chset.get_node(path), FileNode)
208 for path in ('', 'vcs', 'vcs/backends'):
208 for path in ('', 'vcs', 'vcs/backends'):
209 assert isinstance(init_chset.get_node(path), DirNode)
209 assert isinstance(init_chset.get_node(path), DirNode)
210
210
211 with pytest.raises(NodeDoesNotExistError):
211 with pytest.raises(NodeDoesNotExistError):
212 init_chset.get_node(path='foobar')
212 init_chset.get_node(path='foobar')
213
213
214 node = init_chset.get_node('vcs/')
214 node = init_chset.get_node('vcs/')
215 assert hasattr(node, 'kind')
215 assert hasattr(node, 'kind')
216 assert node.kind == NodeKind.DIR
216 assert node.kind == NodeKind.DIR
217
217
218 node = init_chset.get_node('vcs')
218 node = init_chset.get_node('vcs')
219 assert hasattr(node, 'kind')
219 assert hasattr(node, 'kind')
220 assert node.kind == NodeKind.DIR
220 assert node.kind == NodeKind.DIR
221
221
222 node = init_chset.get_node('vcs/__init__.py')
222 node = init_chset.get_node('vcs/__init__.py')
223 assert hasattr(node, 'kind')
223 assert hasattr(node, 'kind')
224 assert node.kind == NodeKind.FILE
224 assert node.kind == NodeKind.FILE
225
225
226 def test_not_existing_changeset(self):
226 def test_not_existing_changeset(self):
227 with pytest.raises(RepositoryError):
227 with pytest.raises(RepositoryError):
228 self.repo.get_changeset('f' * 40)
228 self.repo.get_changeset('f' * 40)
229
229
230 def test_changeset10(self):
230 def test_changeset10(self):
231
231
232 chset10 = self.repo.get_changeset(self.repo.revisions[9])
232 chset10 = self.repo.get_changeset(self.repo.revisions[9])
233 readme = """===
233 readme = """===
234 VCS
234 VCS
235 ===
235 ===
236
236
237 Various Version Control System management abstraction layer for Python.
237 Various Version Control System management abstraction layer for Python.
238
238
239 Introduction
239 Introduction
240 ------------
240 ------------
241
241
242 TODO: To be written...
242 TODO: To be written...
243
243
244 """
244 """
245 node = chset10.get_node('README.rst')
245 node = chset10.get_node('README.rst')
246 assert node.kind == NodeKind.FILE
246 assert node.kind == NodeKind.FILE
247 assert node.content == readme
247 assert node.content == readme
248
248
249
249
250 class TestGitChangeset(object):
250 class TestGitChangeset(object):
251
251
252 def setup_method(self):
252 def setup_method(self):
253 self.repo = GitRepository(TEST_GIT_REPO)
253 self.repo = GitRepository(TEST_GIT_REPO)
254
254
255 def test_default_changeset(self):
255 def test_default_changeset(self):
256 tip = self.repo.get_changeset()
256 tip = self.repo.get_changeset()
257 assert tip == self.repo.get_changeset(None)
257 assert tip == self.repo.get_changeset(None)
258 assert tip == self.repo.get_changeset('tip')
258 assert tip == self.repo.get_changeset('tip')
259
259
260 def test_root_node(self):
260 def test_root_node(self):
261 tip = self.repo.get_changeset()
261 tip = self.repo.get_changeset()
262 assert tip.root is tip.get_node('')
262 assert tip.root is tip.get_node('')
263
263
264 def test_lazy_fetch(self):
264 def test_lazy_fetch(self):
265 """
265 """
266 Test if changeset's nodes expands and are cached as we walk through
266 Test if changeset's nodes expands and are cached as we walk through
267 the revision. This test is somewhat hard to write as order of tests
267 the revision. This test is somewhat hard to write as order of tests
268 is a key here. Written by running command after command in a shell.
268 is a key here. Written by running command after command in a shell.
269 """
269 """
270 commit_id = '2a13f185e4525f9d4b59882791a2d397b90d5ddc'
270 commit_id = '2a13f185e4525f9d4b59882791a2d397b90d5ddc'
271 assert commit_id in self.repo.revisions
271 assert commit_id in self.repo.revisions
272 chset = self.repo.get_changeset(commit_id)
272 chset = self.repo.get_changeset(commit_id)
273 assert len(chset.nodes) == 0
273 assert len(chset.nodes) == 0
274 root = chset.root
274 root = chset.root
275 assert len(chset.nodes) == 1
275 assert len(chset.nodes) == 1
276 assert len(root.nodes) == 8
276 assert len(root.nodes) == 8
277 # accessing root.nodes updates chset.nodes
277 # accessing root.nodes updates chset.nodes
278 assert len(chset.nodes) == 9
278 assert len(chset.nodes) == 9
279
279
280 docs = root.get_node('docs')
280 docs = root.get_node('docs')
281 # we haven't yet accessed anything new as docs dir was already cached
281 # we haven't yet accessed anything new as docs dir was already cached
282 assert len(chset.nodes) == 9
282 assert len(chset.nodes) == 9
283 assert len(docs.nodes) == 8
283 assert len(docs.nodes) == 8
284 # accessing docs.nodes updates chset.nodes
284 # accessing docs.nodes updates chset.nodes
285 assert len(chset.nodes) == 17
285 assert len(chset.nodes) == 17
286
286
287 assert docs is chset.get_node('docs')
287 assert docs is chset.get_node('docs')
288 assert docs is root.nodes[0]
288 assert docs is root.nodes[0]
289 assert docs is root.dirs[0]
289 assert docs is root.dirs[0]
290 assert docs is chset.get_node('docs')
290 assert docs is chset.get_node('docs')
291
291
292 def test_nodes_with_changeset(self):
292 def test_nodes_with_changeset(self):
293 commit_id = '2a13f185e4525f9d4b59882791a2d397b90d5ddc'
293 commit_id = '2a13f185e4525f9d4b59882791a2d397b90d5ddc'
294 chset = self.repo.get_changeset(commit_id)
294 chset = self.repo.get_changeset(commit_id)
295 root = chset.root
295 root = chset.root
296 docs = root.get_node('docs')
296 docs = root.get_node('docs')
297 assert docs is chset.get_node('docs')
297 assert docs is chset.get_node('docs')
298 api = docs.get_node('api')
298 api = docs.get_node('api')
299 assert api is chset.get_node('docs/api')
299 assert api is chset.get_node('docs/api')
300 index = api.get_node('index.rst')
300 index = api.get_node('index.rst')
301 assert index is chset.get_node('docs/api/index.rst')
301 assert index is chset.get_node('docs/api/index.rst')
302 assert index is chset.get_node('docs') \
302 assert index is chset.get_node('docs') \
303 .get_node('api') \
303 .get_node('api') \
304 .get_node('index.rst')
304 .get_node('index.rst')
305
305
306 def test_branch_and_tags(self):
306 def test_branch_and_tags(self):
307 # Those tests seem to show wrong results:
307 # Those tests seem to show wrong results:
308 # in Git, only heads have a branch - most changesets don't
308 # in Git, only heads have a branch - most changesets don't
309 rev0 = self.repo.revisions[0]
309 rev0 = self.repo.revisions[0]
310 chset0 = self.repo.get_changeset(rev0)
310 chset0 = self.repo.get_changeset(rev0)
311 assert chset0.branch is None # should be 'master'?
311 assert chset0.branch is None # should be 'master'?
312 assert chset0.branches == [] # should be 'master'?
312 assert chset0.tags == []
313 assert chset0.tags == []
313
314
314 rev10 = self.repo.revisions[10]
315 rev10 = self.repo.revisions[10]
315 chset10 = self.repo.get_changeset(rev10)
316 chset10 = self.repo.get_changeset(rev10)
316 assert chset10.branch is None # should be 'master'?
317 assert chset10.branch is None # should be 'master'?
318 assert chset10.branches == [] # should be 'master'?
317 assert chset10.tags == []
319 assert chset10.tags == []
318
320
319 rev44 = self.repo.revisions[44]
321 rev44 = self.repo.revisions[44]
320 chset44 = self.repo.get_changeset(rev44)
322 chset44 = self.repo.get_changeset(rev44)
321 assert chset44.branch is None # should be 'web-branch'?
323 assert chset44.branch is None # should be 'web-branch'?
324 assert chset44.branches == [] # should be 'web-branch'?
322
325
323 tip = self.repo.get_changeset('tip')
326 tip = self.repo.get_changeset('tip')
324 assert 'tip' not in tip.tags # it should be?
327 assert 'tip' not in tip.tags # it should be?
325 assert not tip.tags # how it is!
328 assert not tip.tags # how it is!
326
329
327 def _test_slices(self, limit, offset):
330 def _test_slices(self, limit, offset):
328 count = self.repo.count()
331 count = self.repo.count()
329 changesets = self.repo.get_changesets(limit=limit, offset=offset)
332 changesets = self.repo.get_changesets(limit=limit, offset=offset)
330 idx = 0
333 idx = 0
331 for changeset in changesets:
334 for changeset in changesets:
332 rev = offset + idx
335 rev = offset + idx
333 idx += 1
336 idx += 1
334 rev_id = self.repo.revisions[rev]
337 rev_id = self.repo.revisions[rev]
335 if idx > limit:
338 if idx > limit:
336 pytest.fail("Exceeded limit already (getting revision %s, "
339 pytest.fail("Exceeded limit already (getting revision %s, "
337 "there are %s total revisions, offset=%s, limit=%s)"
340 "there are %s total revisions, offset=%s, limit=%s)"
338 % (rev_id, count, offset, limit))
341 % (rev_id, count, offset, limit))
339 assert changeset == self.repo.get_changeset(rev_id)
342 assert changeset == self.repo.get_changeset(rev_id)
340 result = list(self.repo.get_changesets(limit=limit, offset=offset))
343 result = list(self.repo.get_changesets(limit=limit, offset=offset))
341 start = offset
344 start = offset
342 end = limit and offset + limit or None
345 end = limit and offset + limit or None
343 sliced = list(self.repo[start:end])
346 sliced = list(self.repo[start:end])
344 pytest.failUnlessEqual(result, sliced,
347 pytest.failUnlessEqual(result, sliced,
345 msg="Comparison failed for limit=%s, offset=%s"
348 msg="Comparison failed for limit=%s, offset=%s"
346 "(get_changeset returned: %s and sliced: %s"
349 "(get_changeset returned: %s and sliced: %s"
347 % (limit, offset, result, sliced))
350 % (limit, offset, result, sliced))
348
351
349 def _test_file_size(self, revision, path, size):
352 def _test_file_size(self, revision, path, size):
350 node = self.repo.get_changeset(revision).get_node(path)
353 node = self.repo.get_changeset(revision).get_node(path)
351 assert node.is_file()
354 assert node.is_file()
352 assert node.size == size
355 assert node.size == size
353
356
354 def test_file_size(self):
357 def test_file_size(self):
355 to_check = (
358 to_check = (
356 ('c1214f7e79e02fc37156ff215cd71275450cffc3',
359 ('c1214f7e79e02fc37156ff215cd71275450cffc3',
357 'vcs/backends/BaseRepository.py', 502),
360 'vcs/backends/BaseRepository.py', 502),
358 ('d7e0d30fbcae12c90680eb095a4f5f02505ce501',
361 ('d7e0d30fbcae12c90680eb095a4f5f02505ce501',
359 'vcs/backends/hg.py', 854),
362 'vcs/backends/hg.py', 854),
360 ('6e125e7c890379446e98980d8ed60fba87d0f6d1',
363 ('6e125e7c890379446e98980d8ed60fba87d0f6d1',
361 'setup.py', 1068),
364 'setup.py', 1068),
362 ('d955cd312c17b02143c04fa1099a352b04368118',
365 ('d955cd312c17b02143c04fa1099a352b04368118',
363 'vcs/backends/base.py', 2921),
366 'vcs/backends/base.py', 2921),
364 ('ca1eb7957a54bce53b12d1a51b13452f95bc7c7e',
367 ('ca1eb7957a54bce53b12d1a51b13452f95bc7c7e',
365 'vcs/backends/base.py', 3936),
368 'vcs/backends/base.py', 3936),
366 ('f50f42baeed5af6518ef4b0cb2f1423f3851a941',
369 ('f50f42baeed5af6518ef4b0cb2f1423f3851a941',
367 'vcs/backends/base.py', 6189),
370 'vcs/backends/base.py', 6189),
368 )
371 )
369 for revision, path, size in to_check:
372 for revision, path, size in to_check:
370 self._test_file_size(revision, path, size)
373 self._test_file_size(revision, path, size)
371
374
372 def _test_dir_size(self, revision, path, size):
375 def _test_dir_size(self, revision, path, size):
373 node = self.repo.get_changeset(revision).get_node(path)
376 node = self.repo.get_changeset(revision).get_node(path)
374 assert node.size == size
377 assert node.size == size
375
378
376 def test_dir_size(self):
379 def test_dir_size(self):
377 to_check = (
380 to_check = (
378 ('5f2c6ee195929b0be80749243c18121c9864a3b3', '/', 674076),
381 ('5f2c6ee195929b0be80749243c18121c9864a3b3', '/', 674076),
379 ('7ab37bc680b4aa72c34d07b230c866c28e9fc204', '/', 674049),
382 ('7ab37bc680b4aa72c34d07b230c866c28e9fc204', '/', 674049),
380 ('6892503fb8f2a552cef5f4d4cc2cdbd13ae1cd2f', '/', 671830),
383 ('6892503fb8f2a552cef5f4d4cc2cdbd13ae1cd2f', '/', 671830),
381 )
384 )
382 for revision, path, size in to_check:
385 for revision, path, size in to_check:
383 self._test_dir_size(revision, path, size)
386 self._test_dir_size(revision, path, size)
384
387
385 def test_repo_size(self):
388 def test_repo_size(self):
386 assert self.repo.size == 674076
389 assert self.repo.size == 674076
387
390
388 def test_file_history(self):
391 def test_file_history(self):
389 # we can only check if those revisions are present in the history
392 # we can only check if those revisions are present in the history
390 # as we cannot update this test every time file is changed
393 # as we cannot update this test every time file is changed
391 files = {
394 files = {
392 'setup.py': [
395 'setup.py': [
393 '54386793436c938cff89326944d4c2702340037d',
396 '54386793436c938cff89326944d4c2702340037d',
394 '51d254f0ecf5df2ce50c0b115741f4cf13985dab',
397 '51d254f0ecf5df2ce50c0b115741f4cf13985dab',
395 '998ed409c795fec2012b1c0ca054d99888b22090',
398 '998ed409c795fec2012b1c0ca054d99888b22090',
396 '5e0eb4c47f56564395f76333f319d26c79e2fb09',
399 '5e0eb4c47f56564395f76333f319d26c79e2fb09',
397 '0115510b70c7229dbc5dc49036b32e7d91d23acd',
400 '0115510b70c7229dbc5dc49036b32e7d91d23acd',
398 '7cb3fd1b6d8c20ba89e2264f1c8baebc8a52d36e',
401 '7cb3fd1b6d8c20ba89e2264f1c8baebc8a52d36e',
399 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
402 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
400 '191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e',
403 '191caa5b2c81ed17c0794bf7bb9958f4dcb0b87e',
401 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
404 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
402 ],
405 ],
403 'vcs/nodes.py': [
406 'vcs/nodes.py': [
404 '33fa3223355104431402a888fa77a4e9956feb3e',
407 '33fa3223355104431402a888fa77a4e9956feb3e',
405 'fa014c12c26d10ba682fadb78f2a11c24c8118e1',
408 'fa014c12c26d10ba682fadb78f2a11c24c8118e1',
406 'e686b958768ee96af8029fe19c6050b1a8dd3b2b',
409 'e686b958768ee96af8029fe19c6050b1a8dd3b2b',
407 'ab5721ca0a081f26bf43d9051e615af2cc99952f',
410 'ab5721ca0a081f26bf43d9051e615af2cc99952f',
408 'c877b68d18e792a66b7f4c529ea02c8f80801542',
411 'c877b68d18e792a66b7f4c529ea02c8f80801542',
409 '4313566d2e417cb382948f8d9d7c765330356054',
412 '4313566d2e417cb382948f8d9d7c765330356054',
410 '6c2303a793671e807d1cfc70134c9ca0767d98c2',
413 '6c2303a793671e807d1cfc70134c9ca0767d98c2',
411 '54386793436c938cff89326944d4c2702340037d',
414 '54386793436c938cff89326944d4c2702340037d',
412 '54000345d2e78b03a99d561399e8e548de3f3203',
415 '54000345d2e78b03a99d561399e8e548de3f3203',
413 '1c6b3677b37ea064cb4b51714d8f7498f93f4b2b',
416 '1c6b3677b37ea064cb4b51714d8f7498f93f4b2b',
414 '2d03ca750a44440fb5ea8b751176d1f36f8e8f46',
417 '2d03ca750a44440fb5ea8b751176d1f36f8e8f46',
415 '2a08b128c206db48c2f0b8f70df060e6db0ae4f8',
418 '2a08b128c206db48c2f0b8f70df060e6db0ae4f8',
416 '30c26513ff1eb8e5ce0e1c6b477ee5dc50e2f34b',
419 '30c26513ff1eb8e5ce0e1c6b477ee5dc50e2f34b',
417 'ac71e9503c2ca95542839af0ce7b64011b72ea7c',
420 'ac71e9503c2ca95542839af0ce7b64011b72ea7c',
418 '12669288fd13adba2a9b7dd5b870cc23ffab92d2',
421 '12669288fd13adba2a9b7dd5b870cc23ffab92d2',
419 '5a0c84f3e6fe3473e4c8427199d5a6fc71a9b382',
422 '5a0c84f3e6fe3473e4c8427199d5a6fc71a9b382',
420 '12f2f5e2b38e6ff3fbdb5d722efed9aa72ecb0d5',
423 '12f2f5e2b38e6ff3fbdb5d722efed9aa72ecb0d5',
421 '5eab1222a7cd4bfcbabc218ca6d04276d4e27378',
424 '5eab1222a7cd4bfcbabc218ca6d04276d4e27378',
422 'f50f42baeed5af6518ef4b0cb2f1423f3851a941',
425 'f50f42baeed5af6518ef4b0cb2f1423f3851a941',
423 'd7e390a45f6aa96f04f5e7f583ad4f867431aa25',
426 'd7e390a45f6aa96f04f5e7f583ad4f867431aa25',
424 'f15c21f97864b4f071cddfbf2750ec2e23859414',
427 'f15c21f97864b4f071cddfbf2750ec2e23859414',
425 'e906ef056cf539a4e4e5fc8003eaf7cf14dd8ade',
428 'e906ef056cf539a4e4e5fc8003eaf7cf14dd8ade',
426 'ea2b108b48aa8f8c9c4a941f66c1a03315ca1c3b',
429 'ea2b108b48aa8f8c9c4a941f66c1a03315ca1c3b',
427 '84dec09632a4458f79f50ddbbd155506c460b4f9',
430 '84dec09632a4458f79f50ddbbd155506c460b4f9',
428 '0115510b70c7229dbc5dc49036b32e7d91d23acd',
431 '0115510b70c7229dbc5dc49036b32e7d91d23acd',
429 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
432 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
430 '3bf1c5868e570e39569d094f922d33ced2fa3b2b',
433 '3bf1c5868e570e39569d094f922d33ced2fa3b2b',
431 'b8d04012574729d2c29886e53b1a43ef16dd00a1',
434 'b8d04012574729d2c29886e53b1a43ef16dd00a1',
432 '6970b057cffe4aab0a792aa634c89f4bebf01441',
435 '6970b057cffe4aab0a792aa634c89f4bebf01441',
433 'dd80b0f6cf5052f17cc738c2951c4f2070200d7f',
436 'dd80b0f6cf5052f17cc738c2951c4f2070200d7f',
434 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
437 'ff7ca51e58c505fec0dd2491de52c622bb7a806b',
435 ],
438 ],
436 'vcs/backends/git.py': [
439 'vcs/backends/git.py': [
437 '4cf116ad5a457530381135e2f4c453e68a1b0105',
440 '4cf116ad5a457530381135e2f4c453e68a1b0105',
438 '9a751d84d8e9408e736329767387f41b36935153',
441 '9a751d84d8e9408e736329767387f41b36935153',
439 'cb681fb539c3faaedbcdf5ca71ca413425c18f01',
442 'cb681fb539c3faaedbcdf5ca71ca413425c18f01',
440 '428f81bb652bcba8d631bce926e8834ff49bdcc6',
443 '428f81bb652bcba8d631bce926e8834ff49bdcc6',
441 '180ab15aebf26f98f714d8c68715e0f05fa6e1c7',
444 '180ab15aebf26f98f714d8c68715e0f05fa6e1c7',
442 '2b8e07312a2e89e92b90426ab97f349f4bce2a3a',
445 '2b8e07312a2e89e92b90426ab97f349f4bce2a3a',
443 '50e08c506174d8645a4bb517dd122ac946a0f3bf',
446 '50e08c506174d8645a4bb517dd122ac946a0f3bf',
444 '54000345d2e78b03a99d561399e8e548de3f3203',
447 '54000345d2e78b03a99d561399e8e548de3f3203',
445 ],
448 ],
446 }
449 }
447 for path, revs in files.items():
450 for path, revs in files.items():
448 node = self.repo.get_changeset(revs[0]).get_node(path)
451 node = self.repo.get_changeset(revs[0]).get_node(path)
449 node_revs = [chset.raw_id for chset in node.history]
452 node_revs = [chset.raw_id for chset in node.history]
450 assert set(revs).issubset(set(node_revs)), "We assumed that %s is subset of revisions for which file %s " \
453 assert set(revs).issubset(set(node_revs)), "We assumed that %s is subset of revisions for which file %s " \
451 "has been changed, and history of that node returned: %s" \
454 "has been changed, and history of that node returned: %s" \
452 % (revs, path, node_revs)
455 % (revs, path, node_revs)
453
456
454 def test_file_annotate(self):
457 def test_file_annotate(self):
455 files = {
458 files = {
456 'vcs/backends/__init__.py': {
459 'vcs/backends/__init__.py': {
457 'c1214f7e79e02fc37156ff215cd71275450cffc3': {
460 'c1214f7e79e02fc37156ff215cd71275450cffc3': {
458 'lines_no': 1,
461 'lines_no': 1,
459 'changesets': [
462 'changesets': [
460 'c1214f7e79e02fc37156ff215cd71275450cffc3',
463 'c1214f7e79e02fc37156ff215cd71275450cffc3',
461 ],
464 ],
462 },
465 },
463 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647': {
466 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647': {
464 'lines_no': 21,
467 'lines_no': 21,
465 'changesets': [
468 'changesets': [
466 '49d3fd156b6f7db46313fac355dca1a0b94a0017',
469 '49d3fd156b6f7db46313fac355dca1a0b94a0017',
467 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
470 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
468 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
471 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
469 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
472 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
470 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
473 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
471 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
474 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
472 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
475 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
473 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
476 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
474 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
477 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
475 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
478 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
476 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
479 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
477 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
480 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
478 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
481 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
479 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
482 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
480 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
483 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
481 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
484 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
482 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
485 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
483 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
486 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
484 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
487 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
485 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
488 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
486 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
489 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
487 ],
490 ],
488 },
491 },
489 'e29b67bd158580fc90fc5e9111240b90e6e86064': {
492 'e29b67bd158580fc90fc5e9111240b90e6e86064': {
490 'lines_no': 32,
493 'lines_no': 32,
491 'changesets': [
494 'changesets': [
492 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
495 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
493 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
496 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
494 '5eab1222a7cd4bfcbabc218ca6d04276d4e27378',
497 '5eab1222a7cd4bfcbabc218ca6d04276d4e27378',
495 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
498 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
496 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
499 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
497 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
500 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
498 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
501 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
499 '54000345d2e78b03a99d561399e8e548de3f3203',
502 '54000345d2e78b03a99d561399e8e548de3f3203',
500 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
503 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
501 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
504 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
502 '78c3f0c23b7ee935ec276acb8b8212444c33c396',
505 '78c3f0c23b7ee935ec276acb8b8212444c33c396',
503 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
506 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
504 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
507 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
505 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
508 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
506 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
509 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
507 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
510 '2a13f185e4525f9d4b59882791a2d397b90d5ddc',
508 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
511 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
509 '78c3f0c23b7ee935ec276acb8b8212444c33c396',
512 '78c3f0c23b7ee935ec276acb8b8212444c33c396',
510 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
513 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
511 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
514 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
512 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
515 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
513 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
516 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
514 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
517 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
515 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
518 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
516 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
519 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
517 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
520 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
518 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
521 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
519 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
522 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
520 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
523 '992f38217b979d0b0987d0bae3cc26dac85d9b19',
521 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
524 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
522 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
525 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
523 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
526 '16fba1ae9334d79b66d7afed2c2dfbfa2ae53647',
524 ],
527 ],
525 },
528 },
526 },
529 },
527 }
530 }
528
531
529 for fname, revision_dict in files.items():
532 for fname, revision_dict in files.items():
530 for rev, data in revision_dict.items():
533 for rev, data in revision_dict.items():
531 cs = self.repo.get_changeset(rev)
534 cs = self.repo.get_changeset(rev)
532
535
533 l1_1 = [x[1] for x in cs.get_file_annotate(fname)]
536 l1_1 = [x[1] for x in cs.get_file_annotate(fname)]
534 l1_2 = [x[2]().raw_id for x in cs.get_file_annotate(fname)]
537 l1_2 = [x[2]().raw_id for x in cs.get_file_annotate(fname)]
535 assert l1_1 == l1_2
538 assert l1_1 == l1_2
536 l1 = l1_1
539 l1 = l1_1
537 l2 = files[fname][rev]['changesets']
540 l2 = files[fname][rev]['changesets']
538 assert l1 == l2, "The lists of revision for %s@rev %s" \
541 assert l1 == l2, "The lists of revision for %s@rev %s" \
539 "from annotation list should match each other, " \
542 "from annotation list should match each other, " \
540 "got \n%s \nvs \n%s " % (fname, rev, l1, l2)
543 "got \n%s \nvs \n%s " % (fname, rev, l1, l2)
541
544
542 def test_files_state(self):
545 def test_files_state(self):
543 """
546 """
544 Tests state of FileNodes.
547 Tests state of FileNodes.
545 """
548 """
546 node = self.repo \
549 node = self.repo \
547 .get_changeset('e6ea6d16e2f26250124a1f4b4fe37a912f9d86a0') \
550 .get_changeset('e6ea6d16e2f26250124a1f4b4fe37a912f9d86a0') \
548 .get_node('vcs/utils/diffs.py')
551 .get_node('vcs/utils/diffs.py')
549 assert node.state, NodeState.ADDED
552 assert node.state, NodeState.ADDED
550 assert node.added
553 assert node.added
551 assert not node.changed
554 assert not node.changed
552 assert not node.not_changed
555 assert not node.not_changed
553 assert not node.removed
556 assert not node.removed
554
557
555 node = self.repo \
558 node = self.repo \
556 .get_changeset('33fa3223355104431402a888fa77a4e9956feb3e') \
559 .get_changeset('33fa3223355104431402a888fa77a4e9956feb3e') \
557 .get_node('.hgignore')
560 .get_node('.hgignore')
558 assert node.state, NodeState.CHANGED
561 assert node.state, NodeState.CHANGED
559 assert not node.added
562 assert not node.added
560 assert node.changed
563 assert node.changed
561 assert not node.not_changed
564 assert not node.not_changed
562 assert not node.removed
565 assert not node.removed
563
566
564 node = self.repo \
567 node = self.repo \
565 .get_changeset('e29b67bd158580fc90fc5e9111240b90e6e86064') \
568 .get_changeset('e29b67bd158580fc90fc5e9111240b90e6e86064') \
566 .get_node('setup.py')
569 .get_node('setup.py')
567 assert node.state, NodeState.NOT_CHANGED
570 assert node.state, NodeState.NOT_CHANGED
568 assert not node.added
571 assert not node.added
569 assert not node.changed
572 assert not node.changed
570 assert node.not_changed
573 assert node.not_changed
571 assert not node.removed
574 assert not node.removed
572
575
573 # If node has REMOVED state then trying to fetch it would raise
576 # If node has REMOVED state then trying to fetch it would raise
574 # ChangesetError exception
577 # ChangesetError exception
575 chset = self.repo.get_changeset(
578 chset = self.repo.get_changeset(
576 'fa6600f6848800641328adbf7811fd2372c02ab2')
579 'fa6600f6848800641328adbf7811fd2372c02ab2')
577 path = 'vcs/backends/BaseRepository.py'
580 path = 'vcs/backends/BaseRepository.py'
578 with pytest.raises(NodeDoesNotExistError):
581 with pytest.raises(NodeDoesNotExistError):
579 chset.get_node(path)
582 chset.get_node(path)
580 # but it would be one of ``removed`` (changeset's attribute)
583 # but it would be one of ``removed`` (changeset's attribute)
581 assert path in [rf.path for rf in chset.removed]
584 assert path in [rf.path for rf in chset.removed]
582
585
583 chset = self.repo.get_changeset(
586 chset = self.repo.get_changeset(
584 '54386793436c938cff89326944d4c2702340037d')
587 '54386793436c938cff89326944d4c2702340037d')
585 changed = ['setup.py', 'tests/test_nodes.py', 'vcs/backends/hg.py',
588 changed = ['setup.py', 'tests/test_nodes.py', 'vcs/backends/hg.py',
586 'vcs/nodes.py']
589 'vcs/nodes.py']
587 assert set(changed) == set([f.path for f in chset.changed])
590 assert set(changed) == set([f.path for f in chset.changed])
588
591
589 def test_commit_message_is_unicode(self):
592 def test_commit_message_is_unicode(self):
590 for cs in self.repo:
593 for cs in self.repo:
591 assert type(cs.message) == unicode
594 assert type(cs.message) == unicode
592
595
593 def test_changeset_author_is_unicode(self):
596 def test_changeset_author_is_unicode(self):
594 for cs in self.repo:
597 for cs in self.repo:
595 assert type(cs.author) == unicode
598 assert type(cs.author) == unicode
596
599
597 def test_repo_files_content_is_unicode(self):
600 def test_repo_files_content_is_unicode(self):
598 changeset = self.repo.get_changeset()
601 changeset = self.repo.get_changeset()
599 for node in changeset.get_node('/'):
602 for node in changeset.get_node('/'):
600 if node.is_file():
603 if node.is_file():
601 assert type(node.content) == unicode
604 assert type(node.content) == unicode
602
605
603 def test_wrong_path(self):
606 def test_wrong_path(self):
604 # There is 'setup.py' in the root dir but not there:
607 # There is 'setup.py' in the root dir but not there:
605 path = 'foo/bar/setup.py'
608 path = 'foo/bar/setup.py'
606 tip = self.repo.get_changeset()
609 tip = self.repo.get_changeset()
607 with pytest.raises(VCSError):
610 with pytest.raises(VCSError):
608 tip.get_node(path)
611 tip.get_node(path)
609
612
610 def test_author_email(self):
613 def test_author_email(self):
611 assert 'marcin@python-blog.com' == self.repo.get_changeset('c1214f7e79e02fc37156ff215cd71275450cffc3').author_email
614 assert 'marcin@python-blog.com' == self.repo.get_changeset('c1214f7e79e02fc37156ff215cd71275450cffc3').author_email
612 assert 'lukasz.balcerzak@python-center.pl' == self.repo.get_changeset('ff7ca51e58c505fec0dd2491de52c622bb7a806b').author_email
615 assert 'lukasz.balcerzak@python-center.pl' == self.repo.get_changeset('ff7ca51e58c505fec0dd2491de52c622bb7a806b').author_email
613 assert '' == self.repo.get_changeset('8430a588b43b5d6da365400117c89400326e7992').author_email
616 assert '' == self.repo.get_changeset('8430a588b43b5d6da365400117c89400326e7992').author_email
614
617
615 def test_author_username(self):
618 def test_author_username(self):
616 assert 'Marcin Kuzminski' == self.repo.get_changeset('c1214f7e79e02fc37156ff215cd71275450cffc3').author_name
619 assert 'Marcin Kuzminski' == self.repo.get_changeset('c1214f7e79e02fc37156ff215cd71275450cffc3').author_name
617 assert 'Lukasz Balcerzak' == self.repo.get_changeset('ff7ca51e58c505fec0dd2491de52c622bb7a806b').author_name
620 assert 'Lukasz Balcerzak' == self.repo.get_changeset('ff7ca51e58c505fec0dd2491de52c622bb7a806b').author_name
618 assert 'marcink none@none' == self.repo.get_changeset('8430a588b43b5d6da365400117c89400326e7992').author_name
621 assert 'marcink none@none' == self.repo.get_changeset('8430a588b43b5d6da365400117c89400326e7992').author_name
619
622
620
623
621 class TestGitSpecific():
624 class TestGitSpecific():
622
625
623 def test_error_is_raised_for_added_if_diff_name_status_is_wrong(self):
626 def test_error_is_raised_for_added_if_diff_name_status_is_wrong(self):
624 repo = mock.MagicMock()
627 repo = mock.MagicMock()
625 changeset = GitChangeset(repo, 'foobar')
628 changeset = GitChangeset(repo, 'foobar')
626 changeset._diff_name_status = 'foobar'
629 changeset._diff_name_status = 'foobar'
627 with pytest.raises(VCSError):
630 with pytest.raises(VCSError):
628 changeset.added
631 changeset.added
629
632
630 def test_error_is_raised_for_changed_if_diff_name_status_is_wrong(self):
633 def test_error_is_raised_for_changed_if_diff_name_status_is_wrong(self):
631 repo = mock.MagicMock()
634 repo = mock.MagicMock()
632 changeset = GitChangeset(repo, 'foobar')
635 changeset = GitChangeset(repo, 'foobar')
633 changeset._diff_name_status = 'foobar'
636 changeset._diff_name_status = 'foobar'
634 with pytest.raises(VCSError):
637 with pytest.raises(VCSError):
635 changeset.added
638 changeset.added
636
639
637 def test_error_is_raised_for_removed_if_diff_name_status_is_wrong(self):
640 def test_error_is_raised_for_removed_if_diff_name_status_is_wrong(self):
638 repo = mock.MagicMock()
641 repo = mock.MagicMock()
639 changeset = GitChangeset(repo, 'foobar')
642 changeset = GitChangeset(repo, 'foobar')
640 changeset._diff_name_status = 'foobar'
643 changeset._diff_name_status = 'foobar'
641 with pytest.raises(VCSError):
644 with pytest.raises(VCSError):
642 changeset.added
645 changeset.added
643
646
644
647
645 class TestGitSpecificWithRepo(_BackendTestMixin):
648 class TestGitSpecificWithRepo(_BackendTestMixin):
646 backend_alias = 'git'
649 backend_alias = 'git'
647
650
648 @classmethod
651 @classmethod
649 def _get_commits(cls):
652 def _get_commits(cls):
650 return [
653 return [
651 {
654 {
652 'message': 'Initial',
655 'message': 'Initial',
653 'author': 'Joe Doe <joe.doe@example.com>',
656 'author': 'Joe Doe <joe.doe@example.com>',
654 'date': datetime.datetime(2010, 1, 1, 20),
657 'date': datetime.datetime(2010, 1, 1, 20),
655 'added': [
658 'added': [
656 FileNode('foobar/static/js/admin/base.js', content='base'),
659 FileNode('foobar/static/js/admin/base.js', content='base'),
657 FileNode('foobar/static/admin', content='admin',
660 FileNode('foobar/static/admin', content='admin',
658 mode=0120000), # this is a link
661 mode=0120000), # this is a link
659 FileNode('foo', content='foo'),
662 FileNode('foo', content='foo'),
660 ],
663 ],
661 },
664 },
662 {
665 {
663 'message': 'Second',
666 'message': 'Second',
664 'author': 'Joe Doe <joe.doe@example.com>',
667 'author': 'Joe Doe <joe.doe@example.com>',
665 'date': datetime.datetime(2010, 1, 1, 22),
668 'date': datetime.datetime(2010, 1, 1, 22),
666 'added': [
669 'added': [
667 FileNode('foo2', content='foo2'),
670 FileNode('foo2', content='foo2'),
668 ],
671 ],
669 },
672 },
670 ]
673 ]
671
674
672 def test_paths_slow_traversing(self):
675 def test_paths_slow_traversing(self):
673 cs = self.repo.get_changeset()
676 cs = self.repo.get_changeset()
674 assert cs.get_node('foobar').get_node('static').get_node('js').get_node('admin').get_node('base.js').content == 'base'
677 assert cs.get_node('foobar').get_node('static').get_node('js').get_node('admin').get_node('base.js').content == 'base'
675
678
676 def test_paths_fast_traversing(self):
679 def test_paths_fast_traversing(self):
677 cs = self.repo.get_changeset()
680 cs = self.repo.get_changeset()
678 assert cs.get_node('foobar/static/js/admin/base.js').content == 'base'
681 assert cs.get_node('foobar/static/js/admin/base.js').content == 'base'
679
682
680 def test_workdir_get_branch(self):
683 def test_workdir_get_branch(self):
681 self.repo.run_git_command(['checkout', '-b', 'production'])
684 self.repo.run_git_command(['checkout', '-b', 'production'])
682 # Regression test: one of following would fail if we don't check
685 # Regression test: one of following would fail if we don't check
683 # .git/HEAD file
686 # .git/HEAD file
684 self.repo.run_git_command(['checkout', 'production'])
687 self.repo.run_git_command(['checkout', 'production'])
685 assert self.repo.workdir.get_branch() == 'production'
688 assert self.repo.workdir.get_branch() == 'production'
686 self.repo.run_git_command(['checkout', 'master'])
689 self.repo.run_git_command(['checkout', 'master'])
687 assert self.repo.workdir.get_branch() == 'master'
690 assert self.repo.workdir.get_branch() == 'master'
688
691
689 def test_get_diff_runs_git_command_with_hashes(self):
692 def test_get_diff_runs_git_command_with_hashes(self):
690 self.repo.run_git_command = mock.Mock(return_value=['', ''])
693 self.repo.run_git_command = mock.Mock(return_value=['', ''])
691 self.repo.get_diff(0, 1)
694 self.repo.get_diff(0, 1)
692 self.repo.run_git_command.assert_called_once_with(
695 self.repo.run_git_command.assert_called_once_with(
693 ['diff', '-U3', '--full-index', '--binary', '-p', '-M', '--abbrev=40',
696 ['diff', '-U3', '--full-index', '--binary', '-p', '-M', '--abbrev=40',
694 self.repo._get_revision(0), self.repo._get_revision(1)])
697 self.repo._get_revision(0), self.repo._get_revision(1)])
695
698
696 def test_get_diff_runs_git_command_with_str_hashes(self):
699 def test_get_diff_runs_git_command_with_str_hashes(self):
697 self.repo.run_git_command = mock.Mock(return_value=['', ''])
700 self.repo.run_git_command = mock.Mock(return_value=['', ''])
698 self.repo.get_diff(self.repo.EMPTY_CHANGESET, 1)
701 self.repo.get_diff(self.repo.EMPTY_CHANGESET, 1)
699 self.repo.run_git_command.assert_called_once_with(
702 self.repo.run_git_command.assert_called_once_with(
700 ['show', '-U3', '--full-index', '--binary', '-p', '-M', '--abbrev=40',
703 ['show', '-U3', '--full-index', '--binary', '-p', '-M', '--abbrev=40',
701 self.repo._get_revision(1)])
704 self.repo._get_revision(1)])
702
705
703 def test_get_diff_runs_git_command_with_path_if_its_given(self):
706 def test_get_diff_runs_git_command_with_path_if_its_given(self):
704 self.repo.run_git_command = mock.Mock(return_value=['', ''])
707 self.repo.run_git_command = mock.Mock(return_value=['', ''])
705 self.repo.get_diff(0, 1, 'foo')
708 self.repo.get_diff(0, 1, 'foo')
706 self.repo.run_git_command.assert_called_once_with(
709 self.repo.run_git_command.assert_called_once_with(
707 ['diff', '-U3', '--full-index', '--binary', '-p', '-M', '--abbrev=40',
710 ['diff', '-U3', '--full-index', '--binary', '-p', '-M', '--abbrev=40',
708 self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'])
711 self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'])
709
712
710 def test_get_diff_does_not_sanitize_valid_context(self):
713 def test_get_diff_does_not_sanitize_valid_context(self):
711 almost_overflowed_long_int = 2**31-1
714 almost_overflowed_long_int = 2**31-1
712
715
713 self.repo.run_git_command = mock.Mock(return_value=['', ''])
716 self.repo.run_git_command = mock.Mock(return_value=['', ''])
714 self.repo.get_diff(0, 1, 'foo', context=almost_overflowed_long_int)
717 self.repo.get_diff(0, 1, 'foo', context=almost_overflowed_long_int)
715 self.repo.run_git_command.assert_called_once_with(
718 self.repo.run_git_command.assert_called_once_with(
716 ['diff', '-U' + str(almost_overflowed_long_int), '--full-index', '--binary', '-p', '-M', '--abbrev=40',
719 ['diff', '-U' + str(almost_overflowed_long_int), '--full-index', '--binary', '-p', '-M', '--abbrev=40',
717 self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'])
720 self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'])
718
721
719 def test_get_diff_sanitizes_overflowing_context(self):
722 def test_get_diff_sanitizes_overflowing_context(self):
720 overflowed_long_int = 2**31
723 overflowed_long_int = 2**31
721 sanitized_overflowed_long_int = overflowed_long_int-1
724 sanitized_overflowed_long_int = overflowed_long_int-1
722
725
723 self.repo.run_git_command = mock.Mock(return_value=['', ''])
726 self.repo.run_git_command = mock.Mock(return_value=['', ''])
724 self.repo.get_diff(0, 1, 'foo', context=overflowed_long_int)
727 self.repo.get_diff(0, 1, 'foo', context=overflowed_long_int)
725
728
726 self.repo.run_git_command.assert_called_once_with(
729 self.repo.run_git_command.assert_called_once_with(
727 ['diff', '-U' + str(sanitized_overflowed_long_int), '--full-index', '--binary', '-p', '-M', '--abbrev=40',
730 ['diff', '-U' + str(sanitized_overflowed_long_int), '--full-index', '--binary', '-p', '-M', '--abbrev=40',
728 self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'])
731 self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'])
729
732
730 def test_get_diff_does_not_sanitize_zero_context(self):
733 def test_get_diff_does_not_sanitize_zero_context(self):
731 zero_context = 0
734 zero_context = 0
732
735
733 self.repo.run_git_command = mock.Mock(return_value=['', ''])
736 self.repo.run_git_command = mock.Mock(return_value=['', ''])
734 self.repo.get_diff(0, 1, 'foo', context=zero_context)
737 self.repo.get_diff(0, 1, 'foo', context=zero_context)
735
738
736 self.repo.run_git_command.assert_called_once_with(
739 self.repo.run_git_command.assert_called_once_with(
737 ['diff', '-U' + str(zero_context), '--full-index', '--binary', '-p', '-M', '--abbrev=40',
740 ['diff', '-U' + str(zero_context), '--full-index', '--binary', '-p', '-M', '--abbrev=40',
738 self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'])
741 self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'])
739
742
740 def test_get_diff_sanitizes_negative_context(self):
743 def test_get_diff_sanitizes_negative_context(self):
741 negative_context = -10
744 negative_context = -10
742
745
743 self.repo.run_git_command = mock.Mock(return_value=['', ''])
746 self.repo.run_git_command = mock.Mock(return_value=['', ''])
744 self.repo.get_diff(0, 1, 'foo', context=negative_context)
747 self.repo.get_diff(0, 1, 'foo', context=negative_context)
745
748
746 self.repo.run_git_command.assert_called_once_with(
749 self.repo.run_git_command.assert_called_once_with(
747 ['diff', '-U0', '--full-index', '--binary', '-p', '-M', '--abbrev=40',
750 ['diff', '-U0', '--full-index', '--binary', '-p', '-M', '--abbrev=40',
748 self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'])
751 self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'])
749
752
750
753
751 class TestGitRegression(_BackendTestMixin):
754 class TestGitRegression(_BackendTestMixin):
752 backend_alias = 'git'
755 backend_alias = 'git'
753
756
754 @classmethod
757 @classmethod
755 def _get_commits(cls):
758 def _get_commits(cls):
756 return [
759 return [
757 {
760 {
758 'message': 'Initial',
761 'message': 'Initial',
759 'author': 'Joe Doe <joe.doe@example.com>',
762 'author': 'Joe Doe <joe.doe@example.com>',
760 'date': datetime.datetime(2010, 1, 1, 20),
763 'date': datetime.datetime(2010, 1, 1, 20),
761 'added': [
764 'added': [
762 FileNode('bot/__init__.py', content='base'),
765 FileNode('bot/__init__.py', content='base'),
763 FileNode('bot/templates/404.html', content='base'),
766 FileNode('bot/templates/404.html', content='base'),
764 FileNode('bot/templates/500.html', content='base'),
767 FileNode('bot/templates/500.html', content='base'),
765 ],
768 ],
766 },
769 },
767 {
770 {
768 'message': 'Second',
771 'message': 'Second',
769 'author': 'Joe Doe <joe.doe@example.com>',
772 'author': 'Joe Doe <joe.doe@example.com>',
770 'date': datetime.datetime(2010, 1, 1, 22),
773 'date': datetime.datetime(2010, 1, 1, 22),
771 'added': [
774 'added': [
772 FileNode('bot/build/migrations/1.py', content='foo2'),
775 FileNode('bot/build/migrations/1.py', content='foo2'),
773 FileNode('bot/build/migrations/2.py', content='foo2'),
776 FileNode('bot/build/migrations/2.py', content='foo2'),
774 FileNode('bot/build/static/templates/f.html', content='foo2'),
777 FileNode('bot/build/static/templates/f.html', content='foo2'),
775 FileNode('bot/build/static/templates/f1.html', content='foo2'),
778 FileNode('bot/build/static/templates/f1.html', content='foo2'),
776 FileNode('bot/build/templates/err.html', content='foo2'),
779 FileNode('bot/build/templates/err.html', content='foo2'),
777 FileNode('bot/build/templates/err2.html', content='foo2'),
780 FileNode('bot/build/templates/err2.html', content='foo2'),
778 ],
781 ],
779 },
782 },
780 ]
783 ]
781
784
782 def test_similar_paths(self):
785 def test_similar_paths(self):
783 cs = self.repo.get_changeset()
786 cs = self.repo.get_changeset()
784 paths = lambda *n: [x.path for x in n]
787 paths = lambda *n: [x.path for x in n]
785 assert paths(*cs.get_nodes('bot')) == ['bot/build', 'bot/templates', 'bot/__init__.py']
788 assert paths(*cs.get_nodes('bot')) == ['bot/build', 'bot/templates', 'bot/__init__.py']
786 assert paths(*cs.get_nodes('bot/build')) == ['bot/build/migrations', 'bot/build/static', 'bot/build/templates']
789 assert paths(*cs.get_nodes('bot/build')) == ['bot/build/migrations', 'bot/build/static', 'bot/build/templates']
787 assert paths(*cs.get_nodes('bot/build/static')) == ['bot/build/static/templates']
790 assert paths(*cs.get_nodes('bot/build/static')) == ['bot/build/static/templates']
788 # this get_nodes below causes troubles !
791 # this get_nodes below causes troubles !
789 assert paths(*cs.get_nodes('bot/build/static/templates')) == ['bot/build/static/templates/f.html', 'bot/build/static/templates/f1.html']
792 assert paths(*cs.get_nodes('bot/build/static/templates')) == ['bot/build/static/templates/f.html', 'bot/build/static/templates/f1.html']
790 assert paths(*cs.get_nodes('bot/build/templates')) == ['bot/build/templates/err.html', 'bot/build/templates/err2.html']
793 assert paths(*cs.get_nodes('bot/build/templates')) == ['bot/build/templates/err.html', 'bot/build/templates/err2.html']
791 assert paths(*cs.get_nodes('bot/templates/')) == ['bot/templates/404.html', 'bot/templates/500.html']
794 assert paths(*cs.get_nodes('bot/templates/')) == ['bot/templates/404.html', 'bot/templates/500.html']
792
795
793
796
794 class TestGitHooks(object):
797 class TestGitHooks(object):
795 """
798 """
796 Tests related to hook functionality of Git repositories.
799 Tests related to hook functionality of Git repositories.
797 """
800 """
798
801
799 def setup_method(self):
802 def setup_method(self):
800 # For each run we want a fresh repo.
803 # For each run we want a fresh repo.
801 self.repo_directory = get_new_dir("githookrepo")
804 self.repo_directory = get_new_dir("githookrepo")
802 self.repo = GitRepository(self.repo_directory, create=True)
805 self.repo = GitRepository(self.repo_directory, create=True)
803
806
804 # Create a dictionary where keys are hook names, and values are paths to
807 # Create a dictionary where keys are hook names, and values are paths to
805 # them. Deduplicates code in tests a bit.
808 # them. Deduplicates code in tests a bit.
806 self.hook_directory = self.repo.get_hook_location()
809 self.hook_directory = self.repo.get_hook_location()
807 self.kallithea_hooks = dict((h, os.path.join(self.hook_directory, h)) for h in ("pre-receive", "post-receive"))
810 self.kallithea_hooks = dict((h, os.path.join(self.hook_directory, h)) for h in ("pre-receive", "post-receive"))
808
811
809 def test_hooks_created_if_missing(self):
812 def test_hooks_created_if_missing(self):
810 """
813 """
811 Tests if hooks are installed in repository if they are missing.
814 Tests if hooks are installed in repository if they are missing.
812 """
815 """
813
816
814 for hook, hook_path in self.kallithea_hooks.iteritems():
817 for hook, hook_path in self.kallithea_hooks.iteritems():
815 if os.path.exists(hook_path):
818 if os.path.exists(hook_path):
816 os.remove(hook_path)
819 os.remove(hook_path)
817
820
818 ScmModel().install_git_hooks(repo=self.repo)
821 ScmModel().install_git_hooks(repo=self.repo)
819
822
820 for hook, hook_path in self.kallithea_hooks.iteritems():
823 for hook, hook_path in self.kallithea_hooks.iteritems():
821 assert os.path.exists(hook_path)
824 assert os.path.exists(hook_path)
822
825
823 def test_kallithea_hooks_updated(self):
826 def test_kallithea_hooks_updated(self):
824 """
827 """
825 Tests if hooks are updated if they are Kallithea hooks already.
828 Tests if hooks are updated if they are Kallithea hooks already.
826 """
829 """
827
830
828 for hook, hook_path in self.kallithea_hooks.iteritems():
831 for hook, hook_path in self.kallithea_hooks.iteritems():
829 with open(hook_path, "w") as f:
832 with open(hook_path, "w") as f:
830 f.write("KALLITHEA_HOOK_VER=0.0.0\nJUST_BOGUS")
833 f.write("KALLITHEA_HOOK_VER=0.0.0\nJUST_BOGUS")
831
834
832 ScmModel().install_git_hooks(repo=self.repo)
835 ScmModel().install_git_hooks(repo=self.repo)
833
836
834 for hook, hook_path in self.kallithea_hooks.iteritems():
837 for hook, hook_path in self.kallithea_hooks.iteritems():
835 with open(hook_path) as f:
838 with open(hook_path) as f:
836 assert "JUST_BOGUS" not in f.read()
839 assert "JUST_BOGUS" not in f.read()
837
840
838 def test_custom_hooks_untouched(self):
841 def test_custom_hooks_untouched(self):
839 """
842 """
840 Tests if hooks are left untouched if they are not Kallithea hooks.
843 Tests if hooks are left untouched if they are not Kallithea hooks.
841 """
844 """
842
845
843 for hook, hook_path in self.kallithea_hooks.iteritems():
846 for hook, hook_path in self.kallithea_hooks.iteritems():
844 with open(hook_path, "w") as f:
847 with open(hook_path, "w") as f:
845 f.write("#!/bin/bash\n#CUSTOM_HOOK")
848 f.write("#!/bin/bash\n#CUSTOM_HOOK")
846
849
847 ScmModel().install_git_hooks(repo=self.repo)
850 ScmModel().install_git_hooks(repo=self.repo)
848
851
849 for hook, hook_path in self.kallithea_hooks.iteritems():
852 for hook, hook_path in self.kallithea_hooks.iteritems():
850 with open(hook_path) as f:
853 with open(hook_path) as f:
851 assert "CUSTOM_HOOK" in f.read()
854 assert "CUSTOM_HOOK" in f.read()
852
855
853 def test_custom_hooks_forced_update(self):
856 def test_custom_hooks_forced_update(self):
854 """
857 """
855 Tests if hooks are forcefully updated even though they are custom hooks.
858 Tests if hooks are forcefully updated even though they are custom hooks.
856 """
859 """
857
860
858 for hook, hook_path in self.kallithea_hooks.iteritems():
861 for hook, hook_path in self.kallithea_hooks.iteritems():
859 with open(hook_path, "w") as f:
862 with open(hook_path, "w") as f:
860 f.write("#!/bin/bash\n#CUSTOM_HOOK")
863 f.write("#!/bin/bash\n#CUSTOM_HOOK")
861
864
862 ScmModel().install_git_hooks(repo=self.repo, force_create=True)
865 ScmModel().install_git_hooks(repo=self.repo, force_create=True)
863
866
864 for hook, hook_path in self.kallithea_hooks.iteritems():
867 for hook, hook_path in self.kallithea_hooks.iteritems():
865 with open(hook_path) as f:
868 with open(hook_path) as f:
866 assert "KALLITHEA_HOOK_VER" in f.read()
869 assert "KALLITHEA_HOOK_VER" in f.read()
@@ -1,583 +1,586 b''
1 import os
1 import os
2
2
3 import pytest
3 import pytest
4 import mock
4 import mock
5
5
6 from kallithea.lib.utils2 import safe_str
6 from kallithea.lib.utils2 import safe_str
7 from kallithea.lib.vcs.backends.hg import MercurialRepository, MercurialChangeset
7 from kallithea.lib.vcs.backends.hg import MercurialRepository, MercurialChangeset
8 from kallithea.lib.vcs.exceptions import RepositoryError, VCSError, NodeDoesNotExistError
8 from kallithea.lib.vcs.exceptions import RepositoryError, VCSError, NodeDoesNotExistError
9 from kallithea.lib.vcs.nodes import NodeKind, NodeState
9 from kallithea.lib.vcs.nodes import NodeKind, NodeState
10
10
11 from kallithea.tests.vcs.conf import TEST_HG_REPO, TEST_HG_REPO_CLONE, \
11 from kallithea.tests.vcs.conf import TEST_HG_REPO, TEST_HG_REPO_CLONE, \
12 TEST_HG_REPO_PULL, TESTS_TMP_PATH
12 TEST_HG_REPO_PULL, TESTS_TMP_PATH
13
13
14
14
15 class TestMercurialRepository(object):
15 class TestMercurialRepository(object):
16
16
17 def __check_for_existing_repo(self):
17 def __check_for_existing_repo(self):
18 if os.path.exists(TEST_HG_REPO_CLONE):
18 if os.path.exists(TEST_HG_REPO_CLONE):
19 pytest.fail('Cannot test mercurial clone repo as location %s already '
19 pytest.fail('Cannot test mercurial clone repo as location %s already '
20 'exists. You should manually remove it first.'
20 'exists. You should manually remove it first.'
21 % TEST_HG_REPO_CLONE)
21 % TEST_HG_REPO_CLONE)
22
22
23 def setup_method(self):
23 def setup_method(self):
24 self.repo = MercurialRepository(safe_str(TEST_HG_REPO))
24 self.repo = MercurialRepository(safe_str(TEST_HG_REPO))
25
25
26 def test_wrong_repo_path(self):
26 def test_wrong_repo_path(self):
27 wrong_repo_path = os.path.join(TESTS_TMP_PATH, 'errorrepo')
27 wrong_repo_path = os.path.join(TESTS_TMP_PATH, 'errorrepo')
28 with pytest.raises(RepositoryError):
28 with pytest.raises(RepositoryError):
29 MercurialRepository(wrong_repo_path)
29 MercurialRepository(wrong_repo_path)
30
30
31 def test_unicode_path_repo(self):
31 def test_unicode_path_repo(self):
32 with pytest.raises(VCSError):
32 with pytest.raises(VCSError):
33 MercurialRepository(u'iShouldFail')
33 MercurialRepository(u'iShouldFail')
34
34
35 def test_repo_clone(self):
35 def test_repo_clone(self):
36 self.__check_for_existing_repo()
36 self.__check_for_existing_repo()
37 repo = MercurialRepository(safe_str(TEST_HG_REPO))
37 repo = MercurialRepository(safe_str(TEST_HG_REPO))
38 repo_clone = MercurialRepository(TEST_HG_REPO_CLONE,
38 repo_clone = MercurialRepository(TEST_HG_REPO_CLONE,
39 src_url=TEST_HG_REPO, update_after_clone=True)
39 src_url=TEST_HG_REPO, update_after_clone=True)
40 assert len(repo.revisions) == len(repo_clone.revisions)
40 assert len(repo.revisions) == len(repo_clone.revisions)
41 # Checking hashes of changesets should be enough
41 # Checking hashes of changesets should be enough
42 for changeset in repo.get_changesets():
42 for changeset in repo.get_changesets():
43 raw_id = changeset.raw_id
43 raw_id = changeset.raw_id
44 assert raw_id == repo_clone.get_changeset(raw_id).raw_id
44 assert raw_id == repo_clone.get_changeset(raw_id).raw_id
45
45
46 def test_repo_clone_with_update(self):
46 def test_repo_clone_with_update(self):
47 repo = MercurialRepository(safe_str(TEST_HG_REPO))
47 repo = MercurialRepository(safe_str(TEST_HG_REPO))
48 repo_clone = MercurialRepository(TEST_HG_REPO_CLONE + '_w_update',
48 repo_clone = MercurialRepository(TEST_HG_REPO_CLONE + '_w_update',
49 src_url=TEST_HG_REPO, update_after_clone=True)
49 src_url=TEST_HG_REPO, update_after_clone=True)
50 assert len(repo.revisions) == len(repo_clone.revisions)
50 assert len(repo.revisions) == len(repo_clone.revisions)
51
51
52 # check if current workdir was updated
52 # check if current workdir was updated
53 assert os.path.isfile(
53 assert os.path.isfile(
54 os.path.join(
54 os.path.join(
55 TEST_HG_REPO_CLONE + '_w_update', 'MANIFEST.in'
55 TEST_HG_REPO_CLONE + '_w_update', 'MANIFEST.in'
56 )
56 )
57 )
57 )
58
58
59 def test_repo_clone_without_update(self):
59 def test_repo_clone_without_update(self):
60 repo = MercurialRepository(safe_str(TEST_HG_REPO))
60 repo = MercurialRepository(safe_str(TEST_HG_REPO))
61 repo_clone = MercurialRepository(TEST_HG_REPO_CLONE + '_wo_update',
61 repo_clone = MercurialRepository(TEST_HG_REPO_CLONE + '_wo_update',
62 src_url=TEST_HG_REPO, update_after_clone=False)
62 src_url=TEST_HG_REPO, update_after_clone=False)
63 assert len(repo.revisions) == len(repo_clone.revisions)
63 assert len(repo.revisions) == len(repo_clone.revisions)
64 assert not os.path.isfile(
64 assert not os.path.isfile(
65 os.path.join(
65 os.path.join(
66 TEST_HG_REPO_CLONE + '_wo_update', 'MANIFEST.in'
66 TEST_HG_REPO_CLONE + '_wo_update', 'MANIFEST.in'
67 )
67 )
68 )
68 )
69
69
70 def test_pull(self):
70 def test_pull(self):
71 if os.path.exists(TEST_HG_REPO_PULL):
71 if os.path.exists(TEST_HG_REPO_PULL):
72 pytest.fail('Cannot test mercurial pull command as location %s '
72 pytest.fail('Cannot test mercurial pull command as location %s '
73 'already exists. You should manually remove it first'
73 'already exists. You should manually remove it first'
74 % TEST_HG_REPO_PULL)
74 % TEST_HG_REPO_PULL)
75 repo_new = MercurialRepository(TEST_HG_REPO_PULL, create=True)
75 repo_new = MercurialRepository(TEST_HG_REPO_PULL, create=True)
76 assert len(self.repo.revisions) > len(repo_new.revisions)
76 assert len(self.repo.revisions) > len(repo_new.revisions)
77
77
78 repo_new.pull(self.repo.path)
78 repo_new.pull(self.repo.path)
79 repo_new = MercurialRepository(TEST_HG_REPO_PULL)
79 repo_new = MercurialRepository(TEST_HG_REPO_PULL)
80 assert len(self.repo.revisions) == len(repo_new.revisions)
80 assert len(self.repo.revisions) == len(repo_new.revisions)
81
81
82 def test_revisions(self):
82 def test_revisions(self):
83 # there are 21 revisions at bitbucket now
83 # there are 21 revisions at bitbucket now
84 # so we can assume they would be available from now on
84 # so we can assume they would be available from now on
85 subset = set(['b986218ba1c9b0d6a259fac9b050b1724ed8e545',
85 subset = set(['b986218ba1c9b0d6a259fac9b050b1724ed8e545',
86 '3d8f361e72ab303da48d799ff1ac40d5ac37c67e',
86 '3d8f361e72ab303da48d799ff1ac40d5ac37c67e',
87 '6cba7170863a2411822803fa77a0a264f1310b35',
87 '6cba7170863a2411822803fa77a0a264f1310b35',
88 '56349e29c2af3ac913b28bde9a2c6154436e615b',
88 '56349e29c2af3ac913b28bde9a2c6154436e615b',
89 '2dda4e345facb0ccff1a191052dd1606dba6781d',
89 '2dda4e345facb0ccff1a191052dd1606dba6781d',
90 '6fff84722075f1607a30f436523403845f84cd9e',
90 '6fff84722075f1607a30f436523403845f84cd9e',
91 '7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7',
91 '7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7',
92 '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb',
92 '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb',
93 'dc5d2c0661b61928834a785d3e64a3f80d3aad9c',
93 'dc5d2c0661b61928834a785d3e64a3f80d3aad9c',
94 'be90031137367893f1c406e0a8683010fd115b79',
94 'be90031137367893f1c406e0a8683010fd115b79',
95 'db8e58be770518cbb2b1cdfa69146e47cd481481',
95 'db8e58be770518cbb2b1cdfa69146e47cd481481',
96 '84478366594b424af694a6c784cb991a16b87c21',
96 '84478366594b424af694a6c784cb991a16b87c21',
97 '17f8e105dddb9f339600389c6dc7175d395a535c',
97 '17f8e105dddb9f339600389c6dc7175d395a535c',
98 '20a662e756499bde3095ffc9bc0643d1def2d0eb',
98 '20a662e756499bde3095ffc9bc0643d1def2d0eb',
99 '2e319b85e70a707bba0beff866d9f9de032aa4f9',
99 '2e319b85e70a707bba0beff866d9f9de032aa4f9',
100 '786facd2c61deb9cf91e9534735124fb8fc11842',
100 '786facd2c61deb9cf91e9534735124fb8fc11842',
101 '94593d2128d38210a2fcd1aabff6dda0d6d9edf8',
101 '94593d2128d38210a2fcd1aabff6dda0d6d9edf8',
102 'aa6a0de05b7612707db567078e130a6cd114a9a7',
102 'aa6a0de05b7612707db567078e130a6cd114a9a7',
103 'eada5a770da98ab0dd7325e29d00e0714f228d09'
103 'eada5a770da98ab0dd7325e29d00e0714f228d09'
104 ])
104 ])
105 assert subset.issubset(set(self.repo.revisions))
105 assert subset.issubset(set(self.repo.revisions))
106
106
107 # check if we have the proper order of revisions
107 # check if we have the proper order of revisions
108 org = ['b986218ba1c9b0d6a259fac9b050b1724ed8e545',
108 org = ['b986218ba1c9b0d6a259fac9b050b1724ed8e545',
109 '3d8f361e72ab303da48d799ff1ac40d5ac37c67e',
109 '3d8f361e72ab303da48d799ff1ac40d5ac37c67e',
110 '6cba7170863a2411822803fa77a0a264f1310b35',
110 '6cba7170863a2411822803fa77a0a264f1310b35',
111 '56349e29c2af3ac913b28bde9a2c6154436e615b',
111 '56349e29c2af3ac913b28bde9a2c6154436e615b',
112 '2dda4e345facb0ccff1a191052dd1606dba6781d',
112 '2dda4e345facb0ccff1a191052dd1606dba6781d',
113 '6fff84722075f1607a30f436523403845f84cd9e',
113 '6fff84722075f1607a30f436523403845f84cd9e',
114 '7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7',
114 '7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7',
115 '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb',
115 '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb',
116 'dc5d2c0661b61928834a785d3e64a3f80d3aad9c',
116 'dc5d2c0661b61928834a785d3e64a3f80d3aad9c',
117 'be90031137367893f1c406e0a8683010fd115b79',
117 'be90031137367893f1c406e0a8683010fd115b79',
118 'db8e58be770518cbb2b1cdfa69146e47cd481481',
118 'db8e58be770518cbb2b1cdfa69146e47cd481481',
119 '84478366594b424af694a6c784cb991a16b87c21',
119 '84478366594b424af694a6c784cb991a16b87c21',
120 '17f8e105dddb9f339600389c6dc7175d395a535c',
120 '17f8e105dddb9f339600389c6dc7175d395a535c',
121 '20a662e756499bde3095ffc9bc0643d1def2d0eb',
121 '20a662e756499bde3095ffc9bc0643d1def2d0eb',
122 '2e319b85e70a707bba0beff866d9f9de032aa4f9',
122 '2e319b85e70a707bba0beff866d9f9de032aa4f9',
123 '786facd2c61deb9cf91e9534735124fb8fc11842',
123 '786facd2c61deb9cf91e9534735124fb8fc11842',
124 '94593d2128d38210a2fcd1aabff6dda0d6d9edf8',
124 '94593d2128d38210a2fcd1aabff6dda0d6d9edf8',
125 'aa6a0de05b7612707db567078e130a6cd114a9a7',
125 'aa6a0de05b7612707db567078e130a6cd114a9a7',
126 'eada5a770da98ab0dd7325e29d00e0714f228d09',
126 'eada5a770da98ab0dd7325e29d00e0714f228d09',
127 '2c1885c735575ca478bf9e17b0029dca68824458',
127 '2c1885c735575ca478bf9e17b0029dca68824458',
128 'd9bcd465040bf869799b09ad732c04e0eea99fe9',
128 'd9bcd465040bf869799b09ad732c04e0eea99fe9',
129 '469e9c847fe1f6f7a697b8b25b4bc5b48780c1a7',
129 '469e9c847fe1f6f7a697b8b25b4bc5b48780c1a7',
130 '4fb8326d78e5120da2c7468dcf7098997be385da',
130 '4fb8326d78e5120da2c7468dcf7098997be385da',
131 '62b4a097164940bd66030c4db51687f3ec035eed',
131 '62b4a097164940bd66030c4db51687f3ec035eed',
132 '536c1a19428381cfea92ac44985304f6a8049569',
132 '536c1a19428381cfea92ac44985304f6a8049569',
133 '965e8ab3c44b070cdaa5bf727ddef0ada980ecc4',
133 '965e8ab3c44b070cdaa5bf727ddef0ada980ecc4',
134 '9bb326a04ae5d98d437dece54be04f830cf1edd9',
134 '9bb326a04ae5d98d437dece54be04f830cf1edd9',
135 'f8940bcb890a98c4702319fbe36db75ea309b475',
135 'f8940bcb890a98c4702319fbe36db75ea309b475',
136 'ff5ab059786ebc7411e559a2cc309dfae3625a3b',
136 'ff5ab059786ebc7411e559a2cc309dfae3625a3b',
137 '6b6ad5f82ad5bb6190037671bd254bd4e1f4bf08',
137 '6b6ad5f82ad5bb6190037671bd254bd4e1f4bf08',
138 'ee87846a61c12153b51543bf860e1026c6d3dcba', ]
138 'ee87846a61c12153b51543bf860e1026c6d3dcba', ]
139 assert org == self.repo.revisions[:31]
139 assert org == self.repo.revisions[:31]
140
140
141 def test_iter_slice(self):
141 def test_iter_slice(self):
142 sliced = list(self.repo[:10])
142 sliced = list(self.repo[:10])
143 itered = list(self.repo)[:10]
143 itered = list(self.repo)[:10]
144 assert sliced == itered
144 assert sliced == itered
145
145
146 def test_slicing(self):
146 def test_slicing(self):
147 # 4 1 5 10 95
147 # 4 1 5 10 95
148 for sfrom, sto, size in [(0, 4, 4), (1, 2, 1), (10, 15, 5),
148 for sfrom, sto, size in [(0, 4, 4), (1, 2, 1), (10, 15, 5),
149 (10, 20, 10), (5, 100, 95)]:
149 (10, 20, 10), (5, 100, 95)]:
150 revs = list(self.repo[sfrom:sto])
150 revs = list(self.repo[sfrom:sto])
151 assert len(revs) == size
151 assert len(revs) == size
152 assert revs[0] == self.repo.get_changeset(sfrom)
152 assert revs[0] == self.repo.get_changeset(sfrom)
153 assert revs[-1] == self.repo.get_changeset(sto - 1)
153 assert revs[-1] == self.repo.get_changeset(sto - 1)
154
154
155 def test_branches(self):
155 def test_branches(self):
156 # TODO: Need more tests here
156 # TODO: Need more tests here
157
157
158 # active branches
158 # active branches
159 assert 'default' in self.repo.branches
159 assert 'default' in self.repo.branches
160 assert 'stable' in self.repo.branches
160 assert 'stable' in self.repo.branches
161
161
162 # closed
162 # closed
163 assert 'git' in self.repo._get_branches(closed=True)
163 assert 'git' in self.repo._get_branches(closed=True)
164 assert 'web' in self.repo._get_branches(closed=True)
164 assert 'web' in self.repo._get_branches(closed=True)
165
165
166 for name, id in self.repo.branches.items():
166 for name, id in self.repo.branches.items():
167 assert isinstance(self.repo.get_changeset(id), MercurialChangeset)
167 assert isinstance(self.repo.get_changeset(id), MercurialChangeset)
168
168
169 def test_tip_in_tags(self):
169 def test_tip_in_tags(self):
170 # tip is always a tag
170 # tip is always a tag
171 assert 'tip' in self.repo.tags
171 assert 'tip' in self.repo.tags
172
172
173 def test_tip_changeset_in_tags(self):
173 def test_tip_changeset_in_tags(self):
174 tip = self.repo.get_changeset()
174 tip = self.repo.get_changeset()
175 assert self.repo.tags['tip'] == tip.raw_id
175 assert self.repo.tags['tip'] == tip.raw_id
176
176
177 def test_initial_changeset(self):
177 def test_initial_changeset(self):
178
178
179 init_chset = self.repo.get_changeset(0)
179 init_chset = self.repo.get_changeset(0)
180 assert init_chset.message == 'initial import'
180 assert init_chset.message == 'initial import'
181 assert init_chset.author == 'Marcin Kuzminski <marcin@python-blog.com>'
181 assert init_chset.author == 'Marcin Kuzminski <marcin@python-blog.com>'
182 assert sorted(init_chset._file_paths) == sorted([
182 assert sorted(init_chset._file_paths) == sorted([
183 'vcs/__init__.py',
183 'vcs/__init__.py',
184 'vcs/backends/BaseRepository.py',
184 'vcs/backends/BaseRepository.py',
185 'vcs/backends/__init__.py',
185 'vcs/backends/__init__.py',
186 ])
186 ])
187
187
188 assert sorted(init_chset._dir_paths) == sorted(['', 'vcs', 'vcs/backends'])
188 assert sorted(init_chset._dir_paths) == sorted(['', 'vcs', 'vcs/backends'])
189
189
190 with pytest.raises(NodeDoesNotExistError):
190 with pytest.raises(NodeDoesNotExistError):
191 init_chset.get_node(path='foobar')
191 init_chset.get_node(path='foobar')
192
192
193 node = init_chset.get_node('vcs/')
193 node = init_chset.get_node('vcs/')
194 assert hasattr(node, 'kind')
194 assert hasattr(node, 'kind')
195 assert node.kind == NodeKind.DIR
195 assert node.kind == NodeKind.DIR
196
196
197 node = init_chset.get_node('vcs')
197 node = init_chset.get_node('vcs')
198 assert hasattr(node, 'kind')
198 assert hasattr(node, 'kind')
199 assert node.kind == NodeKind.DIR
199 assert node.kind == NodeKind.DIR
200
200
201 node = init_chset.get_node('vcs/__init__.py')
201 node = init_chset.get_node('vcs/__init__.py')
202 assert hasattr(node, 'kind')
202 assert hasattr(node, 'kind')
203 assert node.kind == NodeKind.FILE
203 assert node.kind == NodeKind.FILE
204
204
205 def test_not_existing_changeset(self):
205 def test_not_existing_changeset(self):
206 # rawid
206 # rawid
207 with pytest.raises(RepositoryError):
207 with pytest.raises(RepositoryError):
208 self.repo.get_changeset('abcd' * 10)
208 self.repo.get_changeset('abcd' * 10)
209 # shortid
209 # shortid
210 with pytest.raises(RepositoryError):
210 with pytest.raises(RepositoryError):
211 self.repo.get_changeset('erro' * 4)
211 self.repo.get_changeset('erro' * 4)
212 # numeric
212 # numeric
213 with pytest.raises(RepositoryError):
213 with pytest.raises(RepositoryError):
214 self.repo.get_changeset(self.repo.count() + 1)
214 self.repo.get_changeset(self.repo.count() + 1)
215
215
216 # Small chance we ever get to this one
216 # Small chance we ever get to this one
217 revision = pow(2, 30)
217 revision = pow(2, 30)
218 with pytest.raises(RepositoryError):
218 with pytest.raises(RepositoryError):
219 self.repo.get_changeset(revision)
219 self.repo.get_changeset(revision)
220
220
221 def test_changeset10(self):
221 def test_changeset10(self):
222
222
223 chset10 = self.repo.get_changeset(10)
223 chset10 = self.repo.get_changeset(10)
224 readme = """===
224 readme = """===
225 VCS
225 VCS
226 ===
226 ===
227
227
228 Various Version Control System management abstraction layer for Python.
228 Various Version Control System management abstraction layer for Python.
229
229
230 Introduction
230 Introduction
231 ------------
231 ------------
232
232
233 TODO: To be written...
233 TODO: To be written...
234
234
235 """
235 """
236 node = chset10.get_node('README.rst')
236 node = chset10.get_node('README.rst')
237 assert node.kind == NodeKind.FILE
237 assert node.kind == NodeKind.FILE
238 assert node.content == readme
238 assert node.content == readme
239
239
240 @mock.patch('kallithea.lib.vcs.backends.hg.repository.diffopts')
240 @mock.patch('kallithea.lib.vcs.backends.hg.repository.diffopts')
241 def test_get_diff_does_not_sanitize_zero_context(self, mock_diffopts):
241 def test_get_diff_does_not_sanitize_zero_context(self, mock_diffopts):
242 zero_context = 0
242 zero_context = 0
243
243
244 self.repo.get_diff(0, 1, 'foo', context=zero_context)
244 self.repo.get_diff(0, 1, 'foo', context=zero_context)
245
245
246 mock_diffopts.assert_called_once_with(git=True, showfunc=True, ignorews=False, context=zero_context)
246 mock_diffopts.assert_called_once_with(git=True, showfunc=True, ignorews=False, context=zero_context)
247
247
248 @mock.patch('kallithea.lib.vcs.backends.hg.repository.diffopts')
248 @mock.patch('kallithea.lib.vcs.backends.hg.repository.diffopts')
249 def test_get_diff_sanitizes_negative_context(self, mock_diffopts):
249 def test_get_diff_sanitizes_negative_context(self, mock_diffopts):
250 negative_context = -10
250 negative_context = -10
251 zero_context = 0
251 zero_context = 0
252
252
253 self.repo.get_diff(0, 1, 'foo', context=negative_context)
253 self.repo.get_diff(0, 1, 'foo', context=negative_context)
254
254
255 mock_diffopts.assert_called_once_with(git=True, showfunc=True, ignorews=False, context=zero_context)
255 mock_diffopts.assert_called_once_with(git=True, showfunc=True, ignorews=False, context=zero_context)
256
256
257
257
258 class TestMercurialChangeset(object):
258 class TestMercurialChangeset(object):
259
259
260 def setup_method(self):
260 def setup_method(self):
261 self.repo = MercurialRepository(safe_str(TEST_HG_REPO))
261 self.repo = MercurialRepository(safe_str(TEST_HG_REPO))
262
262
263 def _test_equality(self, changeset):
263 def _test_equality(self, changeset):
264 revision = changeset.revision
264 revision = changeset.revision
265 assert changeset == self.repo.get_changeset(revision)
265 assert changeset == self.repo.get_changeset(revision)
266
266
267 def test_equality(self):
267 def test_equality(self):
268 revs = [0, 10, 20]
268 revs = [0, 10, 20]
269 changesets = [self.repo.get_changeset(rev) for rev in revs]
269 changesets = [self.repo.get_changeset(rev) for rev in revs]
270 for changeset in changesets:
270 for changeset in changesets:
271 self._test_equality(changeset)
271 self._test_equality(changeset)
272
272
273 def test_default_changeset(self):
273 def test_default_changeset(self):
274 tip = self.repo.get_changeset('tip')
274 tip = self.repo.get_changeset('tip')
275 assert tip == self.repo.get_changeset()
275 assert tip == self.repo.get_changeset()
276 assert tip == self.repo.get_changeset(revision=None)
276 assert tip == self.repo.get_changeset(revision=None)
277 assert tip == list(self.repo[-1:])[0]
277 assert tip == list(self.repo[-1:])[0]
278
278
279 def test_root_node(self):
279 def test_root_node(self):
280 tip = self.repo.get_changeset('tip')
280 tip = self.repo.get_changeset('tip')
281 assert tip.root is tip.get_node('')
281 assert tip.root is tip.get_node('')
282
282
283 def test_lazy_fetch(self):
283 def test_lazy_fetch(self):
284 """
284 """
285 Test if changeset's nodes expands and are cached as we walk through
285 Test if changeset's nodes expands and are cached as we walk through
286 the revision. This test is somewhat hard to write as order of tests
286 the revision. This test is somewhat hard to write as order of tests
287 is a key here. Written by running command after command in a shell.
287 is a key here. Written by running command after command in a shell.
288 """
288 """
289 chset = self.repo.get_changeset(45)
289 chset = self.repo.get_changeset(45)
290 assert len(chset.nodes) == 0
290 assert len(chset.nodes) == 0
291 root = chset.root
291 root = chset.root
292 assert len(chset.nodes) == 1
292 assert len(chset.nodes) == 1
293 assert len(root.nodes) == 8
293 assert len(root.nodes) == 8
294 # accessing root.nodes updates chset.nodes
294 # accessing root.nodes updates chset.nodes
295 assert len(chset.nodes) == 9
295 assert len(chset.nodes) == 9
296
296
297 docs = root.get_node('docs')
297 docs = root.get_node('docs')
298 # we haven't yet accessed anything new as docs dir was already cached
298 # we haven't yet accessed anything new as docs dir was already cached
299 assert len(chset.nodes) == 9
299 assert len(chset.nodes) == 9
300 assert len(docs.nodes) == 8
300 assert len(docs.nodes) == 8
301 # accessing docs.nodes updates chset.nodes
301 # accessing docs.nodes updates chset.nodes
302 assert len(chset.nodes) == 17
302 assert len(chset.nodes) == 17
303
303
304 assert docs is chset.get_node('docs')
304 assert docs is chset.get_node('docs')
305 assert docs is root.nodes[0]
305 assert docs is root.nodes[0]
306 assert docs is root.dirs[0]
306 assert docs is root.dirs[0]
307 assert docs is chset.get_node('docs')
307 assert docs is chset.get_node('docs')
308
308
309 def test_nodes_with_changeset(self):
309 def test_nodes_with_changeset(self):
310 chset = self.repo.get_changeset(45)
310 chset = self.repo.get_changeset(45)
311 root = chset.root
311 root = chset.root
312 docs = root.get_node('docs')
312 docs = root.get_node('docs')
313 assert docs is chset.get_node('docs')
313 assert docs is chset.get_node('docs')
314 api = docs.get_node('api')
314 api = docs.get_node('api')
315 assert api is chset.get_node('docs/api')
315 assert api is chset.get_node('docs/api')
316 index = api.get_node('index.rst')
316 index = api.get_node('index.rst')
317 assert index is chset.get_node('docs/api/index.rst')
317 assert index is chset.get_node('docs/api/index.rst')
318 assert index is chset.get_node('docs').get_node('api').get_node('index.rst')
318 assert index is chset.get_node('docs').get_node('api').get_node('index.rst')
319
319
320 def test_branch_and_tags(self):
320 def test_branch_and_tags(self):
321 chset0 = self.repo.get_changeset(0)
321 chset0 = self.repo.get_changeset(0)
322 assert chset0.branch == 'default'
322 assert chset0.branch == 'default'
323 assert chset0.branches == ['default']
323 assert chset0.tags == []
324 assert chset0.tags == []
324
325
325 chset10 = self.repo.get_changeset(10)
326 chset10 = self.repo.get_changeset(10)
326 assert chset10.branch == 'default'
327 assert chset10.branch == 'default'
328 assert chset10.branches == ['default']
327 assert chset10.tags == []
329 assert chset10.tags == []
328
330
329 chset44 = self.repo.get_changeset(44)
331 chset44 = self.repo.get_changeset(44)
330 assert chset44.branch == 'web'
332 assert chset44.branch == 'web'
333 assert chset44.branches == ['web']
331
334
332 tip = self.repo.get_changeset('tip')
335 tip = self.repo.get_changeset('tip')
333 assert 'tip' in tip.tags
336 assert 'tip' in tip.tags
334
337
335 def _test_file_size(self, revision, path, size):
338 def _test_file_size(self, revision, path, size):
336 node = self.repo.get_changeset(revision).get_node(path)
339 node = self.repo.get_changeset(revision).get_node(path)
337 assert node.is_file()
340 assert node.is_file()
338 assert node.size == size
341 assert node.size == size
339
342
340 def test_file_size(self):
343 def test_file_size(self):
341 to_check = (
344 to_check = (
342 (10, 'setup.py', 1068),
345 (10, 'setup.py', 1068),
343 (20, 'setup.py', 1106),
346 (20, 'setup.py', 1106),
344 (60, 'setup.py', 1074),
347 (60, 'setup.py', 1074),
345
348
346 (10, 'vcs/backends/base.py', 2921),
349 (10, 'vcs/backends/base.py', 2921),
347 (20, 'vcs/backends/base.py', 3936),
350 (20, 'vcs/backends/base.py', 3936),
348 (60, 'vcs/backends/base.py', 6189),
351 (60, 'vcs/backends/base.py', 6189),
349 )
352 )
350 for revision, path, size in to_check:
353 for revision, path, size in to_check:
351 self._test_file_size(revision, path, size)
354 self._test_file_size(revision, path, size)
352
355
353 def _test_dir_size(self, revision, path, size):
356 def _test_dir_size(self, revision, path, size):
354 node = self.repo.get_changeset(revision).get_node(path)
357 node = self.repo.get_changeset(revision).get_node(path)
355 assert not node.is_file()
358 assert not node.is_file()
356 assert node.size == size
359 assert node.size == size
357
360
358 def test_dir_size(self):
361 def test_dir_size(self):
359 to_check = (
362 to_check = (
360 ('96507bd11ecc', '/', 682421),
363 ('96507bd11ecc', '/', 682421),
361 ('a53d9201d4bc', '/', 682410),
364 ('a53d9201d4bc', '/', 682410),
362 ('90243de06161', '/', 682006),
365 ('90243de06161', '/', 682006),
363 )
366 )
364 for revision, path, size in to_check:
367 for revision, path, size in to_check:
365 self._test_dir_size(revision, path, size)
368 self._test_dir_size(revision, path, size)
366
369
367 def test_repo_size(self):
370 def test_repo_size(self):
368 assert self.repo.size == 682421
371 assert self.repo.size == 682421
369
372
370 def test_file_history(self):
373 def test_file_history(self):
371 # we can only check if those revisions are present in the history
374 # we can only check if those revisions are present in the history
372 # as we cannot update this test every time file is changed
375 # as we cannot update this test every time file is changed
373 files = {
376 files = {
374 'setup.py': [7, 18, 45, 46, 47, 69, 77],
377 'setup.py': [7, 18, 45, 46, 47, 69, 77],
375 'vcs/nodes.py': [7, 8, 24, 26, 30, 45, 47, 49, 56, 57, 58, 59, 60,
378 'vcs/nodes.py': [7, 8, 24, 26, 30, 45, 47, 49, 56, 57, 58, 59, 60,
376 61, 73, 76],
379 61, 73, 76],
377 'vcs/backends/hg.py': [4, 5, 6, 11, 12, 13, 14, 15, 16, 21, 22, 23,
380 'vcs/backends/hg.py': [4, 5, 6, 11, 12, 13, 14, 15, 16, 21, 22, 23,
378 26, 27, 28, 30, 31, 33, 35, 36, 37, 38, 39, 40, 41, 44, 45, 47,
381 26, 27, 28, 30, 31, 33, 35, 36, 37, 38, 39, 40, 41, 44, 45, 47,
379 48, 49, 53, 54, 55, 58, 60, 61, 67, 68, 69, 70, 73, 77, 78, 79,
382 48, 49, 53, 54, 55, 58, 60, 61, 67, 68, 69, 70, 73, 77, 78, 79,
380 82],
383 82],
381 }
384 }
382 for path, revs in files.items():
385 for path, revs in files.items():
383 tip = self.repo.get_changeset(revs[-1])
386 tip = self.repo.get_changeset(revs[-1])
384 node = tip.get_node(path)
387 node = tip.get_node(path)
385 node_revs = [chset.revision for chset in node.history]
388 node_revs = [chset.revision for chset in node.history]
386 assert set(revs).issubset(set(node_revs)), \
389 assert set(revs).issubset(set(node_revs)), \
387 "We assumed that %s is subset of revisions for which file %s " \
390 "We assumed that %s is subset of revisions for which file %s " \
388 "has been changed, and history of that node returned: %s" \
391 "has been changed, and history of that node returned: %s" \
389 % (revs, path, node_revs)
392 % (revs, path, node_revs)
390
393
391 def test_file_annotate(self):
394 def test_file_annotate(self):
392 files = {
395 files = {
393 'vcs/backends/__init__.py':
396 'vcs/backends/__init__.py':
394 {89: {'lines_no': 31,
397 {89: {'lines_no': 31,
395 'changesets': [32, 32, 61, 32, 32, 37, 32, 32, 32, 44,
398 'changesets': [32, 32, 61, 32, 32, 37, 32, 32, 32, 44,
396 37, 37, 37, 37, 45, 37, 44, 37, 37, 37,
399 37, 37, 37, 37, 45, 37, 44, 37, 37, 37,
397 32, 32, 32, 32, 37, 32, 37, 37, 32,
400 32, 32, 32, 32, 37, 32, 37, 37, 32,
398 32, 32]},
401 32, 32]},
399 20: {'lines_no': 1,
402 20: {'lines_no': 1,
400 'changesets': [4]},
403 'changesets': [4]},
401 55: {'lines_no': 31,
404 55: {'lines_no': 31,
402 'changesets': [32, 32, 45, 32, 32, 37, 32, 32, 32, 44,
405 'changesets': [32, 32, 45, 32, 32, 37, 32, 32, 32, 44,
403 37, 37, 37, 37, 45, 37, 44, 37, 37, 37,
406 37, 37, 37, 37, 45, 37, 44, 37, 37, 37,
404 32, 32, 32, 32, 37, 32, 37, 37, 32,
407 32, 32, 32, 32, 37, 32, 37, 37, 32,
405 32, 32]}},
408 32, 32]}},
406 'vcs/exceptions.py':
409 'vcs/exceptions.py':
407 {89: {'lines_no': 18,
410 {89: {'lines_no': 18,
408 'changesets': [16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
411 'changesets': [16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
409 16, 16, 17, 16, 16, 18, 18, 18]},
412 16, 16, 17, 16, 16, 18, 18, 18]},
410 20: {'lines_no': 18,
413 20: {'lines_no': 18,
411 'changesets': [16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
414 'changesets': [16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
412 16, 16, 17, 16, 16, 18, 18, 18]},
415 16, 16, 17, 16, 16, 18, 18, 18]},
413 55: {'lines_no': 18, 'changesets': [16, 16, 16, 16, 16, 16,
416 55: {'lines_no': 18, 'changesets': [16, 16, 16, 16, 16, 16,
414 16, 16, 16, 16, 16, 16,
417 16, 16, 16, 16, 16, 16,
415 17, 16, 16, 18, 18, 18]}},
418 17, 16, 16, 18, 18, 18]}},
416 'MANIFEST.in': {89: {'lines_no': 5,
419 'MANIFEST.in': {89: {'lines_no': 5,
417 'changesets': [7, 7, 7, 71, 71]},
420 'changesets': [7, 7, 7, 71, 71]},
418 20: {'lines_no': 3,
421 20: {'lines_no': 3,
419 'changesets': [7, 7, 7]},
422 'changesets': [7, 7, 7]},
420 55: {'lines_no': 3,
423 55: {'lines_no': 3,
421 'changesets': [7, 7, 7]}}}
424 'changesets': [7, 7, 7]}}}
422
425
423 for fname, revision_dict in files.items():
426 for fname, revision_dict in files.items():
424 for rev, data in revision_dict.items():
427 for rev, data in revision_dict.items():
425 cs = self.repo.get_changeset(rev)
428 cs = self.repo.get_changeset(rev)
426 l1_1 = [x[1] for x in cs.get_file_annotate(fname)]
429 l1_1 = [x[1] for x in cs.get_file_annotate(fname)]
427 l1_2 = [x[2]().raw_id for x in cs.get_file_annotate(fname)]
430 l1_2 = [x[2]().raw_id for x in cs.get_file_annotate(fname)]
428 assert l1_1 == l1_2
431 assert l1_1 == l1_2
429 l1 = l1_2 = [x[2]().revision for x in cs.get_file_annotate(fname)]
432 l1 = l1_2 = [x[2]().revision for x in cs.get_file_annotate(fname)]
430 l2 = files[fname][rev]['changesets']
433 l2 = files[fname][rev]['changesets']
431 assert l1 == l2, "The lists of revision for %s@rev%s" \
434 assert l1 == l2, "The lists of revision for %s@rev%s" \
432 "from annotation list should match each other," \
435 "from annotation list should match each other," \
433 "got \n%s \nvs \n%s " % (fname, rev, l1, l2)
436 "got \n%s \nvs \n%s " % (fname, rev, l1, l2)
434
437
435 def test_changeset_state(self):
438 def test_changeset_state(self):
436 """
439 """
437 Tests which files have been added/changed/removed at particular revision
440 Tests which files have been added/changed/removed at particular revision
438 """
441 """
439
442
440 # rev 46ad32a4f974:
443 # rev 46ad32a4f974:
441 # hg st --rev 46ad32a4f974
444 # hg st --rev 46ad32a4f974
442 # changed: 13
445 # changed: 13
443 # added: 20
446 # added: 20
444 # removed: 1
447 # removed: 1
445 changed = set(['.hgignore'
448 changed = set(['.hgignore'
446 , 'README.rst' , 'docs/conf.py' , 'docs/index.rst' , 'setup.py'
449 , 'README.rst' , 'docs/conf.py' , 'docs/index.rst' , 'setup.py'
447 , 'tests/test_hg.py' , 'tests/test_nodes.py' , 'vcs/__init__.py'
450 , 'tests/test_hg.py' , 'tests/test_nodes.py' , 'vcs/__init__.py'
448 , 'vcs/backends/__init__.py' , 'vcs/backends/base.py'
451 , 'vcs/backends/__init__.py' , 'vcs/backends/base.py'
449 , 'vcs/backends/hg.py' , 'vcs/nodes.py' , 'vcs/utils/__init__.py'])
452 , 'vcs/backends/hg.py' , 'vcs/nodes.py' , 'vcs/utils/__init__.py'])
450
453
451 added = set(['docs/api/backends/hg.rst'
454 added = set(['docs/api/backends/hg.rst'
452 , 'docs/api/backends/index.rst' , 'docs/api/index.rst'
455 , 'docs/api/backends/index.rst' , 'docs/api/index.rst'
453 , 'docs/api/nodes.rst' , 'docs/api/web/index.rst'
456 , 'docs/api/nodes.rst' , 'docs/api/web/index.rst'
454 , 'docs/api/web/simplevcs.rst' , 'docs/installation.rst'
457 , 'docs/api/web/simplevcs.rst' , 'docs/installation.rst'
455 , 'docs/quickstart.rst' , 'setup.cfg' , 'vcs/utils/baseui_config.py'
458 , 'docs/quickstart.rst' , 'setup.cfg' , 'vcs/utils/baseui_config.py'
456 , 'vcs/utils/web.py' , 'vcs/web/__init__.py' , 'vcs/web/exceptions.py'
459 , 'vcs/utils/web.py' , 'vcs/web/__init__.py' , 'vcs/web/exceptions.py'
457 , 'vcs/web/simplevcs/__init__.py' , 'vcs/web/simplevcs/exceptions.py'
460 , 'vcs/web/simplevcs/__init__.py' , 'vcs/web/simplevcs/exceptions.py'
458 , 'vcs/web/simplevcs/middleware.py' , 'vcs/web/simplevcs/models.py'
461 , 'vcs/web/simplevcs/middleware.py' , 'vcs/web/simplevcs/models.py'
459 , 'vcs/web/simplevcs/settings.py' , 'vcs/web/simplevcs/utils.py'
462 , 'vcs/web/simplevcs/settings.py' , 'vcs/web/simplevcs/utils.py'
460 , 'vcs/web/simplevcs/views.py'])
463 , 'vcs/web/simplevcs/views.py'])
461
464
462 removed = set(['docs/api.rst'])
465 removed = set(['docs/api.rst'])
463
466
464 chset64 = self.repo.get_changeset('46ad32a4f974')
467 chset64 = self.repo.get_changeset('46ad32a4f974')
465 assert set((node.path for node in chset64.added)) == added
468 assert set((node.path for node in chset64.added)) == added
466 assert set((node.path for node in chset64.changed)) == changed
469 assert set((node.path for node in chset64.changed)) == changed
467 assert set((node.path for node in chset64.removed)) == removed
470 assert set((node.path for node in chset64.removed)) == removed
468
471
469 # rev b090f22d27d6:
472 # rev b090f22d27d6:
470 # hg st --rev b090f22d27d6
473 # hg st --rev b090f22d27d6
471 # changed: 13
474 # changed: 13
472 # added: 20
475 # added: 20
473 # removed: 1
476 # removed: 1
474 chset88 = self.repo.get_changeset('b090f22d27d6')
477 chset88 = self.repo.get_changeset('b090f22d27d6')
475 assert set((node.path for node in chset88.added)) == set()
478 assert set((node.path for node in chset88.added)) == set()
476 assert set((node.path for node in chset88.changed)) == set(['.hgignore'])
479 assert set((node.path for node in chset88.changed)) == set(['.hgignore'])
477 assert set((node.path for node in chset88.removed)) == set()
480 assert set((node.path for node in chset88.removed)) == set()
478
481
479 # 85:
482 # 85:
480 # added: 2 ['vcs/utils/diffs.py', 'vcs/web/simplevcs/views/diffs.py']
483 # added: 2 ['vcs/utils/diffs.py', 'vcs/web/simplevcs/views/diffs.py']
481 # changed: 4 ['vcs/web/simplevcs/models.py', ...]
484 # changed: 4 ['vcs/web/simplevcs/models.py', ...]
482 # removed: 1 ['vcs/utils/web.py']
485 # removed: 1 ['vcs/utils/web.py']
483 chset85 = self.repo.get_changeset(85)
486 chset85 = self.repo.get_changeset(85)
484 assert set((node.path for node in chset85.added)) == set([
487 assert set((node.path for node in chset85.added)) == set([
485 'vcs/utils/diffs.py',
488 'vcs/utils/diffs.py',
486 'vcs/web/simplevcs/views/diffs.py'
489 'vcs/web/simplevcs/views/diffs.py'
487 ])
490 ])
488
491
489 assert set((node.path for node in chset85.changed)) == set([
492 assert set((node.path for node in chset85.changed)) == set([
490 'vcs/web/simplevcs/models.py',
493 'vcs/web/simplevcs/models.py',
491 'vcs/web/simplevcs/utils.py',
494 'vcs/web/simplevcs/utils.py',
492 'vcs/web/simplevcs/views/__init__.py',
495 'vcs/web/simplevcs/views/__init__.py',
493 'vcs/web/simplevcs/views/repository.py',
496 'vcs/web/simplevcs/views/repository.py',
494 ])
497 ])
495
498
496 assert set((node.path for node in chset85.removed)) == set([
499 assert set((node.path for node in chset85.removed)) == set([
497 'vcs/utils/web.py'
500 'vcs/utils/web.py'
498 ])
501 ])
499
502
500
503
501 def test_files_state(self):
504 def test_files_state(self):
502 """
505 """
503 Tests state of FileNodes.
506 Tests state of FileNodes.
504 """
507 """
505 chset = self.repo.get_changeset(85)
508 chset = self.repo.get_changeset(85)
506 node = chset.get_node('vcs/utils/diffs.py')
509 node = chset.get_node('vcs/utils/diffs.py')
507 assert node.state, NodeState.ADDED
510 assert node.state, NodeState.ADDED
508 assert node.added
511 assert node.added
509 assert not node.changed
512 assert not node.changed
510 assert not node.not_changed
513 assert not node.not_changed
511 assert not node.removed
514 assert not node.removed
512
515
513 chset = self.repo.get_changeset(88)
516 chset = self.repo.get_changeset(88)
514 node = chset.get_node('.hgignore')
517 node = chset.get_node('.hgignore')
515 assert node.state, NodeState.CHANGED
518 assert node.state, NodeState.CHANGED
516 assert not node.added
519 assert not node.added
517 assert node.changed
520 assert node.changed
518 assert not node.not_changed
521 assert not node.not_changed
519 assert not node.removed
522 assert not node.removed
520
523
521 chset = self.repo.get_changeset(85)
524 chset = self.repo.get_changeset(85)
522 node = chset.get_node('setup.py')
525 node = chset.get_node('setup.py')
523 assert node.state, NodeState.NOT_CHANGED
526 assert node.state, NodeState.NOT_CHANGED
524 assert not node.added
527 assert not node.added
525 assert not node.changed
528 assert not node.changed
526 assert node.not_changed
529 assert node.not_changed
527 assert not node.removed
530 assert not node.removed
528
531
529 # If node has REMOVED state then trying to fetch it would raise
532 # If node has REMOVED state then trying to fetch it would raise
530 # ChangesetError exception
533 # ChangesetError exception
531 chset = self.repo.get_changeset(2)
534 chset = self.repo.get_changeset(2)
532 path = 'vcs/backends/BaseRepository.py'
535 path = 'vcs/backends/BaseRepository.py'
533 with pytest.raises(NodeDoesNotExistError):
536 with pytest.raises(NodeDoesNotExistError):
534 chset.get_node(path)
537 chset.get_node(path)
535 # but it would be one of ``removed`` (changeset's attribute)
538 # but it would be one of ``removed`` (changeset's attribute)
536 assert path in [rf.path for rf in chset.removed]
539 assert path in [rf.path for rf in chset.removed]
537
540
538 def test_commit_message_is_unicode(self):
541 def test_commit_message_is_unicode(self):
539 for cm in self.repo:
542 for cm in self.repo:
540 assert type(cm.message) == unicode
543 assert type(cm.message) == unicode
541
544
542 def test_changeset_author_is_unicode(self):
545 def test_changeset_author_is_unicode(self):
543 for cm in self.repo:
546 for cm in self.repo:
544 assert type(cm.author) == unicode
547 assert type(cm.author) == unicode
545
548
546 def test_repo_files_content_is_unicode(self):
549 def test_repo_files_content_is_unicode(self):
547 test_changeset = self.repo.get_changeset(100)
550 test_changeset = self.repo.get_changeset(100)
548 for node in test_changeset.get_node('/'):
551 for node in test_changeset.get_node('/'):
549 if node.is_file():
552 if node.is_file():
550 assert type(node.content) == unicode
553 assert type(node.content) == unicode
551
554
552 def test_wrong_path(self):
555 def test_wrong_path(self):
553 # There is 'setup.py' in the root dir but not there:
556 # There is 'setup.py' in the root dir but not there:
554 path = 'foo/bar/setup.py'
557 path = 'foo/bar/setup.py'
555 with pytest.raises(VCSError):
558 with pytest.raises(VCSError):
556 self.repo.get_changeset().get_node(path)
559 self.repo.get_changeset().get_node(path)
557
560
558 def test_archival_file(self):
561 def test_archival_file(self):
559 # TODO:
562 # TODO:
560 pass
563 pass
561
564
562 def test_archival_as_generator(self):
565 def test_archival_as_generator(self):
563 # TODO:
566 # TODO:
564 pass
567 pass
565
568
566 def test_archival_wrong_kind(self):
569 def test_archival_wrong_kind(self):
567 tip = self.repo.get_changeset()
570 tip = self.repo.get_changeset()
568 with pytest.raises(VCSError):
571 with pytest.raises(VCSError):
569 tip.fill_archive(kind='error')
572 tip.fill_archive(kind='error')
570
573
571 def test_archival_empty_prefix(self):
574 def test_archival_empty_prefix(self):
572 # TODO:
575 # TODO:
573 pass
576 pass
574
577
575 def test_author_email(self):
578 def test_author_email(self):
576 assert 'marcin@python-blog.com' == self.repo.get_changeset('b986218ba1c9').author_email
579 assert 'marcin@python-blog.com' == self.repo.get_changeset('b986218ba1c9').author_email
577 assert 'lukasz.balcerzak@python-center.pl' == self.repo.get_changeset('3803844fdbd3').author_email
580 assert 'lukasz.balcerzak@python-center.pl' == self.repo.get_changeset('3803844fdbd3').author_email
578 assert '' == self.repo.get_changeset('84478366594b').author_email
581 assert '' == self.repo.get_changeset('84478366594b').author_email
579
582
580 def test_author_username(self):
583 def test_author_username(self):
581 assert 'Marcin Kuzminski' == self.repo.get_changeset('b986218ba1c9').author_name
584 assert 'Marcin Kuzminski' == self.repo.get_changeset('b986218ba1c9').author_name
582 assert 'Lukasz Balcerzak' == self.repo.get_changeset('3803844fdbd3').author_name
585 assert 'Lukasz Balcerzak' == self.repo.get_changeset('3803844fdbd3').author_name
583 assert 'marcink' == self.repo.get_changeset('84478366594b').author_name
586 assert 'marcink' == self.repo.get_changeset('84478366594b').author_name
General Comments 0
You need to be logged in to leave comments. Login now