##// END OF EJS Templates
Multiple changes for compare system...
marcink -
r3015:16af2498 beta
parent child Browse files
Show More
@@ -1,161 +1,167
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.compare
3 rhodecode.controllers.compare
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 compare controller for pylons showoing differences between two
6 compare controller for pylons showoing differences between two
7 repos, branches, bookmarks or tips
7 repos, branches, bookmarks or tips
8
8
9 :created_on: May 6, 2012
9 :created_on: May 6, 2012
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
12 :license: GPLv3, see COPYING for more details.
13 """
13 """
14 # This program is free software: you can redistribute it and/or modify
14 # This program is free software: you can redistribute it and/or modify
15 # it under the terms of the GNU General Public License as published by
15 # it under the terms of the GNU General Public License as published by
16 # the Free Software Foundation, either version 3 of the License, or
16 # the Free Software Foundation, either version 3 of the License, or
17 # (at your option) any later version.
17 # (at your option) any later version.
18 #
18 #
19 # This program is distributed in the hope that it will be useful,
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
22 # GNU General Public License for more details.
23 #
23 #
24 # You should have received a copy of the GNU General Public License
24 # You should have received a copy of the GNU General Public License
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 import logging
26 import logging
27 import traceback
27 import traceback
28
28
29 from webob.exc import HTTPNotFound
29 from webob.exc import HTTPNotFound
30 from pylons import request, response, session, tmpl_context as c, url
30 from pylons import request, response, session, tmpl_context as c, url
31 from pylons.controllers.util import abort, redirect
31 from pylons.controllers.util import abort, redirect
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33
33
34 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError, RepositoryError
34 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError, RepositoryError
35 from rhodecode.lib import helpers as h
35 from rhodecode.lib import helpers as h
36 from rhodecode.lib.base import BaseRepoController, render
36 from rhodecode.lib.base import BaseRepoController, render
37 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
37 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
38 from rhodecode.lib import diffs
38 from rhodecode.lib import diffs
39
39
40 from rhodecode.model.db import Repository
40 from rhodecode.model.db import Repository
41 from rhodecode.model.pull_request import PullRequestModel
41 from rhodecode.model.pull_request import PullRequestModel
42 from webob.exc import HTTPBadRequest
42 from webob.exc import HTTPBadRequest
43 from rhodecode.lib.utils2 import str2bool
43 from rhodecode.lib.utils2 import str2bool
44 from rhodecode.lib.diffs import LimitedDiffContainer
44 from rhodecode.lib.diffs import LimitedDiffContainer
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 class CompareController(BaseRepoController):
49 class CompareController(BaseRepoController):
50
50
51 @LoginRequired()
51 @LoginRequired()
52 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
52 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
53 'repository.admin')
53 'repository.admin')
54 def __before__(self):
54 def __before__(self):
55 super(CompareController, self).__before__()
55 super(CompareController, self).__before__()
56
56
57 def __get_cs_or_redirect(self, rev, repo, redirect_after=True,
57 def __get_cs_or_redirect(self, rev, repo, redirect_after=True,
58 partial=False):
58 partial=False):
59 """
59 """
60 Safe way to get changeset if error occur it redirects to changeset with
60 Safe way to get changeset if error occur it redirects to changeset with
61 proper message. If partial is set then don't do redirect raise Exception
61 proper message. If partial is set then don't do redirect raise Exception
62 instead
62 instead
63
63
64 :param rev: revision to fetch
64 :param rev: revision to fetch
65 :param repo: repo instance
65 :param repo: repo instance
66 """
66 """
67
67
68 try:
68 try:
69 type_, rev = rev
69 type_, rev = rev
70 return repo.scm_instance.get_changeset(rev)
70 return repo.scm_instance.get_changeset(rev)
71 except EmptyRepositoryError, e:
71 except EmptyRepositoryError, e:
72 if not redirect_after:
72 if not redirect_after:
73 return None
73 return None
74 h.flash(h.literal(_('There are no changesets yet')),
74 h.flash(h.literal(_('There are no changesets yet')),
75 category='warning')
75 category='warning')
76 redirect(url('summary_home', repo_name=repo.repo_name))
76 redirect(url('summary_home', repo_name=repo.repo_name))
77
77
78 except RepositoryError, e:
78 except RepositoryError, e:
79 log.error(traceback.format_exc())
79 log.error(traceback.format_exc())
80 h.flash(str(e), category='warning')
80 h.flash(str(e), category='warning')
81 if not partial:
81 if not partial:
82 redirect(h.url('summary_home', repo_name=repo.repo_name))
82 redirect(h.url('summary_home', repo_name=repo.repo_name))
83 raise HTTPBadRequest()
83 raise HTTPBadRequest()
84
84
85 def index(self, org_ref_type, org_ref, other_ref_type, other_ref):
85 def index(self, org_ref_type, org_ref, other_ref_type, other_ref):
86
86
87 org_repo = c.rhodecode_db_repo.repo_name
87 org_repo = c.rhodecode_db_repo.repo_name
88 org_ref = (org_ref_type, org_ref)
88 org_ref = (org_ref_type, org_ref)
89 other_ref = (other_ref_type, other_ref)
89 other_ref = (other_ref_type, other_ref)
90 other_repo = request.GET.get('repo', org_repo)
90 other_repo = request.GET.get('repo', org_repo)
91 bundle_compare = str2bool(request.GET.get('bundle', True))
91 remote_compare = str2bool(request.GET.get('bundle', True))
92 c.fulldiff = fulldiff = request.GET.get('fulldiff')
92 c.fulldiff = fulldiff = request.GET.get('fulldiff')
93
93
94 c.swap_url = h.url('compare_url', repo_name=other_repo,
94 c.swap_url = h.url('compare_url', repo_name=other_repo,
95 org_ref_type=other_ref[0], org_ref=other_ref[1],
95 org_ref_type=other_ref[0], org_ref=other_ref[1],
96 other_ref_type=org_ref[0], other_ref=org_ref[1],
96 other_ref_type=org_ref[0], other_ref=org_ref[1],
97 repo=org_repo, as_form=request.GET.get('as_form'),
97 repo=org_repo, as_form=request.GET.get('as_form'),
98 bundle=bundle_compare)
98 bundle=remote_compare)
99
99
100 c.org_repo = org_repo = Repository.get_by_repo_name(org_repo)
100 c.org_repo = org_repo = Repository.get_by_repo_name(org_repo)
101 c.other_repo = other_repo = Repository.get_by_repo_name(other_repo)
101 c.other_repo = other_repo = Repository.get_by_repo_name(other_repo)
102
102
103 if c.org_repo is None or c.other_repo is None:
103 if c.org_repo is None or c.other_repo is None:
104 log.error('Could not found repo %s or %s' % (org_repo, other_repo))
104 log.error('Could not found repo %s or %s' % (org_repo, other_repo))
105 raise HTTPNotFound
105 raise HTTPNotFound
106
106
107 if c.org_repo != c.other_repo and h.is_git(c.rhodecode_repo):
107 if c.org_repo != c.other_repo and h.is_git(c.rhodecode_repo):
108 log.error('compare of two remote repos not available for GIT REPOS')
108 log.error('compare of two remote repos not available for GIT REPOS')
109 raise HTTPNotFound
109 raise HTTPNotFound
110
110
111 if c.org_repo.scm_instance.alias != c.other_repo.scm_instance.alias:
111 if c.org_repo.scm_instance.alias != c.other_repo.scm_instance.alias:
112 log.error('compare of two different kind of remote repos not available')
112 log.error('compare of two different kind of remote repos not available')
113 raise HTTPNotFound
113 raise HTTPNotFound
114
114
115 partial = request.environ.get('HTTP_X_PARTIAL_XHR')
115 partial = request.environ.get('HTTP_X_PARTIAL_XHR')
116 self.__get_cs_or_redirect(rev=org_ref, repo=org_repo, partial=partial)
116 self.__get_cs_or_redirect(rev=org_ref, repo=org_repo, partial=partial)
117 self.__get_cs_or_redirect(rev=other_ref, repo=other_repo, partial=partial)
117 self.__get_cs_or_redirect(rev=other_ref, repo=other_repo, partial=partial)
118
118
119 c.cs_ranges, discovery_data = PullRequestModel().get_compare_data(
119 c.cs_ranges, discovery_data = PullRequestModel().get_compare_data(
120 org_repo, org_ref, other_repo, other_ref
120 org_repo, org_ref, other_repo, other_ref
121 )
121 )
122
122
123 c.statuses = c.rhodecode_db_repo.statuses([x.raw_id for x in
123 c.statuses = c.rhodecode_db_repo.statuses([x.raw_id for x in
124 c.cs_ranges])
124 c.cs_ranges])
125 c.target_repo = c.repo_name
125 c.target_repo = c.repo_name
126 # defines that we need hidden inputs with changesets
126 # defines that we need hidden inputs with changesets
127 c.as_form = request.GET.get('as_form', False)
127 c.as_form = request.GET.get('as_form', False)
128 if partial:
128 if partial:
129 return render('compare/compare_cs.html')
129 return render('compare/compare_cs.html')
130
130
131 if not bundle_compare and c.cs_ranges:
131 c.org_ref = org_ref[1]
132 c.other_ref = other_ref[1]
133
134 if not remote_compare and c.cs_ranges:
132 # case we want a simple diff without incoming changesets, just
135 # case we want a simple diff without incoming changesets, just
133 # for review purposes. Make the diff on the forked repo, with
136 # for review purposes. Make the diff on the forked repo, with
134 # revision that is common ancestor
137 # revision that is common ancestor
135 other_ref = ('rev', c.cs_ranges[-1].parents[0].raw_id)
138 other_ref = ('rev', c.cs_ranges[-1].parents[0].raw_id)
136 other_repo = org_repo
139 other_repo = org_repo
137
140
138 c.org_ref = org_ref[1]
141 diff_limit = self.cut_off_limit if not fulldiff else None
139 c.other_ref = other_ref[1]
142 _diff = diffs.differ(org_repo, org_ref, other_repo, other_ref,
143 discovery_data, remote_compare=remote_compare)
140
144
141 _diff = diffs.differ(other_repo, other_ref, org_repo, org_ref,
145 diff_processor = diffs.DiffProcessor(_diff or '', format='gitdiff',
142 discovery_data, bundle_compare=bundle_compare)
143 diff_limit = self.cut_off_limit if not fulldiff else None
144 diff_processor = diffs.DiffProcessor(_diff, format='gitdiff',
145 diff_limit=diff_limit)
146 diff_limit=diff_limit)
146 _parsed = diff_processor.prepare()
147 _parsed = diff_processor.prepare()
147
148
148 c.limited_diff = False
149 c.limited_diff = False
149 if isinstance(_parsed, LimitedDiffContainer):
150 if isinstance(_parsed, LimitedDiffContainer):
150 c.limited_diff = True
151 c.limited_diff = True
151
152
152 c.files = []
153 c.files = []
153 c.changes = {}
154 c.changes = {}
154
155 c.lines_added = 0
156 c.lines_deleted = 0
155 for f in _parsed:
157 for f in _parsed:
158 st = f['stats']
159 if st[0] != 'b':
160 c.lines_added += st[0]
161 c.lines_deleted += st[1]
156 fid = h.FID('', f['filename'])
162 fid = h.FID('', f['filename'])
157 c.files.append([fid, f['operation'], f['filename'], f['stats']])
163 c.files.append([fid, f['operation'], f['filename'], f['stats']])
158 diff = diff_processor.as_html(enable_comments=False, parsed_lines=[f])
164 diff = diff_processor.as_html(enable_comments=False, parsed_lines=[f])
159 c.changes[fid] = [f['operation'], f['filename'], diff]
165 c.changes[fid] = [f['operation'], f['filename'], diff]
160
166
161 return render('compare/compare_diff.html')
167 return render('compare/compare_diff.html')
@@ -1,446 +1,449
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.pullrequests
3 rhodecode.controllers.pullrequests
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 pull requests controller for rhodecode for initializing pull requests
6 pull requests controller for rhodecode for initializing pull requests
7
7
8 :created_on: May 7, 2012
8 :created_on: May 7, 2012
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import formencode
27 import formencode
28
28
29 from webob.exc import HTTPNotFound, HTTPForbidden
29 from webob.exc import HTTPNotFound, HTTPForbidden
30 from collections import defaultdict
30 from collections import defaultdict
31 from itertools import groupby
31 from itertools import groupby
32
32
33 from pylons import request, response, session, tmpl_context as c, url
33 from pylons import request, response, session, tmpl_context as c, url
34 from pylons.controllers.util import abort, redirect
34 from pylons.controllers.util import abort, redirect
35 from pylons.i18n.translation import _
35 from pylons.i18n.translation import _
36 from pylons.decorators import jsonify
36 from pylons.decorators import jsonify
37
37
38 from rhodecode.lib.compat import json
38 from rhodecode.lib.compat import json
39 from rhodecode.lib.base import BaseRepoController, render
39 from rhodecode.lib.base import BaseRepoController, render
40 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
40 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
41 NotAnonymous
41 NotAnonymous
42 from rhodecode.lib import helpers as h
42 from rhodecode.lib import helpers as h
43 from rhodecode.lib import diffs
43 from rhodecode.lib import diffs
44 from rhodecode.lib.utils import action_logger
44 from rhodecode.lib.utils import action_logger
45 from rhodecode.model.db import User, PullRequest, ChangesetStatus,\
45 from rhodecode.model.db import User, PullRequest, ChangesetStatus,\
46 ChangesetComment
46 ChangesetComment
47 from rhodecode.model.pull_request import PullRequestModel
47 from rhodecode.model.pull_request import PullRequestModel
48 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
49 from rhodecode.model.repo import RepoModel
49 from rhodecode.model.repo import RepoModel
50 from rhodecode.model.comment import ChangesetCommentsModel
50 from rhodecode.model.comment import ChangesetCommentsModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
52 from rhodecode.model.forms import PullRequestForm
52 from rhodecode.model.forms import PullRequestForm
53 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
53 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
54 from rhodecode.lib.vcs.backends.base import EmptyChangeset
54
55
55 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
56
57
57
58
58 class PullrequestsController(BaseRepoController):
59 class PullrequestsController(BaseRepoController):
59
60
60 @LoginRequired()
61 @LoginRequired()
61 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
62 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
62 'repository.admin')
63 'repository.admin')
63 def __before__(self):
64 def __before__(self):
64 super(PullrequestsController, self).__before__()
65 super(PullrequestsController, self).__before__()
65 repo_model = RepoModel()
66 repo_model = RepoModel()
66 c.users_array = repo_model.get_users_js()
67 c.users_array = repo_model.get_users_js()
67 c.users_groups_array = repo_model.get_users_groups_js()
68 c.users_groups_array = repo_model.get_users_groups_js()
68
69
69 def _get_repo_refs(self, repo):
70 def _get_repo_refs(self, repo):
70 hist_l = []
71 hist_l = []
71
72
72 branches_group = ([('branch:%s:%s' % (k, v), k) for
73 branches_group = ([('branch:%s:%s' % (k, v), k) for
73 k, v in repo.branches.iteritems()], _("Branches"))
74 k, v in repo.branches.iteritems()], _("Branches"))
74 bookmarks_group = ([('book:%s:%s' % (k, v), k) for
75 bookmarks_group = ([('book:%s:%s' % (k, v), k) for
75 k, v in repo.bookmarks.iteritems()], _("Bookmarks"))
76 k, v in repo.bookmarks.iteritems()], _("Bookmarks"))
76 tags_group = ([('tag:%s:%s' % (k, v), k) for
77 tags_group = ([('tag:%s:%s' % (k, v), k) for
77 k, v in repo.tags.iteritems()], _("Tags"))
78 k, v in repo.tags.iteritems()], _("Tags"))
78
79
79 hist_l.append(bookmarks_group)
80 hist_l.append(bookmarks_group)
80 hist_l.append(branches_group)
81 hist_l.append(branches_group)
81 hist_l.append(tags_group)
82 hist_l.append(tags_group)
82
83
83 return hist_l
84 return hist_l
84
85
85 def _get_default_rev(self, repo):
86 def _get_default_rev(self, repo):
86 """
87 """
87 Get's default revision to do compare on pull request
88 Get's default revision to do compare on pull request
88
89
89 :param repo:
90 :param repo:
90 """
91 """
91 repo = repo.scm_instance
92 repo = repo.scm_instance
92 if 'default' in repo.branches:
93 if 'default' in repo.branches:
93 return 'default'
94 return 'default'
94 else:
95 else:
95 #if repo doesn't have default branch return first found
96 #if repo doesn't have default branch return first found
96 return repo.branches.keys()[0]
97 return repo.branches.keys()[0]
97
98
98 def show_all(self, repo_name):
99 def show_all(self, repo_name):
99 c.pull_requests = PullRequestModel().get_all(repo_name)
100 c.pull_requests = PullRequestModel().get_all(repo_name)
100 c.repo_name = repo_name
101 c.repo_name = repo_name
101 return render('/pullrequests/pullrequest_show_all.html')
102 return render('/pullrequests/pullrequest_show_all.html')
102
103
103 @NotAnonymous()
104 @NotAnonymous()
104 def index(self):
105 def index(self):
105 org_repo = c.rhodecode_db_repo
106 org_repo = c.rhodecode_db_repo
106
107
107 if org_repo.scm_instance.alias != 'hg':
108 if org_repo.scm_instance.alias != 'hg':
108 log.error('Review not available for GIT REPOS')
109 log.error('Review not available for GIT REPOS')
109 raise HTTPNotFound
110 raise HTTPNotFound
110
111
111 try:
112 try:
112 org_repo.scm_instance.get_changeset()
113 org_repo.scm_instance.get_changeset()
113 except EmptyRepositoryError, e:
114 except EmptyRepositoryError, e:
114 h.flash(h.literal(_('There are no changesets yet')),
115 h.flash(h.literal(_('There are no changesets yet')),
115 category='warning')
116 category='warning')
116 redirect(url('summary_home', repo_name=org_repo.repo_name))
117 redirect(url('summary_home', repo_name=org_repo.repo_name))
117
118
118 other_repos_info = {}
119 other_repos_info = {}
119
120
120 c.org_refs = self._get_repo_refs(c.rhodecode_repo)
121 c.org_refs = self._get_repo_refs(c.rhodecode_repo)
121 c.org_repos = []
122 c.org_repos = []
122 c.other_repos = []
123 c.other_repos = []
123 c.org_repos.append((org_repo.repo_name, '%s/%s' % (
124 c.org_repos.append((org_repo.repo_name, '%s/%s' % (
124 org_repo.user.username, c.repo_name))
125 org_repo.user.username, c.repo_name))
125 )
126 )
126
127
127 # add org repo to other so we can open pull request agains itself
128 # add org repo to other so we can open pull request agains itself
128 c.other_repos.extend(c.org_repos)
129 c.other_repos.extend(c.org_repos)
129
130
130 c.default_pull_request = org_repo.repo_name # repo name pre-selected
131 c.default_pull_request = org_repo.repo_name # repo name pre-selected
131 c.default_pull_request_rev = self._get_default_rev(org_repo) # revision pre-selected
132 c.default_pull_request_rev = self._get_default_rev(org_repo) # revision pre-selected
132 c.default_revs = self._get_repo_refs(org_repo.scm_instance)
133 c.default_revs = self._get_repo_refs(org_repo.scm_instance)
133 #add orginal repo
134 #add orginal repo
134 other_repos_info[org_repo.repo_name] = {
135 other_repos_info[org_repo.repo_name] = {
135 'gravatar': h.gravatar_url(org_repo.user.email, 24),
136 'gravatar': h.gravatar_url(org_repo.user.email, 24),
136 'description': org_repo.description,
137 'description': org_repo.description,
137 'revs': h.select('other_ref', '', c.default_revs, class_='refs')
138 'revs': h.select('other_ref', '', c.default_revs, class_='refs')
138 }
139 }
139
140
140 #gather forks and add to this list
141 #gather forks and add to this list
141 for fork in org_repo.forks:
142 for fork in org_repo.forks:
142 c.other_repos.append((fork.repo_name, '%s/%s' % (
143 c.other_repos.append((fork.repo_name, '%s/%s' % (
143 fork.user.username, fork.repo_name))
144 fork.user.username, fork.repo_name))
144 )
145 )
145 other_repos_info[fork.repo_name] = {
146 other_repos_info[fork.repo_name] = {
146 'gravatar': h.gravatar_url(fork.user.email, 24),
147 'gravatar': h.gravatar_url(fork.user.email, 24),
147 'description': fork.description,
148 'description': fork.description,
148 'revs': h.select('other_ref', '',
149 'revs': h.select('other_ref', '',
149 self._get_repo_refs(fork.scm_instance),
150 self._get_repo_refs(fork.scm_instance),
150 class_='refs')
151 class_='refs')
151 }
152 }
152 #add parents of this fork also, but only if it's not empty
153 #add parents of this fork also, but only if it's not empty
153 if org_repo.parent and org_repo.parent.scm_instance.revisions:
154 if org_repo.parent and org_repo.parent.scm_instance.revisions:
154 c.default_pull_request = org_repo.parent.repo_name
155 c.default_pull_request = org_repo.parent.repo_name
155 c.default_pull_request_rev = self._get_default_rev(org_repo.parent)
156 c.default_pull_request_rev = self._get_default_rev(org_repo.parent)
156 c.default_revs = self._get_repo_refs(org_repo.parent.scm_instance)
157 c.default_revs = self._get_repo_refs(org_repo.parent.scm_instance)
157 c.other_repos.append((org_repo.parent.repo_name, '%s/%s' % (
158 c.other_repos.append((org_repo.parent.repo_name, '%s/%s' % (
158 org_repo.parent.user.username,
159 org_repo.parent.user.username,
159 org_repo.parent.repo_name))
160 org_repo.parent.repo_name))
160 )
161 )
161 other_repos_info[org_repo.parent.repo_name] = {
162 other_repos_info[org_repo.parent.repo_name] = {
162 'gravatar': h.gravatar_url(org_repo.parent.user.email, 24),
163 'gravatar': h.gravatar_url(org_repo.parent.user.email, 24),
163 'description': org_repo.parent.description,
164 'description': org_repo.parent.description,
164 'revs': h.select('other_ref', '',
165 'revs': h.select('other_ref', '',
165 self._get_repo_refs(org_repo.parent.scm_instance),
166 self._get_repo_refs(org_repo.parent.scm_instance),
166 class_='refs')
167 class_='refs')
167 }
168 }
168
169
169 c.other_repos_info = json.dumps(other_repos_info)
170 c.other_repos_info = json.dumps(other_repos_info)
170 c.review_members = [org_repo.user]
171 c.review_members = [org_repo.user]
171 return render('/pullrequests/pullrequest.html')
172 return render('/pullrequests/pullrequest.html')
172
173
173 @NotAnonymous()
174 @NotAnonymous()
174 def create(self, repo_name):
175 def create(self, repo_name):
175 repo = RepoModel()._get_repo(repo_name)
176 repo = RepoModel()._get_repo(repo_name)
176 try:
177 try:
177 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
178 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
178 except formencode.Invalid, errors:
179 except formencode.Invalid, errors:
179 log.error(traceback.format_exc())
180 log.error(traceback.format_exc())
180 if errors.error_dict.get('revisions'):
181 if errors.error_dict.get('revisions'):
181 msg = 'Revisions: %s' % errors.error_dict['revisions']
182 msg = 'Revisions: %s' % errors.error_dict['revisions']
182 elif errors.error_dict.get('pullrequest_title'):
183 elif errors.error_dict.get('pullrequest_title'):
183 msg = _('Pull request requires a title with min. 3 chars')
184 msg = _('Pull request requires a title with min. 3 chars')
184 else:
185 else:
185 msg = _('error during creation of pull request')
186 msg = _('error during creation of pull request')
186
187
187 h.flash(msg, 'error')
188 h.flash(msg, 'error')
188 return redirect(url('pullrequest_home', repo_name=repo_name))
189 return redirect(url('pullrequest_home', repo_name=repo_name))
189
190
190 org_repo = _form['org_repo']
191 org_repo = _form['org_repo']
191 org_ref = _form['org_ref']
192 org_ref = _form['org_ref']
192 other_repo = _form['other_repo']
193 other_repo = _form['other_repo']
193 other_ref = _form['other_ref']
194 other_ref = _form['other_ref']
194 revisions = _form['revisions']
195 revisions = _form['revisions']
195 reviewers = _form['review_members']
196 reviewers = _form['review_members']
196
197
197 title = _form['pullrequest_title']
198 title = _form['pullrequest_title']
198 description = _form['pullrequest_desc']
199 description = _form['pullrequest_desc']
199
200
200 try:
201 try:
201 pull_request = PullRequestModel().create(
202 pull_request = PullRequestModel().create(
202 self.rhodecode_user.user_id, org_repo, org_ref, other_repo,
203 self.rhodecode_user.user_id, org_repo, org_ref, other_repo,
203 other_ref, revisions, reviewers, title, description
204 other_ref, revisions, reviewers, title, description
204 )
205 )
205 Session().commit()
206 Session().commit()
206 h.flash(_('Successfully opened new pull request'),
207 h.flash(_('Successfully opened new pull request'),
207 category='success')
208 category='success')
208 except Exception:
209 except Exception:
209 h.flash(_('Error occurred during sending pull request'),
210 h.flash(_('Error occurred during sending pull request'),
210 category='error')
211 category='error')
211 log.error(traceback.format_exc())
212 log.error(traceback.format_exc())
212 return redirect(url('pullrequest_home', repo_name=repo_name))
213 return redirect(url('pullrequest_home', repo_name=repo_name))
213
214
214 return redirect(url('pullrequest_show', repo_name=other_repo,
215 return redirect(url('pullrequest_show', repo_name=other_repo,
215 pull_request_id=pull_request.pull_request_id))
216 pull_request_id=pull_request.pull_request_id))
216
217
217 @NotAnonymous()
218 @NotAnonymous()
218 @jsonify
219 @jsonify
219 def update(self, repo_name, pull_request_id):
220 def update(self, repo_name, pull_request_id):
220 pull_request = PullRequest.get_or_404(pull_request_id)
221 pull_request = PullRequest.get_or_404(pull_request_id)
221 if pull_request.is_closed():
222 if pull_request.is_closed():
222 raise HTTPForbidden()
223 raise HTTPForbidden()
223 #only owner or admin can update it
224 #only owner or admin can update it
224 owner = pull_request.author.user_id == c.rhodecode_user.user_id
225 owner = pull_request.author.user_id == c.rhodecode_user.user_id
225 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
226 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
226 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
227 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
227 request.POST.get('reviewers_ids', '').split(',')))
228 request.POST.get('reviewers_ids', '').split(',')))
228
229
229 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
230 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
230 Session.commit()
231 Session.commit()
231 return True
232 return True
232 raise HTTPForbidden()
233 raise HTTPForbidden()
233
234
234 @NotAnonymous()
235 @NotAnonymous()
235 @jsonify
236 @jsonify
236 def delete(self, repo_name, pull_request_id):
237 def delete(self, repo_name, pull_request_id):
237 pull_request = PullRequest.get_or_404(pull_request_id)
238 pull_request = PullRequest.get_or_404(pull_request_id)
238 #only owner can delete it !
239 #only owner can delete it !
239 if pull_request.author.user_id == c.rhodecode_user.user_id:
240 if pull_request.author.user_id == c.rhodecode_user.user_id:
240 PullRequestModel().delete(pull_request)
241 PullRequestModel().delete(pull_request)
241 Session().commit()
242 Session().commit()
242 h.flash(_('Successfully deleted pull request'),
243 h.flash(_('Successfully deleted pull request'),
243 category='success')
244 category='success')
244 return redirect(url('admin_settings_my_account'))
245 return redirect(url('admin_settings_my_account'))
245 raise HTTPForbidden()
246 raise HTTPForbidden()
246
247
247 def _load_compare_data(self, pull_request, enable_comments=True):
248 def _load_compare_data(self, pull_request, enable_comments=True):
248 """
249 """
249 Load context data needed for generating compare diff
250 Load context data needed for generating compare diff
250
251
251 :param pull_request:
252 :param pull_request:
252 :type pull_request:
253 :type pull_request:
253 """
254 """
254
255
255 org_repo = pull_request.org_repo
256 org_repo = pull_request.org_repo
256 (org_ref_type,
257 (org_ref_type,
257 org_ref_name,
258 org_ref_name,
258 org_ref_rev) = pull_request.org_ref.split(':')
259 org_ref_rev) = pull_request.org_ref.split(':')
259
260
260 other_repo = pull_request.other_repo
261 other_repo = pull_request.other_repo
261 (other_ref_type,
262 (other_ref_type,
262 other_ref_name,
263 other_ref_name,
263 other_ref_rev) = pull_request.other_ref.split(':')
264 other_ref_rev) = pull_request.other_ref.split(':')
264
265
265 # despite opening revisions for bookmarks/branches/tags, we always
266 # despite opening revisions for bookmarks/branches/tags, we always
266 # convert this to rev to prevent changes after book or branch change
267 # convert this to rev to prevent changes after book or branch change
267 org_ref = ('rev', org_ref_rev)
268 org_ref = ('rev', org_ref_rev)
268 other_ref = ('rev', other_ref_rev)
269 other_ref = ('rev', other_ref_rev)
269
270
270 c.org_repo = org_repo
271 c.org_repo = org_repo
271 c.other_repo = other_repo
272 c.other_repo = other_repo
272
273
273 c.cs_ranges, discovery_data = PullRequestModel().get_compare_data(
274 c.cs_ranges, discovery_data = PullRequestModel().get_compare_data(
274 org_repo, org_ref, other_repo, other_ref
275 org_repo, org_ref, other_repo, other_ref
275 )
276 )
276 if c.cs_ranges:
277 if c.cs_ranges:
277 # case we want a simple diff without incoming changesets, just
278 # case we want a simple diff without incoming changesets, just
278 # for review purposes. Make the diff on the forked repo, with
279 # for review purposes. Make the diff on the forked repo, with
279 # revision that is common ancestor
280 # revision that is common ancestor
280 other_ref = ('rev', c.cs_ranges[-1].parents[0].raw_id)
281 other_ref = ('rev', getattr(c.cs_ranges[-1].parents[0]
282 if c.cs_ranges[-1].parents
283 else EmptyChangeset(), 'raw_id'))
281 other_repo = org_repo
284 other_repo = org_repo
282
285
283 c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges])
286 c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges])
284 # defines that we need hidden inputs with changesets
287 # defines that we need hidden inputs with changesets
285 c.as_form = request.GET.get('as_form', False)
288 c.as_form = request.GET.get('as_form', False)
286
289
287 c.org_ref = org_ref[1]
290 c.org_ref = org_ref[1]
288 c.other_ref = other_ref[1]
291 c.other_ref = other_ref[1]
289 # diff needs to have swapped org with other to generate proper diff
292 _diff = diffs.differ(org_repo, org_ref, other_repo, other_ref,
290 _diff = diffs.differ(other_repo, other_ref, org_repo, org_ref,
291 discovery_data)
293 discovery_data)
294
292 diff_processor = diffs.DiffProcessor(_diff, format='gitdiff')
295 diff_processor = diffs.DiffProcessor(_diff, format='gitdiff')
293 _parsed = diff_processor.prepare()
296 _parsed = diff_processor.prepare()
294
297
295 c.files = []
298 c.files = []
296 c.changes = {}
299 c.changes = {}
297
300
298 for f in _parsed:
301 for f in _parsed:
299 fid = h.FID('', f['filename'])
302 fid = h.FID('', f['filename'])
300 c.files.append([fid, f['operation'], f['filename'], f['stats']])
303 c.files.append([fid, f['operation'], f['filename'], f['stats']])
301 diff = diff_processor.as_html(enable_comments=enable_comments,
304 diff = diff_processor.as_html(enable_comments=enable_comments,
302 parsed_lines=[f])
305 parsed_lines=[f])
303 c.changes[fid] = [f['operation'], f['filename'], diff]
306 c.changes[fid] = [f['operation'], f['filename'], diff]
304
307
305 def show(self, repo_name, pull_request_id):
308 def show(self, repo_name, pull_request_id):
306 repo_model = RepoModel()
309 repo_model = RepoModel()
307 c.users_array = repo_model.get_users_js()
310 c.users_array = repo_model.get_users_js()
308 c.users_groups_array = repo_model.get_users_groups_js()
311 c.users_groups_array = repo_model.get_users_groups_js()
309 c.pull_request = PullRequest.get_or_404(pull_request_id)
312 c.pull_request = PullRequest.get_or_404(pull_request_id)
310 c.target_repo = c.pull_request.org_repo.repo_name
313 c.target_repo = c.pull_request.org_repo.repo_name
311
314
312 cc_model = ChangesetCommentsModel()
315 cc_model = ChangesetCommentsModel()
313 cs_model = ChangesetStatusModel()
316 cs_model = ChangesetStatusModel()
314 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
317 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
315 pull_request=c.pull_request,
318 pull_request=c.pull_request,
316 with_revisions=True)
319 with_revisions=True)
317
320
318 cs_statuses = defaultdict(list)
321 cs_statuses = defaultdict(list)
319 for st in _cs_statuses:
322 for st in _cs_statuses:
320 cs_statuses[st.author.username] += [st]
323 cs_statuses[st.author.username] += [st]
321
324
322 c.pull_request_reviewers = []
325 c.pull_request_reviewers = []
323 c.pull_request_pending_reviewers = []
326 c.pull_request_pending_reviewers = []
324 for o in c.pull_request.reviewers:
327 for o in c.pull_request.reviewers:
325 st = cs_statuses.get(o.user.username, None)
328 st = cs_statuses.get(o.user.username, None)
326 if st:
329 if st:
327 sorter = lambda k: k.version
330 sorter = lambda k: k.version
328 st = [(x, list(y)[0])
331 st = [(x, list(y)[0])
329 for x, y in (groupby(sorted(st, key=sorter), sorter))]
332 for x, y in (groupby(sorted(st, key=sorter), sorter))]
330 else:
333 else:
331 c.pull_request_pending_reviewers.append(o.user)
334 c.pull_request_pending_reviewers.append(o.user)
332 c.pull_request_reviewers.append([o.user, st])
335 c.pull_request_reviewers.append([o.user, st])
333
336
334 # pull_requests repo_name we opened it against
337 # pull_requests repo_name we opened it against
335 # ie. other_repo must match
338 # ie. other_repo must match
336 if repo_name != c.pull_request.other_repo.repo_name:
339 if repo_name != c.pull_request.other_repo.repo_name:
337 raise HTTPNotFound
340 raise HTTPNotFound
338
341
339 # load compare data into template context
342 # load compare data into template context
340 enable_comments = not c.pull_request.is_closed()
343 enable_comments = not c.pull_request.is_closed()
341 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
344 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
342
345
343 # inline comments
346 # inline comments
344 c.inline_cnt = 0
347 c.inline_cnt = 0
345 c.inline_comments = cc_model.get_inline_comments(
348 c.inline_comments = cc_model.get_inline_comments(
346 c.rhodecode_db_repo.repo_id,
349 c.rhodecode_db_repo.repo_id,
347 pull_request=pull_request_id)
350 pull_request=pull_request_id)
348 # count inline comments
351 # count inline comments
349 for __, lines in c.inline_comments:
352 for __, lines in c.inline_comments:
350 for comments in lines.values():
353 for comments in lines.values():
351 c.inline_cnt += len(comments)
354 c.inline_cnt += len(comments)
352 # comments
355 # comments
353 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
356 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
354 pull_request=pull_request_id)
357 pull_request=pull_request_id)
355
358
356 try:
359 try:
357 cur_status = c.statuses[c.pull_request.revisions[0]][0]
360 cur_status = c.statuses[c.pull_request.revisions[0]][0]
358 except:
361 except:
359 log.error(traceback.format_exc())
362 log.error(traceback.format_exc())
360 cur_status = 'undefined'
363 cur_status = 'undefined'
361 if c.pull_request.is_closed() and 0:
364 if c.pull_request.is_closed() and 0:
362 c.current_changeset_status = cur_status
365 c.current_changeset_status = cur_status
363 else:
366 else:
364 # changeset(pull-request) status calulation based on reviewers
367 # changeset(pull-request) status calulation based on reviewers
365 c.current_changeset_status = cs_model.calculate_status(
368 c.current_changeset_status = cs_model.calculate_status(
366 c.pull_request_reviewers,
369 c.pull_request_reviewers,
367 )
370 )
368 c.changeset_statuses = ChangesetStatus.STATUSES
371 c.changeset_statuses = ChangesetStatus.STATUSES
369
372
370 return render('/pullrequests/pullrequest_show.html')
373 return render('/pullrequests/pullrequest_show.html')
371
374
372 @NotAnonymous()
375 @NotAnonymous()
373 @jsonify
376 @jsonify
374 def comment(self, repo_name, pull_request_id):
377 def comment(self, repo_name, pull_request_id):
375 pull_request = PullRequest.get_or_404(pull_request_id)
378 pull_request = PullRequest.get_or_404(pull_request_id)
376 if pull_request.is_closed():
379 if pull_request.is_closed():
377 raise HTTPForbidden()
380 raise HTTPForbidden()
378
381
379 status = request.POST.get('changeset_status')
382 status = request.POST.get('changeset_status')
380 change_status = request.POST.get('change_changeset_status')
383 change_status = request.POST.get('change_changeset_status')
381 text = request.POST.get('text')
384 text = request.POST.get('text')
382 if status and change_status:
385 if status and change_status:
383 text = text or (_('Status change -> %s')
386 text = text or (_('Status change -> %s')
384 % ChangesetStatus.get_status_lbl(status))
387 % ChangesetStatus.get_status_lbl(status))
385 comm = ChangesetCommentsModel().create(
388 comm = ChangesetCommentsModel().create(
386 text=text,
389 text=text,
387 repo=c.rhodecode_db_repo.repo_id,
390 repo=c.rhodecode_db_repo.repo_id,
388 user=c.rhodecode_user.user_id,
391 user=c.rhodecode_user.user_id,
389 pull_request=pull_request_id,
392 pull_request=pull_request_id,
390 f_path=request.POST.get('f_path'),
393 f_path=request.POST.get('f_path'),
391 line_no=request.POST.get('line'),
394 line_no=request.POST.get('line'),
392 status_change=(ChangesetStatus.get_status_lbl(status)
395 status_change=(ChangesetStatus.get_status_lbl(status)
393 if status and change_status else None)
396 if status and change_status else None)
394 )
397 )
395
398
396 # get status if set !
399 # get status if set !
397 if status and change_status:
400 if status and change_status:
398 ChangesetStatusModel().set_status(
401 ChangesetStatusModel().set_status(
399 c.rhodecode_db_repo.repo_id,
402 c.rhodecode_db_repo.repo_id,
400 status,
403 status,
401 c.rhodecode_user.user_id,
404 c.rhodecode_user.user_id,
402 comm,
405 comm,
403 pull_request=pull_request_id
406 pull_request=pull_request_id
404 )
407 )
405 action_logger(self.rhodecode_user,
408 action_logger(self.rhodecode_user,
406 'user_commented_pull_request:%s' % pull_request_id,
409 'user_commented_pull_request:%s' % pull_request_id,
407 c.rhodecode_db_repo, self.ip_addr, self.sa)
410 c.rhodecode_db_repo, self.ip_addr, self.sa)
408
411
409 if request.POST.get('save_close'):
412 if request.POST.get('save_close'):
410 PullRequestModel().close_pull_request(pull_request_id)
413 PullRequestModel().close_pull_request(pull_request_id)
411 action_logger(self.rhodecode_user,
414 action_logger(self.rhodecode_user,
412 'user_closed_pull_request:%s' % pull_request_id,
415 'user_closed_pull_request:%s' % pull_request_id,
413 c.rhodecode_db_repo, self.ip_addr, self.sa)
416 c.rhodecode_db_repo, self.ip_addr, self.sa)
414
417
415 Session().commit()
418 Session().commit()
416
419
417 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
420 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
418 return redirect(h.url('pullrequest_show', repo_name=repo_name,
421 return redirect(h.url('pullrequest_show', repo_name=repo_name,
419 pull_request_id=pull_request_id))
422 pull_request_id=pull_request_id))
420
423
421 data = {
424 data = {
422 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
425 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
423 }
426 }
424 if comm:
427 if comm:
425 c.co = comm
428 c.co = comm
426 data.update(comm.get_dict())
429 data.update(comm.get_dict())
427 data.update({'rendered_text':
430 data.update({'rendered_text':
428 render('changeset/changeset_comment_block.html')})
431 render('changeset/changeset_comment_block.html')})
429
432
430 return data
433 return data
431
434
432 @NotAnonymous()
435 @NotAnonymous()
433 @jsonify
436 @jsonify
434 def delete_comment(self, repo_name, comment_id):
437 def delete_comment(self, repo_name, comment_id):
435 co = ChangesetComment.get(comment_id)
438 co = ChangesetComment.get(comment_id)
436 if co.pull_request.is_closed():
439 if co.pull_request.is_closed():
437 #don't allow deleting comments on closed pull request
440 #don't allow deleting comments on closed pull request
438 raise HTTPForbidden()
441 raise HTTPForbidden()
439
442
440 owner = lambda: co.author.user_id == c.rhodecode_user.user_id
443 owner = lambda: co.author.user_id == c.rhodecode_user.user_id
441 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
444 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
442 ChangesetCommentsModel().delete(comment=co)
445 ChangesetCommentsModel().delete(comment=co)
443 Session().commit()
446 Session().commit()
444 return True
447 return True
445 else:
448 else:
446 raise HTTPForbidden()
449 raise HTTPForbidden()
@@ -1,771 +1,768
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.diffs
3 rhodecode.lib.diffs
4 ~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~
5
5
6 Set of diffing helpers, previously part of vcs
6 Set of diffing helpers, previously part of vcs
7
7
8
8
9 :created_on: Dec 4, 2011
9 :created_on: Dec 4, 2011
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :original copyright: 2007-2008 by Armin Ronacher
12 :original copyright: 2007-2008 by Armin Ronacher
13 :license: GPLv3, see COPYING for more details.
13 :license: GPLv3, see COPYING for more details.
14 """
14 """
15 # This program is free software: you can redistribute it and/or modify
15 # This program is free software: you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License as published by
16 # it under the terms of the GNU General Public License as published by
17 # the Free Software Foundation, either version 3 of the License, or
17 # the Free Software Foundation, either version 3 of the License, or
18 # (at your option) any later version.
18 # (at your option) any later version.
19 #
19 #
20 # This program is distributed in the hope that it will be useful,
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
23 # GNU General Public License for more details.
24 #
24 #
25 # You should have received a copy of the GNU General Public License
25 # You should have received a copy of the GNU General Public License
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
27
28 import re
28 import re
29 import difflib
29 import difflib
30 import logging
30 import logging
31 import traceback
31 import traceback
32
32
33 from itertools import tee, imap
33 from itertools import tee, imap
34
34
35 from mercurial import patch
35 from mercurial import patch
36 from mercurial.mdiff import diffopts
36 from mercurial.mdiff import diffopts
37 from mercurial.bundlerepo import bundlerepository
37 from mercurial.bundlerepo import bundlerepository
38
38
39 from pylons.i18n.translation import _
39 from pylons.i18n.translation import _
40
40
41 from rhodecode.lib.compat import BytesIO
41 from rhodecode.lib.compat import BytesIO
42 from rhodecode.lib.vcs.utils.hgcompat import localrepo
42 from rhodecode.lib.vcs.utils.hgcompat import localrepo
43 from rhodecode.lib.vcs.exceptions import VCSError
43 from rhodecode.lib.vcs.exceptions import VCSError
44 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
44 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
45 from rhodecode.lib.vcs.backends.base import EmptyChangeset
45 from rhodecode.lib.vcs.backends.base import EmptyChangeset
46 from rhodecode.lib.helpers import escape
46 from rhodecode.lib.helpers import escape
47 from rhodecode.lib.utils import make_ui
47 from rhodecode.lib.utils import make_ui
48 from rhodecode.lib.utils2 import safe_unicode
48 from rhodecode.lib.utils2 import safe_unicode
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 def wrap_to_table(str_):
53 def wrap_to_table(str_):
54 return '''<table class="code-difftable">
54 return '''<table class="code-difftable">
55 <tr class="line no-comment">
55 <tr class="line no-comment">
56 <td class="lineno new"></td>
56 <td class="lineno new"></td>
57 <td class="code no-comment"><pre>%s</pre></td>
57 <td class="code no-comment"><pre>%s</pre></td>
58 </tr>
58 </tr>
59 </table>''' % str_
59 </table>''' % str_
60
60
61
61
62 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
62 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
63 ignore_whitespace=True, line_context=3,
63 ignore_whitespace=True, line_context=3,
64 enable_comments=False):
64 enable_comments=False):
65 """
65 """
66 returns a wrapped diff into a table, checks for cut_off_limit and presents
66 returns a wrapped diff into a table, checks for cut_off_limit and presents
67 proper message
67 proper message
68 """
68 """
69
69
70 if filenode_old is None:
70 if filenode_old is None:
71 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
71 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
72
72
73 if filenode_old.is_binary or filenode_new.is_binary:
73 if filenode_old.is_binary or filenode_new.is_binary:
74 diff = wrap_to_table(_('binary file'))
74 diff = wrap_to_table(_('binary file'))
75 stats = (0, 0)
75 stats = (0, 0)
76 size = 0
76 size = 0
77
77
78 elif cut_off_limit != -1 and (cut_off_limit is None or
78 elif cut_off_limit != -1 and (cut_off_limit is None or
79 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
79 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
80
80
81 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
81 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
82 ignore_whitespace=ignore_whitespace,
82 ignore_whitespace=ignore_whitespace,
83 context=line_context)
83 context=line_context)
84 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
84 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
85
85
86 diff = diff_processor.as_html(enable_comments=enable_comments)
86 diff = diff_processor.as_html(enable_comments=enable_comments)
87 stats = diff_processor.stat()
87 stats = diff_processor.stat()
88 size = len(diff or '')
88 size = len(diff or '')
89 else:
89 else:
90 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
90 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
91 'diff menu to display this diff'))
91 'diff menu to display this diff'))
92 stats = (0, 0)
92 stats = (0, 0)
93 size = 0
93 size = 0
94 if not diff:
94 if not diff:
95 submodules = filter(lambda o: isinstance(o, SubModuleNode),
95 submodules = filter(lambda o: isinstance(o, SubModuleNode),
96 [filenode_new, filenode_old])
96 [filenode_new, filenode_old])
97 if submodules:
97 if submodules:
98 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
98 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
99 else:
99 else:
100 diff = wrap_to_table(_('No changes detected'))
100 diff = wrap_to_table(_('No changes detected'))
101
101
102 cs1 = filenode_old.changeset.raw_id
102 cs1 = filenode_old.changeset.raw_id
103 cs2 = filenode_new.changeset.raw_id
103 cs2 = filenode_new.changeset.raw_id
104
104
105 return size, cs1, cs2, diff, stats
105 return size, cs1, cs2, diff, stats
106
106
107
107
108 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
108 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
109 """
109 """
110 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
110 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
111
111
112 :param ignore_whitespace: ignore whitespaces in diff
112 :param ignore_whitespace: ignore whitespaces in diff
113 """
113 """
114 # make sure we pass in default context
114 # make sure we pass in default context
115 context = context or 3
115 context = context or 3
116 submodules = filter(lambda o: isinstance(o, SubModuleNode),
116 submodules = filter(lambda o: isinstance(o, SubModuleNode),
117 [filenode_new, filenode_old])
117 [filenode_new, filenode_old])
118 if submodules:
118 if submodules:
119 return ''
119 return ''
120
120
121 for filenode in (filenode_old, filenode_new):
121 for filenode in (filenode_old, filenode_new):
122 if not isinstance(filenode, FileNode):
122 if not isinstance(filenode, FileNode):
123 raise VCSError("Given object should be FileNode object, not %s"
123 raise VCSError("Given object should be FileNode object, not %s"
124 % filenode.__class__)
124 % filenode.__class__)
125
125
126 repo = filenode_new.changeset.repository
126 repo = filenode_new.changeset.repository
127 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
127 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
128 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
128 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
129
129
130 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
130 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
131 ignore_whitespace, context)
131 ignore_whitespace, context)
132 return vcs_gitdiff
132 return vcs_gitdiff
133
133
134 NEW_FILENODE = 1
134 NEW_FILENODE = 1
135 DEL_FILENODE = 2
135 DEL_FILENODE = 2
136 MOD_FILENODE = 3
136 MOD_FILENODE = 3
137 RENAMED_FILENODE = 4
137 RENAMED_FILENODE = 4
138 CHMOD_FILENODE = 5
138 CHMOD_FILENODE = 5
139
139
140
140
141 class DiffLimitExceeded(Exception):
141 class DiffLimitExceeded(Exception):
142 pass
142 pass
143
143
144
144
145 class LimitedDiffContainer(object):
145 class LimitedDiffContainer(object):
146
146
147 def __init__(self, diff_limit, cur_diff_size, diff):
147 def __init__(self, diff_limit, cur_diff_size, diff):
148 self.diff = diff
148 self.diff = diff
149 self.diff_limit = diff_limit
149 self.diff_limit = diff_limit
150 self.cur_diff_size = cur_diff_size
150 self.cur_diff_size = cur_diff_size
151
151
152 def __iter__(self):
152 def __iter__(self):
153 for l in self.diff:
153 for l in self.diff:
154 yield l
154 yield l
155
155
156
156
157 class DiffProcessor(object):
157 class DiffProcessor(object):
158 """
158 """
159 Give it a unified or git diff and it returns a list of the files that were
159 Give it a unified or git diff and it returns a list of the files that were
160 mentioned in the diff together with a dict of meta information that
160 mentioned in the diff together with a dict of meta information that
161 can be used to render it in a HTML template.
161 can be used to render it in a HTML template.
162 """
162 """
163 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
163 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
164 _newline_marker = '\\ No newline at end of file\n'
164 _newline_marker = '\\ No newline at end of file\n'
165 _git_header_re = re.compile(r"""
165 _git_header_re = re.compile(r"""
166 #^diff[ ]--git
166 #^diff[ ]--git
167 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
167 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
168 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n
168 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n
169 ^rename[ ]from[ ](?P<rename_from>\S+)\n
169 ^rename[ ]from[ ](?P<rename_from>\S+)\n
170 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
170 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
171 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
171 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
172 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
172 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
173 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
173 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
174 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
174 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
175 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
175 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
176 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
176 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
177 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
177 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
178 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
178 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
179 """, re.VERBOSE | re.MULTILINE)
179 """, re.VERBOSE | re.MULTILINE)
180 _hg_header_re = re.compile(r"""
180 _hg_header_re = re.compile(r"""
181 #^diff[ ]--git
181 #^diff[ ]--git
182 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
182 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
183 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))?
183 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))?
184 (?:^rename[ ]from[ ](?P<rename_from>\S+)\n
184 (?:^rename[ ]from[ ](?P<rename_from>\S+)\n
185 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
185 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
186 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
186 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
187 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
187 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
188 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
188 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
189 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
189 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
190 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
190 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
191 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
191 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
192 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
192 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
193 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
193 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
194 """, re.VERBOSE | re.MULTILINE)
194 """, re.VERBOSE | re.MULTILINE)
195
195
196 def __init__(self, diff, vcs='hg', format='gitdiff', diff_limit=None):
196 def __init__(self, diff, vcs='hg', format='gitdiff', diff_limit=None):
197 """
197 """
198 :param diff: a text in diff format
198 :param diff: a text in diff format
199 :param vcs: type of version controll hg or git
199 :param vcs: type of version controll hg or git
200 :param format: format of diff passed, `udiff` or `gitdiff`
200 :param format: format of diff passed, `udiff` or `gitdiff`
201 :param diff_limit: define the size of diff that is considered "big"
201 :param diff_limit: define the size of diff that is considered "big"
202 based on that parameter cut off will be triggered, set to None
202 based on that parameter cut off will be triggered, set to None
203 to show full diff
203 to show full diff
204 """
204 """
205 if not isinstance(diff, basestring):
205 if not isinstance(diff, basestring):
206 raise Exception('Diff must be a basestring got %s instead' % type(diff))
206 raise Exception('Diff must be a basestring got %s instead' % type(diff))
207
207
208 self._diff = diff
208 self._diff = diff
209 self._format = format
209 self._format = format
210 self.adds = 0
210 self.adds = 0
211 self.removes = 0
211 self.removes = 0
212 # calculate diff size
212 # calculate diff size
213 self.diff_size = len(diff)
213 self.diff_size = len(diff)
214 self.diff_limit = diff_limit
214 self.diff_limit = diff_limit
215 self.cur_diff_size = 0
215 self.cur_diff_size = 0
216 self.parsed = False
216 self.parsed = False
217 self.parsed_diff = []
217 self.parsed_diff = []
218 self.vcs = vcs
218 self.vcs = vcs
219
219
220 if format == 'gitdiff':
220 if format == 'gitdiff':
221 self.differ = self._highlight_line_difflib
221 self.differ = self._highlight_line_difflib
222 self._parser = self._parse_gitdiff
222 self._parser = self._parse_gitdiff
223 else:
223 else:
224 self.differ = self._highlight_line_udiff
224 self.differ = self._highlight_line_udiff
225 self._parser = self._parse_udiff
225 self._parser = self._parse_udiff
226
226
227 def _copy_iterator(self):
227 def _copy_iterator(self):
228 """
228 """
229 make a fresh copy of generator, we should not iterate thru
229 make a fresh copy of generator, we should not iterate thru
230 an original as it's needed for repeating operations on
230 an original as it's needed for repeating operations on
231 this instance of DiffProcessor
231 this instance of DiffProcessor
232 """
232 """
233 self.__udiff, iterator_copy = tee(self.__udiff)
233 self.__udiff, iterator_copy = tee(self.__udiff)
234 return iterator_copy
234 return iterator_copy
235
235
236 def _escaper(self, string):
236 def _escaper(self, string):
237 """
237 """
238 Escaper for diff escapes special chars and checks the diff limit
238 Escaper for diff escapes special chars and checks the diff limit
239
239
240 :param string:
240 :param string:
241 :type string:
241 :type string:
242 """
242 """
243
243
244 self.cur_diff_size += len(string)
244 self.cur_diff_size += len(string)
245
245
246 # escaper get's iterated on each .next() call and it checks if each
246 # escaper get's iterated on each .next() call and it checks if each
247 # parsed line doesn't exceed the diff limit
247 # parsed line doesn't exceed the diff limit
248 if self.diff_limit is not None and self.cur_diff_size > self.diff_limit:
248 if self.diff_limit is not None and self.cur_diff_size > self.diff_limit:
249 raise DiffLimitExceeded('Diff Limit Exceeded')
249 raise DiffLimitExceeded('Diff Limit Exceeded')
250
250
251 return safe_unicode(string).replace('&', '&amp;')\
251 return safe_unicode(string).replace('&', '&amp;')\
252 .replace('<', '&lt;')\
252 .replace('<', '&lt;')\
253 .replace('>', '&gt;')
253 .replace('>', '&gt;')
254
254
255 def _line_counter(self, l):
255 def _line_counter(self, l):
256 """
256 """
257 Checks each line and bumps total adds/removes for this diff
257 Checks each line and bumps total adds/removes for this diff
258
258
259 :param l:
259 :param l:
260 """
260 """
261 if l.startswith('+') and not l.startswith('+++'):
261 if l.startswith('+') and not l.startswith('+++'):
262 self.adds += 1
262 self.adds += 1
263 elif l.startswith('-') and not l.startswith('---'):
263 elif l.startswith('-') and not l.startswith('---'):
264 self.removes += 1
264 self.removes += 1
265 return safe_unicode(l)
265 return safe_unicode(l)
266
266
267 def _highlight_line_difflib(self, line, next_):
267 def _highlight_line_difflib(self, line, next_):
268 """
268 """
269 Highlight inline changes in both lines.
269 Highlight inline changes in both lines.
270 """
270 """
271
271
272 if line['action'] == 'del':
272 if line['action'] == 'del':
273 old, new = line, next_
273 old, new = line, next_
274 else:
274 else:
275 old, new = next_, line
275 old, new = next_, line
276
276
277 oldwords = re.split(r'(\W)', old['line'])
277 oldwords = re.split(r'(\W)', old['line'])
278 newwords = re.split(r'(\W)', new['line'])
278 newwords = re.split(r'(\W)', new['line'])
279
279
280 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
280 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
281
281
282 oldfragments, newfragments = [], []
282 oldfragments, newfragments = [], []
283 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
283 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
284 oldfrag = ''.join(oldwords[i1:i2])
284 oldfrag = ''.join(oldwords[i1:i2])
285 newfrag = ''.join(newwords[j1:j2])
285 newfrag = ''.join(newwords[j1:j2])
286 if tag != 'equal':
286 if tag != 'equal':
287 if oldfrag:
287 if oldfrag:
288 oldfrag = '<del>%s</del>' % oldfrag
288 oldfrag = '<del>%s</del>' % oldfrag
289 if newfrag:
289 if newfrag:
290 newfrag = '<ins>%s</ins>' % newfrag
290 newfrag = '<ins>%s</ins>' % newfrag
291 oldfragments.append(oldfrag)
291 oldfragments.append(oldfrag)
292 newfragments.append(newfrag)
292 newfragments.append(newfrag)
293
293
294 old['line'] = "".join(oldfragments)
294 old['line'] = "".join(oldfragments)
295 new['line'] = "".join(newfragments)
295 new['line'] = "".join(newfragments)
296
296
297 def _highlight_line_udiff(self, line, next_):
297 def _highlight_line_udiff(self, line, next_):
298 """
298 """
299 Highlight inline changes in both lines.
299 Highlight inline changes in both lines.
300 """
300 """
301 start = 0
301 start = 0
302 limit = min(len(line['line']), len(next_['line']))
302 limit = min(len(line['line']), len(next_['line']))
303 while start < limit and line['line'][start] == next_['line'][start]:
303 while start < limit and line['line'][start] == next_['line'][start]:
304 start += 1
304 start += 1
305 end = -1
305 end = -1
306 limit -= start
306 limit -= start
307 while -end <= limit and line['line'][end] == next_['line'][end]:
307 while -end <= limit and line['line'][end] == next_['line'][end]:
308 end -= 1
308 end -= 1
309 end += 1
309 end += 1
310 if start or end:
310 if start or end:
311 def do(l):
311 def do(l):
312 last = end + len(l['line'])
312 last = end + len(l['line'])
313 if l['action'] == 'add':
313 if l['action'] == 'add':
314 tag = 'ins'
314 tag = 'ins'
315 else:
315 else:
316 tag = 'del'
316 tag = 'del'
317 l['line'] = '%s<%s>%s</%s>%s' % (
317 l['line'] = '%s<%s>%s</%s>%s' % (
318 l['line'][:start],
318 l['line'][:start],
319 tag,
319 tag,
320 l['line'][start:last],
320 l['line'][start:last],
321 tag,
321 tag,
322 l['line'][last:]
322 l['line'][last:]
323 )
323 )
324 do(line)
324 do(line)
325 do(next_)
325 do(next_)
326
326
327 def _get_header(self, diff_chunk):
327 def _get_header(self, diff_chunk):
328 """
328 """
329 parses the diff header, and returns parts, and leftover diff
329 parses the diff header, and returns parts, and leftover diff
330 parts consists of 14 elements::
330 parts consists of 14 elements::
331
331
332 a_path, b_path, similarity_index, rename_from, rename_to,
332 a_path, b_path, similarity_index, rename_from, rename_to,
333 old_mode, new_mode, new_file_mode, deleted_file_mode,
333 old_mode, new_mode, new_file_mode, deleted_file_mode,
334 a_blob_id, b_blob_id, b_mode, a_file, b_file
334 a_blob_id, b_blob_id, b_mode, a_file, b_file
335
335
336 :param diff_chunk:
336 :param diff_chunk:
337 :type diff_chunk:
337 :type diff_chunk:
338 """
338 """
339
339
340 if self.vcs == 'git':
340 if self.vcs == 'git':
341 match = self._git_header_re.match(diff_chunk)
341 match = self._git_header_re.match(diff_chunk)
342 diff = diff_chunk[match.end():]
342 diff = diff_chunk[match.end():]
343 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
343 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
344 elif self.vcs == 'hg':
344 elif self.vcs == 'hg':
345 match = self._hg_header_re.match(diff_chunk)
345 match = self._hg_header_re.match(diff_chunk)
346 diff = diff_chunk[match.end():]
346 diff = diff_chunk[match.end():]
347 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
347 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
348 else:
348 else:
349 raise Exception('VCS type %s is not supported' % self.vcs)
349 raise Exception('VCS type %s is not supported' % self.vcs)
350
350
351 def _parse_gitdiff(self, inline_diff=True):
351 def _parse_gitdiff(self, inline_diff=True):
352 _files = []
352 _files = []
353 diff_container = lambda arg: arg
353 diff_container = lambda arg: arg
354
354
355 ##split the diff in chunks of separate --git a/file b/file chunks
355 ##split the diff in chunks of separate --git a/file b/file chunks
356 for raw_diff in ('\n' + self._diff).split('\ndiff --git')[1:]:
356 for raw_diff in ('\n' + self._diff).split('\ndiff --git')[1:]:
357 binary = False
357 binary = False
358 binary_msg = 'unknown binary'
358 binary_msg = 'unknown binary'
359 head, diff = self._get_header(raw_diff)
359 head, diff = self._get_header(raw_diff)
360
360
361 if not head['a_file'] and head['b_file']:
361 if not head['a_file'] and head['b_file']:
362 op = 'A'
362 op = 'A'
363 elif head['a_file'] and head['b_file']:
363 elif head['a_file'] and head['b_file']:
364 op = 'M'
364 op = 'M'
365 elif head['a_file'] and not head['b_file']:
365 elif head['a_file'] and not head['b_file']:
366 op = 'D'
366 op = 'D'
367 else:
367 else:
368 #probably we're dealing with a binary file 1
368 #probably we're dealing with a binary file 1
369 binary = True
369 binary = True
370 if head['deleted_file_mode']:
370 if head['deleted_file_mode']:
371 op = 'D'
371 op = 'D'
372 stats = ['b', DEL_FILENODE]
372 stats = ['b', DEL_FILENODE]
373 binary_msg = 'deleted binary file'
373 binary_msg = 'deleted binary file'
374 elif head['new_file_mode']:
374 elif head['new_file_mode']:
375 op = 'A'
375 op = 'A'
376 stats = ['b', NEW_FILENODE]
376 stats = ['b', NEW_FILENODE]
377 binary_msg = 'new binary file %s' % head['new_file_mode']
377 binary_msg = 'new binary file %s' % head['new_file_mode']
378 else:
378 else:
379 if head['new_mode'] and head['old_mode']:
379 if head['new_mode'] and head['old_mode']:
380 stats = ['b', CHMOD_FILENODE]
380 stats = ['b', CHMOD_FILENODE]
381 op = 'M'
381 op = 'M'
382 binary_msg = ('modified binary file chmod %s => %s'
382 binary_msg = ('modified binary file chmod %s => %s'
383 % (head['old_mode'], head['new_mode']))
383 % (head['old_mode'], head['new_mode']))
384 elif (head['rename_from'] and head['rename_to']
384 elif (head['rename_from'] and head['rename_to']
385 and head['rename_from'] != head['rename_to']):
385 and head['rename_from'] != head['rename_to']):
386 stats = ['b', RENAMED_FILENODE]
386 stats = ['b', RENAMED_FILENODE]
387 op = 'M'
387 op = 'M'
388 binary_msg = ('file renamed from %s to %s'
388 binary_msg = ('file renamed from %s to %s'
389 % (head['rename_from'], head['rename_to']))
389 % (head['rename_from'], head['rename_to']))
390 else:
390 else:
391 stats = ['b', MOD_FILENODE]
391 stats = ['b', MOD_FILENODE]
392 op = 'M'
392 op = 'M'
393 binary_msg = 'modified binary file'
393 binary_msg = 'modified binary file'
394
394
395 if not binary:
395 if not binary:
396 try:
396 try:
397 chunks, stats = self._parse_lines(diff)
397 chunks, stats = self._parse_lines(diff)
398 except DiffLimitExceeded:
398 except DiffLimitExceeded:
399 diff_container = lambda _diff: LimitedDiffContainer(
399 diff_container = lambda _diff: LimitedDiffContainer(
400 self.diff_limit,
400 self.diff_limit,
401 self.cur_diff_size,
401 self.cur_diff_size,
402 _diff)
402 _diff)
403 break
403 break
404 else:
404 else:
405 chunks = []
405 chunks = []
406 chunks.append([{
406 chunks.append([{
407 'old_lineno': '',
407 'old_lineno': '',
408 'new_lineno': '',
408 'new_lineno': '',
409 'action': 'binary',
409 'action': 'binary',
410 'line': binary_msg,
410 'line': binary_msg,
411 }])
411 }])
412
412
413 _files.append({
413 _files.append({
414 'filename': head['b_path'],
414 'filename': head['b_path'],
415 'old_revision': head['a_blob_id'],
415 'old_revision': head['a_blob_id'],
416 'new_revision': head['b_blob_id'],
416 'new_revision': head['b_blob_id'],
417 'chunks': chunks,
417 'chunks': chunks,
418 'operation': op,
418 'operation': op,
419 'stats': stats,
419 'stats': stats,
420 })
420 })
421
421
422 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
422 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
423
423
424 if inline_diff is False:
424 if inline_diff is False:
425 return diff_container(sorted(_files, key=sorter))
425 return diff_container(sorted(_files, key=sorter))
426
426
427 # highlight inline changes
427 # highlight inline changes
428 for diff_data in _files:
428 for diff_data in _files:
429 for chunk in diff_data['chunks']:
429 for chunk in diff_data['chunks']:
430 lineiter = iter(chunk)
430 lineiter = iter(chunk)
431 try:
431 try:
432 while 1:
432 while 1:
433 line = lineiter.next()
433 line = lineiter.next()
434 if line['action'] not in ['unmod', 'context']:
434 if line['action'] not in ['unmod', 'context']:
435 nextline = lineiter.next()
435 nextline = lineiter.next()
436 if nextline['action'] in ['unmod', 'context'] or \
436 if nextline['action'] in ['unmod', 'context'] or \
437 nextline['action'] == line['action']:
437 nextline['action'] == line['action']:
438 continue
438 continue
439 self.differ(line, nextline)
439 self.differ(line, nextline)
440 except StopIteration:
440 except StopIteration:
441 pass
441 pass
442
442
443 return diff_container(sorted(_files, key=sorter))
443 return diff_container(sorted(_files, key=sorter))
444
444
445 def _parse_udiff(self, inline_diff=True):
445 def _parse_udiff(self, inline_diff=True):
446 raise NotImplementedError()
446 raise NotImplementedError()
447
447
448 def _parse_lines(self, diff):
448 def _parse_lines(self, diff):
449 """
449 """
450 Parse the diff an return data for the template.
450 Parse the diff an return data for the template.
451 """
451 """
452
452
453 lineiter = iter(diff)
453 lineiter = iter(diff)
454 stats = [0, 0]
454 stats = [0, 0]
455
455
456 try:
456 try:
457 chunks = []
457 chunks = []
458 line = lineiter.next()
458 line = lineiter.next()
459
459
460 while line:
460 while line:
461 lines = []
461 lines = []
462 chunks.append(lines)
462 chunks.append(lines)
463
463
464 match = self._chunk_re.match(line)
464 match = self._chunk_re.match(line)
465
465
466 if not match:
466 if not match:
467 break
467 break
468
468
469 gr = match.groups()
469 gr = match.groups()
470 (old_line, old_end,
470 (old_line, old_end,
471 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
471 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
472 old_line -= 1
472 old_line -= 1
473 new_line -= 1
473 new_line -= 1
474
474
475 context = len(gr) == 5
475 context = len(gr) == 5
476 old_end += old_line
476 old_end += old_line
477 new_end += new_line
477 new_end += new_line
478
478
479 if context:
479 if context:
480 # skip context only if it's first line
480 # skip context only if it's first line
481 if int(gr[0]) > 1:
481 if int(gr[0]) > 1:
482 lines.append({
482 lines.append({
483 'old_lineno': '...',
483 'old_lineno': '...',
484 'new_lineno': '...',
484 'new_lineno': '...',
485 'action': 'context',
485 'action': 'context',
486 'line': line,
486 'line': line,
487 })
487 })
488
488
489 line = lineiter.next()
489 line = lineiter.next()
490
490
491 while old_line < old_end or new_line < new_end:
491 while old_line < old_end or new_line < new_end:
492 if line:
492 if line:
493 command = line[0]
493 command = line[0]
494 if command in ['+', '-', ' ']:
494 if command in ['+', '-', ' ']:
495 #only modify the line if it's actually a diff
495 #only modify the line if it's actually a diff
496 # thing
496 # thing
497 line = line[1:]
497 line = line[1:]
498 else:
498 else:
499 command = ' '
499 command = ' '
500
500
501 affects_old = affects_new = False
501 affects_old = affects_new = False
502
502
503 # ignore those if we don't expect them
503 # ignore those if we don't expect them
504 if command in '#@':
504 if command in '#@':
505 continue
505 continue
506 elif command == '+':
506 elif command == '+':
507 affects_new = True
507 affects_new = True
508 action = 'add'
508 action = 'add'
509 stats[0] += 1
509 stats[0] += 1
510 elif command == '-':
510 elif command == '-':
511 affects_old = True
511 affects_old = True
512 action = 'del'
512 action = 'del'
513 stats[1] += 1
513 stats[1] += 1
514 else:
514 else:
515 affects_old = affects_new = True
515 affects_old = affects_new = True
516 action = 'unmod'
516 action = 'unmod'
517
517
518 if line != self._newline_marker:
518 if line != self._newline_marker:
519 old_line += affects_old
519 old_line += affects_old
520 new_line += affects_new
520 new_line += affects_new
521 lines.append({
521 lines.append({
522 'old_lineno': affects_old and old_line or '',
522 'old_lineno': affects_old and old_line or '',
523 'new_lineno': affects_new and new_line or '',
523 'new_lineno': affects_new and new_line or '',
524 'action': action,
524 'action': action,
525 'line': line
525 'line': line
526 })
526 })
527
527
528 line = lineiter.next()
528 line = lineiter.next()
529
529
530 if line == self._newline_marker:
530 if line == self._newline_marker:
531 # we need to append to lines, since this is not
531 # we need to append to lines, since this is not
532 # counted in the line specs of diff
532 # counted in the line specs of diff
533 lines.append({
533 lines.append({
534 'old_lineno': '...',
534 'old_lineno': '...',
535 'new_lineno': '...',
535 'new_lineno': '...',
536 'action': 'context',
536 'action': 'context',
537 'line': line
537 'line': line
538 })
538 })
539
539
540 except StopIteration:
540 except StopIteration:
541 pass
541 pass
542 return chunks, stats
542 return chunks, stats
543
543
544 def _safe_id(self, idstring):
544 def _safe_id(self, idstring):
545 """Make a string safe for including in an id attribute.
545 """Make a string safe for including in an id attribute.
546
546
547 The HTML spec says that id attributes 'must begin with
547 The HTML spec says that id attributes 'must begin with
548 a letter ([A-Za-z]) and may be followed by any number
548 a letter ([A-Za-z]) and may be followed by any number
549 of letters, digits ([0-9]), hyphens ("-"), underscores
549 of letters, digits ([0-9]), hyphens ("-"), underscores
550 ("_"), colons (":"), and periods (".")'. These regexps
550 ("_"), colons (":"), and periods (".")'. These regexps
551 are slightly over-zealous, in that they remove colons
551 are slightly over-zealous, in that they remove colons
552 and periods unnecessarily.
552 and periods unnecessarily.
553
553
554 Whitespace is transformed into underscores, and then
554 Whitespace is transformed into underscores, and then
555 anything which is not a hyphen or a character that
555 anything which is not a hyphen or a character that
556 matches \w (alphanumerics and underscore) is removed.
556 matches \w (alphanumerics and underscore) is removed.
557
557
558 """
558 """
559 # Transform all whitespace to underscore
559 # Transform all whitespace to underscore
560 idstring = re.sub(r'\s', "_", '%s' % idstring)
560 idstring = re.sub(r'\s', "_", '%s' % idstring)
561 # Remove everything that is not a hyphen or a member of \w
561 # Remove everything that is not a hyphen or a member of \w
562 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
562 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
563 return idstring
563 return idstring
564
564
565 def prepare(self, inline_diff=True):
565 def prepare(self, inline_diff=True):
566 """
566 """
567 Prepare the passed udiff for HTML rendering. It'l return a list
567 Prepare the passed udiff for HTML rendering. It'l return a list
568 of dicts with diff information
568 of dicts with diff information
569 """
569 """
570 parsed = self._parser(inline_diff=inline_diff)
570 parsed = self._parser(inline_diff=inline_diff)
571 self.parsed = True
571 self.parsed = True
572 self.parsed_diff = parsed
572 self.parsed_diff = parsed
573 return parsed
573 return parsed
574
574
575 def as_raw(self, diff_lines=None):
575 def as_raw(self, diff_lines=None):
576 """
576 """
577 Returns raw string diff
577 Returns raw string diff
578 """
578 """
579 return self._diff
579 return self._diff
580 #return u''.join(imap(self._line_counter, self._diff.splitlines(1)))
580 #return u''.join(imap(self._line_counter, self._diff.splitlines(1)))
581
581
582 def as_html(self, table_class='code-difftable', line_class='line',
582 def as_html(self, table_class='code-difftable', line_class='line',
583 new_lineno_class='lineno old', old_lineno_class='lineno new',
583 new_lineno_class='lineno old', old_lineno_class='lineno new',
584 code_class='code', enable_comments=False, parsed_lines=None):
584 code_class='code', enable_comments=False, parsed_lines=None):
585 """
585 """
586 Return given diff as html table with customized css classes
586 Return given diff as html table with customized css classes
587 """
587 """
588 def _link_to_if(condition, label, url):
588 def _link_to_if(condition, label, url):
589 """
589 """
590 Generates a link if condition is meet or just the label if not.
590 Generates a link if condition is meet or just the label if not.
591 """
591 """
592
592
593 if condition:
593 if condition:
594 return '''<a href="%(url)s">%(label)s</a>''' % {
594 return '''<a href="%(url)s">%(label)s</a>''' % {
595 'url': url,
595 'url': url,
596 'label': label
596 'label': label
597 }
597 }
598 else:
598 else:
599 return label
599 return label
600 if not self.parsed:
600 if not self.parsed:
601 self.prepare()
601 self.prepare()
602
602
603 diff_lines = self.parsed_diff
603 diff_lines = self.parsed_diff
604 if parsed_lines:
604 if parsed_lines:
605 diff_lines = parsed_lines
605 diff_lines = parsed_lines
606
606
607 _html_empty = True
607 _html_empty = True
608 _html = []
608 _html = []
609 _html.append('''<table class="%(table_class)s">\n''' % {
609 _html.append('''<table class="%(table_class)s">\n''' % {
610 'table_class': table_class
610 'table_class': table_class
611 })
611 })
612
612
613 for diff in diff_lines:
613 for diff in diff_lines:
614 for line in diff['chunks']:
614 for line in diff['chunks']:
615 _html_empty = False
615 _html_empty = False
616 for change in line:
616 for change in line:
617 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
617 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
618 'lc': line_class,
618 'lc': line_class,
619 'action': change['action']
619 'action': change['action']
620 })
620 })
621 anchor_old_id = ''
621 anchor_old_id = ''
622 anchor_new_id = ''
622 anchor_new_id = ''
623 anchor_old = "%(filename)s_o%(oldline_no)s" % {
623 anchor_old = "%(filename)s_o%(oldline_no)s" % {
624 'filename': self._safe_id(diff['filename']),
624 'filename': self._safe_id(diff['filename']),
625 'oldline_no': change['old_lineno']
625 'oldline_no': change['old_lineno']
626 }
626 }
627 anchor_new = "%(filename)s_n%(oldline_no)s" % {
627 anchor_new = "%(filename)s_n%(oldline_no)s" % {
628 'filename': self._safe_id(diff['filename']),
628 'filename': self._safe_id(diff['filename']),
629 'oldline_no': change['new_lineno']
629 'oldline_no': change['new_lineno']
630 }
630 }
631 cond_old = (change['old_lineno'] != '...' and
631 cond_old = (change['old_lineno'] != '...' and
632 change['old_lineno'])
632 change['old_lineno'])
633 cond_new = (change['new_lineno'] != '...' and
633 cond_new = (change['new_lineno'] != '...' and
634 change['new_lineno'])
634 change['new_lineno'])
635 if cond_old:
635 if cond_old:
636 anchor_old_id = 'id="%s"' % anchor_old
636 anchor_old_id = 'id="%s"' % anchor_old
637 if cond_new:
637 if cond_new:
638 anchor_new_id = 'id="%s"' % anchor_new
638 anchor_new_id = 'id="%s"' % anchor_new
639 ###########################################################
639 ###########################################################
640 # OLD LINE NUMBER
640 # OLD LINE NUMBER
641 ###########################################################
641 ###########################################################
642 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
642 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
643 'a_id': anchor_old_id,
643 'a_id': anchor_old_id,
644 'olc': old_lineno_class
644 'olc': old_lineno_class
645 })
645 })
646
646
647 _html.append('''%(link)s''' % {
647 _html.append('''%(link)s''' % {
648 'link': _link_to_if(True, change['old_lineno'],
648 'link': _link_to_if(True, change['old_lineno'],
649 '#%s' % anchor_old)
649 '#%s' % anchor_old)
650 })
650 })
651 _html.append('''</td>\n''')
651 _html.append('''</td>\n''')
652 ###########################################################
652 ###########################################################
653 # NEW LINE NUMBER
653 # NEW LINE NUMBER
654 ###########################################################
654 ###########################################################
655
655
656 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
656 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
657 'a_id': anchor_new_id,
657 'a_id': anchor_new_id,
658 'nlc': new_lineno_class
658 'nlc': new_lineno_class
659 })
659 })
660
660
661 _html.append('''%(link)s''' % {
661 _html.append('''%(link)s''' % {
662 'link': _link_to_if(True, change['new_lineno'],
662 'link': _link_to_if(True, change['new_lineno'],
663 '#%s' % anchor_new)
663 '#%s' % anchor_new)
664 })
664 })
665 _html.append('''</td>\n''')
665 _html.append('''</td>\n''')
666 ###########################################################
666 ###########################################################
667 # CODE
667 # CODE
668 ###########################################################
668 ###########################################################
669 comments = '' if enable_comments else 'no-comment'
669 comments = '' if enable_comments else 'no-comment'
670 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
670 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
671 'cc': code_class,
671 'cc': code_class,
672 'inc': comments
672 'inc': comments
673 })
673 })
674 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
674 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
675 'code': change['line']
675 'code': change['line']
676 })
676 })
677
677
678 _html.append('''\t</td>''')
678 _html.append('''\t</td>''')
679 _html.append('''\n</tr>\n''')
679 _html.append('''\n</tr>\n''')
680 _html.append('''</table>''')
680 _html.append('''</table>''')
681 if _html_empty:
681 if _html_empty:
682 return None
682 return None
683 return ''.join(_html)
683 return ''.join(_html)
684
684
685 def stat(self):
685 def stat(self):
686 """
686 """
687 Returns tuple of added, and removed lines for this instance
687 Returns tuple of added, and removed lines for this instance
688 """
688 """
689 return self.adds, self.removes
689 return self.adds, self.removes
690
690
691
691
692 class InMemoryBundleRepo(bundlerepository):
692 class InMemoryBundleRepo(bundlerepository):
693 def __init__(self, ui, path, bundlestream):
693 def __init__(self, ui, path, bundlestream):
694 self._tempparent = None
694 self._tempparent = None
695 localrepo.localrepository.__init__(self, ui, path)
695 localrepo.localrepository.__init__(self, ui, path)
696 self.ui.setconfig('phases', 'publish', False)
696 self.ui.setconfig('phases', 'publish', False)
697
697
698 self.bundle = bundlestream
698 self.bundle = bundlestream
699
699
700 # dict with the mapping 'filename' -> position in the bundle
700 # dict with the mapping 'filename' -> position in the bundle
701 self.bundlefilespos = {}
701 self.bundlefilespos = {}
702
702
703
703
704 def differ(org_repo, org_ref, other_repo, other_ref, discovery_data=None,
704 def differ(org_repo, org_ref, other_repo, other_ref, discovery_data=None,
705 bundle_compare=False, context=3, ignore_whitespace=False):
705 remote_compare=False, context=3, ignore_whitespace=False):
706 """
706 """
707 General differ between branches, bookmarks, revisions of two remote related
707 General differ between branches, bookmarks, revisions of two remote or
708 repositories
708 local but related repositories
709
709
710 :param org_repo:
710 :param org_repo:
711 :type org_repo:
712 :param org_ref:
711 :param org_ref:
713 :type org_ref:
714 :param other_repo:
712 :param other_repo:
715 :type other_repo:
713 :type other_repo:
716 :param other_ref:
717 :type other_ref:
714 :type other_ref:
718 """
715 """
719
716
720 bundlerepo = None
721 ignore_whitespace = ignore_whitespace
722 context = context
723 org_repo_scm = org_repo.scm_instance
717 org_repo_scm = org_repo.scm_instance
718 other_repo_scm = other_repo.scm_instance
719
724 org_repo = org_repo_scm._repo
720 org_repo = org_repo_scm._repo
725 other_repo = other_repo.scm_instance._repo
721 other_repo = other_repo_scm._repo
726 opts = diffopts(git=True, ignorews=ignore_whitespace, context=context)
722
727 org_ref = org_ref[1]
723 org_ref = org_ref[1]
728 other_ref = other_ref[1]
724 other_ref = other_ref[1]
729
725
730 if org_repo == other_repo:
726 if org_repo == other_repo:
731 log.debug('running diff between %s@%s and %s@%s'
727 log.debug('running diff between %s@%s and %s@%s'
732 % (org_repo, org_ref, other_repo, other_ref))
728 % (org_repo, org_ref, other_repo, other_ref))
733 _diff = org_repo_scm.get_diff(rev1=other_ref, rev2=org_ref,
729 _diff = org_repo_scm.get_diff(rev1=org_ref, rev2=other_ref,
734 ignore_whitespace=ignore_whitespace, context=context)
730 ignore_whitespace=ignore_whitespace, context=context)
735 return _diff
731 return _diff
736
732
737 elif bundle_compare:
733 elif remote_compare:
738
734 opts = diffopts(git=True, ignorews=ignore_whitespace, context=context)
739 common, incoming, rheads = discovery_data
735 common, incoming, rheads = discovery_data
740 other_repo_peer = localrepo.locallegacypeer(other_repo.local())
736 org_repo_peer = localrepo.locallegacypeer(org_repo.local())
741 # create a bundle (uncompressed if other repo is not local)
737 # create a bundle (uncompressed if other repo is not local)
742 if other_repo_peer.capable('getbundle') and incoming:
738 if org_repo_peer.capable('getbundle'):
743 # disable repo hooks here since it's just bundle !
739 # disable repo hooks here since it's just bundle !
744 # patch and reset hooks section of UI config to not run any
740 # patch and reset hooks section of UI config to not run any
745 # hooks on fetching archives with subrepos
741 # hooks on fetching archives with subrepos
746 for k, _ in other_repo.ui.configitems('hooks'):
742 for k, _ in org_repo.ui.configitems('hooks'):
747 other_repo.ui.setconfig('hooks', k, None)
743 org_repo.ui.setconfig('hooks', k, None)
748
744
749 unbundle = other_repo.getbundle('incoming', common=common,
745 unbundle = org_repo.getbundle('incoming', common=common,
750 heads=None)
746 heads=None)
751
747
752 buf = BytesIO()
748 buf = BytesIO()
753 while True:
749 while True:
754 chunk = unbundle._stream.read(1024 * 4)
750 chunk = unbundle._stream.read(1024 * 4)
755 if not chunk:
751 if not chunk:
756 break
752 break
757 buf.write(chunk)
753 buf.write(chunk)
758
754
759 buf.seek(0)
755 buf.seek(0)
760 # replace chunked _stream with data that can do tell() and seek()
756 # replace chunked _stream with data that can do tell() and seek()
761 unbundle._stream = buf
757 unbundle._stream = buf
762
758
763 ui = make_ui('db')
759 ui = make_ui('db')
764 bundlerepo = InMemoryBundleRepo(ui, path=org_repo.root,
760 bundlerepo = InMemoryBundleRepo(ui, path=org_repo.root,
765 bundlestream=unbundle)
761 bundlestream=unbundle)
766
762
767 return ''.join(patch.diff(bundlerepo or org_repo,
763 return ''.join(patch.diff(bundlerepo,
768 node1=org_repo[org_ref].node(),
764 node1=other_repo[other_ref].node(),
769 node2=other_repo[other_ref].node(),
765 node2=org_repo[org_ref].node(),
770 opts=opts))
766 opts=opts))
771
767
768 return '' No newline at end of file
@@ -1,277 +1,277
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.model.pull_request
3 rhodecode.model.pull_request
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 pull request model for RhodeCode
6 pull request model for RhodeCode
7
7
8 :created_on: Jun 6, 2012
8 :created_on: Jun 6, 2012
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2012-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2012-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
25
26 import logging
26 import logging
27 import binascii
27 import binascii
28 import datetime
28 import datetime
29 import re
29 import re
30
30
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32
32
33 from rhodecode.model.meta import Session
33 from rhodecode.model.meta import Session
34 from rhodecode.lib import helpers as h
34 from rhodecode.lib import helpers as h
35 from rhodecode.model import BaseModel
35 from rhodecode.model import BaseModel
36 from rhodecode.model.db import PullRequest, PullRequestReviewers, Notification
36 from rhodecode.model.db import PullRequest, PullRequestReviewers, Notification
37 from rhodecode.model.notification import NotificationModel
37 from rhodecode.model.notification import NotificationModel
38 from rhodecode.lib.utils2 import safe_unicode
38 from rhodecode.lib.utils2 import safe_unicode
39
39
40 from rhodecode.lib.vcs.utils.hgcompat import discovery, localrepo, scmutil, \
40 from rhodecode.lib.vcs.utils.hgcompat import discovery, localrepo, scmutil, \
41 findcommonoutgoing
41 findcommonoutgoing
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 class PullRequestModel(BaseModel):
46 class PullRequestModel(BaseModel):
47
47
48 cls = PullRequest
48 cls = PullRequest
49
49
50 def __get_pull_request(self, pull_request):
50 def __get_pull_request(self, pull_request):
51 return self._get_instance(PullRequest, pull_request)
51 return self._get_instance(PullRequest, pull_request)
52
52
53 def get_all(self, repo):
53 def get_all(self, repo):
54 repo = self._get_repo(repo)
54 repo = self._get_repo(repo)
55 return PullRequest.query().filter(PullRequest.other_repo == repo).all()
55 return PullRequest.query().filter(PullRequest.other_repo == repo).all()
56
56
57 def create(self, created_by, org_repo, org_ref, other_repo,
57 def create(self, created_by, org_repo, org_ref, other_repo,
58 other_ref, revisions, reviewers, title, description=None):
58 other_ref, revisions, reviewers, title, description=None):
59
59
60 created_by_user = self._get_user(created_by)
60 created_by_user = self._get_user(created_by)
61 org_repo = self._get_repo(org_repo)
61 org_repo = self._get_repo(org_repo)
62 other_repo = self._get_repo(other_repo)
62 other_repo = self._get_repo(other_repo)
63
63
64 new = PullRequest()
64 new = PullRequest()
65 new.org_repo = org_repo
65 new.org_repo = org_repo
66 new.org_ref = org_ref
66 new.org_ref = org_ref
67 new.other_repo = other_repo
67 new.other_repo = other_repo
68 new.other_ref = other_ref
68 new.other_ref = other_ref
69 new.revisions = revisions
69 new.revisions = revisions
70 new.title = title
70 new.title = title
71 new.description = description
71 new.description = description
72 new.author = created_by_user
72 new.author = created_by_user
73 self.sa.add(new)
73 self.sa.add(new)
74 Session().flush()
74 Session().flush()
75 #members
75 #members
76 for member in reviewers:
76 for member in reviewers:
77 _usr = self._get_user(member)
77 _usr = self._get_user(member)
78 reviewer = PullRequestReviewers(_usr, new)
78 reviewer = PullRequestReviewers(_usr, new)
79 self.sa.add(reviewer)
79 self.sa.add(reviewer)
80
80
81 #notification to reviewers
81 #notification to reviewers
82 notif = NotificationModel()
82 notif = NotificationModel()
83
83
84 pr_url = h.url('pullrequest_show', repo_name=other_repo.repo_name,
84 pr_url = h.url('pullrequest_show', repo_name=other_repo.repo_name,
85 pull_request_id=new.pull_request_id,
85 pull_request_id=new.pull_request_id,
86 qualified=True,
86 qualified=True,
87 )
87 )
88 subject = safe_unicode(
88 subject = safe_unicode(
89 h.link_to(
89 h.link_to(
90 _('%(user)s wants you to review pull request #%(pr_id)s') % \
90 _('%(user)s wants you to review pull request #%(pr_id)s') % \
91 {'user': created_by_user.username,
91 {'user': created_by_user.username,
92 'pr_id': new.pull_request_id},
92 'pr_id': new.pull_request_id},
93 pr_url
93 pr_url
94 )
94 )
95 )
95 )
96 body = description
96 body = description
97 kwargs = {
97 kwargs = {
98 'pr_title': title,
98 'pr_title': title,
99 'pr_user_created': h.person(created_by_user.email),
99 'pr_user_created': h.person(created_by_user.email),
100 'pr_repo_url': h.url('summary_home', repo_name=other_repo.repo_name,
100 'pr_repo_url': h.url('summary_home', repo_name=other_repo.repo_name,
101 qualified=True,),
101 qualified=True,),
102 'pr_url': pr_url,
102 'pr_url': pr_url,
103 'pr_revisions': revisions
103 'pr_revisions': revisions
104 }
104 }
105 notif.create(created_by=created_by_user, subject=subject, body=body,
105 notif.create(created_by=created_by_user, subject=subject, body=body,
106 recipients=reviewers,
106 recipients=reviewers,
107 type_=Notification.TYPE_PULL_REQUEST, email_kwargs=kwargs)
107 type_=Notification.TYPE_PULL_REQUEST, email_kwargs=kwargs)
108 return new
108 return new
109
109
110 def update_reviewers(self, pull_request, reviewers_ids):
110 def update_reviewers(self, pull_request, reviewers_ids):
111 reviewers_ids = set(reviewers_ids)
111 reviewers_ids = set(reviewers_ids)
112 pull_request = self.__get_pull_request(pull_request)
112 pull_request = self.__get_pull_request(pull_request)
113 current_reviewers = PullRequestReviewers.query()\
113 current_reviewers = PullRequestReviewers.query()\
114 .filter(PullRequestReviewers.pull_request==
114 .filter(PullRequestReviewers.pull_request==
115 pull_request)\
115 pull_request)\
116 .all()
116 .all()
117 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
117 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
118
118
119 to_add = reviewers_ids.difference(current_reviewers_ids)
119 to_add = reviewers_ids.difference(current_reviewers_ids)
120 to_remove = current_reviewers_ids.difference(reviewers_ids)
120 to_remove = current_reviewers_ids.difference(reviewers_ids)
121
121
122 log.debug("Adding %s reviewers" % to_add)
122 log.debug("Adding %s reviewers" % to_add)
123 log.debug("Removing %s reviewers" % to_remove)
123 log.debug("Removing %s reviewers" % to_remove)
124
124
125 for uid in to_add:
125 for uid in to_add:
126 _usr = self._get_user(uid)
126 _usr = self._get_user(uid)
127 reviewer = PullRequestReviewers(_usr, pull_request)
127 reviewer = PullRequestReviewers(_usr, pull_request)
128 self.sa.add(reviewer)
128 self.sa.add(reviewer)
129
129
130 for uid in to_remove:
130 for uid in to_remove:
131 reviewer = PullRequestReviewers.query()\
131 reviewer = PullRequestReviewers.query()\
132 .filter(PullRequestReviewers.user_id==uid,
132 .filter(PullRequestReviewers.user_id==uid,
133 PullRequestReviewers.pull_request==pull_request)\
133 PullRequestReviewers.pull_request==pull_request)\
134 .scalar()
134 .scalar()
135 if reviewer:
135 if reviewer:
136 self.sa.delete(reviewer)
136 self.sa.delete(reviewer)
137
137
138 def delete(self, pull_request):
138 def delete(self, pull_request):
139 pull_request = self.__get_pull_request(pull_request)
139 pull_request = self.__get_pull_request(pull_request)
140 Session().delete(pull_request)
140 Session().delete(pull_request)
141
141
142 def close_pull_request(self, pull_request):
142 def close_pull_request(self, pull_request):
143 pull_request = self.__get_pull_request(pull_request)
143 pull_request = self.__get_pull_request(pull_request)
144 pull_request.status = PullRequest.STATUS_CLOSED
144 pull_request.status = PullRequest.STATUS_CLOSED
145 pull_request.updated_on = datetime.datetime.now()
145 pull_request.updated_on = datetime.datetime.now()
146 self.sa.add(pull_request)
146 self.sa.add(pull_request)
147
147
148 def _get_changesets(self, alias, org_repo, org_ref, other_repo, other_ref,
148 def _get_changesets(self, alias, org_repo, org_ref, other_repo, other_ref,
149 discovery_data):
149 discovery_data):
150 """
150 """
151 Returns a list of changesets that are incoming from org_repo@org_ref
151 Returns a list of changesets that are incoming from org_repo@org_ref
152 to other_repo@other_ref
152 to other_repo@other_ref
153
153
154 :param org_repo:
154 :param org_repo:
155 :type org_repo:
155 :type org_repo:
156 :param org_ref:
156 :param org_ref:
157 :type org_ref:
157 :type org_ref:
158 :param other_repo:
158 :param other_repo:
159 :type other_repo:
159 :type other_repo:
160 :param other_ref:
160 :param other_ref:
161 :type other_ref:
161 :type other_ref:
162 :param tmp:
162 :param tmp:
163 :type tmp:
163 :type tmp:
164 """
164 """
165 changesets = []
165 changesets = []
166 #case two independent repos
166 #case two independent repos
167 common, incoming, rheads = discovery_data
167 common, incoming, rheads = discovery_data
168 if org_repo != other_repo and incoming:
168 if org_repo != other_repo and incoming:
169 obj = findcommonoutgoing(org_repo._repo,
169 obj = findcommonoutgoing(org_repo._repo,
170 localrepo.locallegacypeer(other_repo._repo.local()),
170 localrepo.locallegacypeer(other_repo._repo.local()),
171 force=True)
171 force=True)
172 revs = obj.missing
172 revs = obj.missing
173
173
174 for cs in reversed(map(binascii.hexlify, revs)):
174 for cs in reversed(map(binascii.hexlify, revs)):
175 changesets.append(org_repo.get_changeset(cs))
175 changesets.append(org_repo.get_changeset(cs))
176 else:
176 else:
177 #no remote compare do it on the same repository
177 #no remote compare do it on the same repository
178 if alias == 'hg':
178 if alias == 'hg':
179 _revset_predicates = {
179 _revset_predicates = {
180 'branch': 'branch',
180 'branch': 'branch',
181 'book': 'bookmark',
181 'book': 'bookmark',
182 'tag': 'tag',
182 'tag': 'tag',
183 'rev': 'id',
183 'rev': 'id',
184 }
184 }
185
185
186 revs = [
186 revs = [
187 "ancestors(%s('%s')) and not ancestors(%s('%s'))" % (
187 "ancestors(%s('%s')) and not ancestors(%s('%s'))" % (
188 _revset_predicates[other_ref[0]], other_ref[1],
188 _revset_predicates[org_ref[0]], org_ref[1],
189 _revset_predicates[org_ref[0]], org_ref[1],
189 _revset_predicates[other_ref[0]], other_ref[1]
190 )
190 )
191 ]
191 ]
192
192
193 out = scmutil.revrange(org_repo._repo, revs)
193 out = scmutil.revrange(org_repo._repo, revs)
194 for cs in reversed(out):
194 for cs in (out):
195 changesets.append(org_repo.get_changeset(cs))
195 changesets.append(org_repo.get_changeset(cs))
196 elif alias == 'git':
196 elif alias == 'git':
197 so, se = org_repo.run_git_command(
197 so, se = org_repo.run_git_command(
198 'log --pretty="format: %%H" -s -p %s..%s' % (org_ref[1],
198 'log --reverse --pretty="format: %%H" -s -p %s..%s' % (org_ref[1],
199 other_ref[1])
199 other_ref[1])
200 )
200 )
201 ids = re.findall(r'[0-9a-fA-F]{40}', so)
201 ids = re.findall(r'[0-9a-fA-F]{40}', so)
202 for cs in reversed(ids):
202 for cs in (ids):
203 changesets.append(org_repo.get_changeset(cs))
203 changesets.append(org_repo.get_changeset(cs))
204
204
205 return changesets
205 return changesets
206
206
207 def _get_discovery(self, org_repo, org_ref, other_repo, other_ref):
207 def _get_discovery(self, org_repo, org_ref, other_repo, other_ref):
208 """
208 """
209 Get's mercurial discovery data used to calculate difference between
209 Get's mercurial discovery data used to calculate difference between
210 repos and refs
210 repos and refs
211
211
212 :param org_repo:
212 :param org_repo:
213 :type org_repo:
213 :type org_repo:
214 :param org_ref:
214 :param org_ref:
215 :type org_ref:
215 :type org_ref:
216 :param other_repo:
216 :param other_repo:
217 :type other_repo:
217 :type other_repo:
218 :param other_ref:
218 :param other_ref:
219 :type other_ref:
219 :type other_ref:
220 """
220 """
221
221
222 _org_repo = org_repo._repo
222 _org_repo = org_repo._repo
223 org_rev_type, org_rev = org_ref
223 org_rev_type, org_rev = org_ref
224
224
225 _other_repo = other_repo._repo
225 _other_repo = other_repo._repo
226 other_rev_type, other_rev = other_ref
226 other_rev_type, other_rev = other_ref
227
227
228 log.debug('Doing discovery for %s@%s vs %s@%s' % (
228 log.debug('Doing discovery for %s@%s vs %s@%s' % (
229 org_repo, org_ref, other_repo, other_ref)
229 org_repo, org_ref, other_repo, other_ref)
230 )
230 )
231
231
232 #log.debug('Filter heads are %s[%s]' % ('', org_ref[1]))
232 #log.debug('Filter heads are %s[%s]' % ('', org_ref[1]))
233 org_peer = localrepo.locallegacypeer(_org_repo.local())
233 org_peer = localrepo.locallegacypeer(_org_repo.local())
234 tmp = discovery.findcommonincoming(
234 tmp = discovery.findcommonincoming(
235 repo=_other_repo, # other_repo we check for incoming
235 repo=_other_repo, # other_repo we check for incoming
236 remote=org_peer, # org_repo source for incoming
236 remote=org_peer, # org_repo source for incoming
237 heads=[_other_repo[other_rev].node(),
237 heads=[_other_repo[other_rev].node(),
238 _org_repo[org_rev].node()],
238 _org_repo[org_rev].node()],
239 force=True
239 force=True
240 )
240 )
241 return tmp
241 return tmp
242
242
243 def get_compare_data(self, org_repo, org_ref, other_repo, other_ref):
243 def get_compare_data(self, org_repo, org_ref, other_repo, other_ref):
244 """
244 """
245 Returns a tuple of incomming changesets, and discoverydata cache for
245 Returns a tuple of incomming changesets, and discoverydata cache for
246 mercurial repositories
246 mercurial repositories
247
247
248 :param org_repo:
248 :param org_repo:
249 :type org_repo:
249 :type org_repo:
250 :param org_ref:
250 :param org_ref:
251 :type org_ref:
251 :type org_ref:
252 :param other_repo:
252 :param other_repo:
253 :type other_repo:
253 :type other_repo:
254 :param other_ref:
254 :param other_ref:
255 :type other_ref:
255 :type other_ref:
256 """
256 """
257
257
258 if len(org_ref) != 2 or not isinstance(org_ref, (list, tuple)):
258 if len(org_ref) != 2 or not isinstance(org_ref, (list, tuple)):
259 raise Exception('org_ref must be a two element list/tuple')
259 raise Exception('org_ref must be a two element list/tuple')
260
260
261 if len(other_ref) != 2 or not isinstance(org_ref, (list, tuple)):
261 if len(other_ref) != 2 or not isinstance(org_ref, (list, tuple)):
262 raise Exception('other_ref must be a two element list/tuple')
262 raise Exception('other_ref must be a two element list/tuple')
263
263
264 org_repo_scm = org_repo.scm_instance
264 org_repo_scm = org_repo.scm_instance
265 other_repo_scm = other_repo.scm_instance
265 other_repo_scm = other_repo.scm_instance
266
266
267 alias = org_repo.scm_instance.alias
267 alias = org_repo.scm_instance.alias
268 discovery_data = [None, None, None]
268 discovery_data = [None, None, None]
269 if alias == 'hg':
269 if alias == 'hg':
270 discovery_data = self._get_discovery(org_repo_scm, org_ref,
270 discovery_data = self._get_discovery(org_repo_scm, org_ref,
271 other_repo_scm, other_ref)
271 other_repo_scm, other_ref)
272 cs_ranges = self._get_changesets(alias,
272 cs_ranges = self._get_changesets(alias,
273 org_repo_scm, org_ref,
273 org_repo_scm, org_ref,
274 other_repo_scm, other_ref,
274 other_repo_scm, other_ref,
275 discovery_data)
275 discovery_data)
276
276
277 return cs_ranges, discovery_data
277 return cs_ranges, discovery_data
@@ -1,122 +1,122
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.html"/>
2 <%inherit file="/base/base.html"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 ${_('%s Changesets') % c.repo_name} - r${c.cs_ranges[0].revision}:${h.short_id(c.cs_ranges[0].raw_id)} -> r${c.cs_ranges[-1].revision}:${h.short_id(c.cs_ranges[-1].raw_id)} - ${c.rhodecode_name}
5 ${_('%s Changesets') % c.repo_name} - r${c.cs_ranges[0].revision}:${h.short_id(c.cs_ranges[0].raw_id)} -> r${c.cs_ranges[-1].revision}:${h.short_id(c.cs_ranges[-1].raw_id)} - ${c.rhodecode_name}
6 </%def>
6 </%def>
7
7
8 <%def name="breadcrumbs_links()">
8 <%def name="breadcrumbs_links()">
9 ${h.link_to(_(u'Home'),h.url('/'))}
9 ${h.link_to(_(u'Home'),h.url('/'))}
10 &raquo;
10 &raquo;
11 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
11 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
12 &raquo;
12 &raquo;
13 ${_('Changesets')} - r${c.cs_ranges[0].revision}:${h.short_id(c.cs_ranges[0].raw_id)} -> r${c.cs_ranges[-1].revision}:${h.short_id(c.cs_ranges[-1].raw_id)}
13 ${_('Changesets')} - r${c.cs_ranges[0].revision}:${h.short_id(c.cs_ranges[0].raw_id)} -> r${c.cs_ranges[-1].revision}:${h.short_id(c.cs_ranges[-1].raw_id)}
14 </%def>
14 </%def>
15
15
16 <%def name="page_nav()">
16 <%def name="page_nav()">
17 ${self.menu('changelog')}
17 ${self.menu('changelog')}
18 </%def>
18 </%def>
19
19
20 <%def name="main()">
20 <%def name="main()">
21 <div class="box">
21 <div class="box">
22 <!-- box / title -->
22 <!-- box / title -->
23 <div class="title">
23 <div class="title">
24 ${self.breadcrumbs()}
24 ${self.breadcrumbs()}
25 </div>
25 </div>
26 <div class="table">
26 <div class="table">
27 <div id="body" class="diffblock">
27 <div id="body" class="diffblock">
28 <div class="code-header cv">
28 <div class="code-header cv">
29 <h3 class="code-header-title">${_('Compare View')}</h3>
29 <h3 class="code-header-title">${_('Compare View')} / ${h.link_to(_('Show combined compare'),h.url('compare_url',repo_name=c.repo_name,org_ref_type='rev',org_ref=getattr(c.cs_ranges[0].parents[0] if c.cs_ranges[0].parents else h.EmptyChangeset(),'raw_id'),other_ref_type='rev',other_ref=c.cs_ranges[-1].raw_id))}</h3>
30 <div>
30 <div>
31 ${_('Changesets')} - r${c.cs_ranges[0].revision}:${h.short_id(c.cs_ranges[0].raw_id)} -> r${c.cs_ranges[-1].revision}:${h.short_id(c.cs_ranges[-1].raw_id)}
31 ${_('Changesets')} - r${c.cs_ranges[0].revision}:${h.short_id(c.cs_ranges[0].raw_id)} -> r${c.cs_ranges[-1].revision}:${h.short_id(c.cs_ranges[-1].raw_id)}
32 </div>
32 </div>
33 </div>
33 </div>
34 </div>
34 </div>
35 <div id="changeset_compare_view_content">
35 <div id="changeset_compare_view_content">
36 <div class="container">
36 <div class="container">
37 <table class="compare_view_commits noborder">
37 <table class="compare_view_commits noborder">
38 %for cnt,cs in enumerate(c.cs_ranges):
38 %for cnt,cs in enumerate(c.cs_ranges):
39 <tr>
39 <tr>
40 <td><div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(h.email_or_none(cs.author),14)}"/></div></td>
40 <td><div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(h.email_or_none(cs.author),14)}"/></div></td>
41 <td>${h.link_to('r%s:%s' % (cs.revision,h.short_id(cs.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</td>
41 <td>${h.link_to('r%s:%s' % (cs.revision,h.short_id(cs.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</td>
42 <td><div class="author">${h.person(cs.author)}</div></td>
42 <td><div class="author">${h.person(cs.author)}</div></td>
43 <td><span class="tooltip" title="${h.age(cs.date)}">${cs.date}</span></td>
43 <td><span class="tooltip" title="${h.age(cs.date)}">${cs.date}</span></td>
44 <td>
44 <td>
45 %if c.statuses:
45 %if c.statuses:
46 <div title="${h.tooltip(_('Changeset status'))}" class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses[cnt])}" /></div>
46 <div title="${h.tooltip(_('Changeset status'))}" class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses[cnt])}" /></div>
47 %endif
47 %endif
48 </td>
48 </td>
49 <td><div class="message">${h.urlify_commit(h.wrap_paragraphs(cs.message),c.repo_name)}</div></td>
49 <td><div class="message">${h.urlify_commit(h.wrap_paragraphs(cs.message),c.repo_name)}</div></td>
50 </tr>
50 </tr>
51 %endfor
51 %endfor
52 </table>
52 </table>
53 </div>
53 </div>
54 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Files affected')}</div>
54 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Files affected')}</div>
55 <div class="cs_files">
55 <div class="cs_files">
56 %for cs in c.cs_ranges:
56 %for cs in c.cs_ranges:
57 <div class="cur_cs">${h.link_to('r%s:%s' % (cs.revision,h.short_id(cs.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</div>
57 <div class="cur_cs">${h.link_to('r%s:%s' % (cs.revision,h.short_id(cs.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</div>
58 %for FID, (cs1, cs2, change, path, diff, stats) in c.changes[cs.raw_id].iteritems():
58 %for FID, (cs1, cs2, change, path, diff, stats) in c.changes[cs.raw_id].iteritems():
59 <div class="cs_${change}">${h.link_to(h.safe_unicode(path),h.url.current(anchor=FID))}</div>
59 <div class="cs_${change}">${h.link_to(h.safe_unicode(path),h.url.current(anchor=FID))}</div>
60 %endfor
60 %endfor
61 %endfor
61 %endfor
62 </div>
62 </div>
63 </div>
63 </div>
64
64
65 </div>
65 </div>
66 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
66 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
67 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
67 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
68 %for cs in c.cs_ranges:
68 %for cs in c.cs_ranges:
69 ##${comment.comment_inline_form(cs)}
69 ##${comment.comment_inline_form(cs)}
70 ## diff block
70 ## diff block
71 <div class="h3">
71 <div class="h3">
72 <a class="tooltip" title="${h.tooltip(cs.message)}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id)}">${'r%s:%s' % (cs.revision,h.short_id(cs.raw_id))}</a>
72 <a class="tooltip" title="${h.tooltip(cs.message)}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id)}">${'r%s:%s' % (cs.revision,h.short_id(cs.raw_id))}</a>
73 <div class="gravatar">
73 <div class="gravatar">
74 <img alt="gravatar" src="${h.gravatar_url(h.email_or_none(cs.author),20)}"/>
74 <img alt="gravatar" src="${h.gravatar_url(h.email_or_none(cs.author),20)}"/>
75 </div>
75 </div>
76 <div class="right">
76 <div class="right">
77 <span class="logtags">
77 <span class="logtags">
78 %if len(cs.parents)>1:
78 %if len(cs.parents)>1:
79 <span class="merge">${_('merge')}</span>
79 <span class="merge">${_('merge')}</span>
80 %endif
80 %endif
81 %if cs.branch:
81 %if cs.branch:
82 <span class="branchtag" title="${'%s %s' % (_('branch'),cs.branch)}">
82 <span class="branchtag" title="${'%s %s' % (_('branch'),cs.branch)}">
83 ${h.link_to(h.shorter(cs.branch),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}
83 ${h.link_to(h.shorter(cs.branch),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}
84 </span>
84 </span>
85 %endif
85 %endif
86 %if h.is_hg(c.rhodecode_repo):
86 %if h.is_hg(c.rhodecode_repo):
87 %for book in cs.bookmarks:
87 %for book in cs.bookmarks:
88 <span class="bookbook" title="${'%s %s' % (_('bookmark'),book)}">
88 <span class="bookbook" title="${'%s %s' % (_('bookmark'),book)}">
89 ${h.link_to(h.shorter(book),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}
89 ${h.link_to(h.shorter(book),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}
90 </span>
90 </span>
91 %endfor
91 %endfor
92 %endif
92 %endif
93 %for tag in cs.tags:
93 %for tag in cs.tags:
94 <span class="tagtag" title="${'%s %s' % (_('tag'),tag)}">
94 <span class="tagtag" title="${'%s %s' % (_('tag'),tag)}">
95 ${h.link_to(h.shorter(tag),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
95 ${h.link_to(h.shorter(tag),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
96 %endfor
96 %endfor
97 </span>
97 </span>
98 </div>
98 </div>
99 </div>
99 </div>
100 ${diff_block.diff_block(c.changes[cs.raw_id])}
100 ${diff_block.diff_block(c.changes[cs.raw_id])}
101 ##${comment.comments(cs)}
101 ##${comment.comments(cs)}
102
102
103 %endfor
103 %endfor
104 <script type="text/javascript">
104 <script type="text/javascript">
105
105
106 YUE.onDOMReady(function(){
106 YUE.onDOMReady(function(){
107
107
108 YUE.on(YUQ('.diff-menu-activate'),'click',function(e){
108 YUE.on(YUQ('.diff-menu-activate'),'click',function(e){
109 var act = e.currentTarget.nextElementSibling;
109 var act = e.currentTarget.nextElementSibling;
110
110
111 if(YUD.hasClass(act,'active')){
111 if(YUD.hasClass(act,'active')){
112 YUD.removeClass(act,'active');
112 YUD.removeClass(act,'active');
113 YUD.setStyle(act,'display','none');
113 YUD.setStyle(act,'display','none');
114 }else{
114 }else{
115 YUD.addClass(act,'active');
115 YUD.addClass(act,'active');
116 YUD.setStyle(act,'display','');
116 YUD.setStyle(act,'display','');
117 }
117 }
118 });
118 });
119 })
119 })
120 </script>
120 </script>
121 </div>
121 </div>
122 </%def>
122 </%def>
@@ -1,28 +1,28
1 ## Changesets table !
1 ## Changesets table !
2 <div class="container">
2 <div class="container">
3 <table class="compare_view_commits noborder">
3 <table class="compare_view_commits noborder">
4 %if not c.cs_ranges:
4 %if not c.cs_ranges:
5 <tr><td>${_('No changesets')}</td></tr>
5 <span class="empty_data">${_('No changesets')}</span>
6 %else:
6 %else:
7 %for cnt, cs in enumerate(c.cs_ranges):
7 %for cnt, cs in enumerate(c.cs_ranges):
8 <tr>
8 <tr>
9 <td><div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(h.email_or_none(cs.author),14)}"/></div></td>
9 <td><div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(h.email_or_none(cs.author),14)}"/></div></td>
10 <td>
10 <td>
11 %if cs.raw_id in c.statuses:
11 %if cs.raw_id in c.statuses:
12 <div title="${c.statuses[cs.raw_id][1]}" class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses[cs.raw_id][0])}" /></div>
12 <div title="${c.statuses[cs.raw_id][1]}" class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses[cs.raw_id][0])}" /></div>
13 %endif
13 %endif
14 </td>
14 </td>
15 <td>${h.link_to('r%s:%s' % (cs.revision,h.short_id(cs.raw_id)),h.url('changeset_home',repo_name=c.target_repo,revision=cs.raw_id))}
15 <td>${h.link_to('r%s:%s' % (cs.revision,h.short_id(cs.raw_id)),h.url('changeset_home',repo_name=c.target_repo,revision=cs.raw_id))}
16 %if c.as_form:
16 %if c.as_form:
17 ${h.hidden('revisions',cs.raw_id)}
17 ${h.hidden('revisions',cs.raw_id)}
18 %endif
18 %endif
19 </td>
19 </td>
20 <td><div class="author">${h.person(cs.author)}</div></td>
20 <td><div class="author">${h.person(cs.author)}</div></td>
21 <td><span class="tooltip" title="${h.tooltip(h.age(cs.date))}">${cs.date}</span></td>
21 <td><span class="tooltip" title="${h.tooltip(h.age(cs.date))}">${cs.date}</span></td>
22 <td><div class="message tooltip" title="${h.tooltip(cs.message)}" style="white-space:normal">${h.urlify_commit(h.shorter(cs.message, 60),c.repo_name)}</div></td>
22 <td><div class="message tooltip" title="${h.tooltip(cs.message)}" style="white-space:normal">${h.urlify_commit(h.shorter(cs.message, 60),c.repo_name)}</div></td>
23 </tr>
23 </tr>
24 %endfor
24 %endfor
25
25
26 %endif
26 %endif
27 </table>
27 </table>
28 </div>
28 </div>
@@ -1,82 +1,93
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.html"/>
2 <%inherit file="/base/base.html"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 ${c.repo_name} ${_('Compare')} ${'%s@%s' % (c.org_repo.repo_name, c.org_ref)} -> ${'%s@%s' % (c.other_repo.repo_name, c.other_ref)}
5 ${c.repo_name} ${_('Compare')} ${'%s@%s' % (c.org_repo.repo_name, c.org_ref)} -> ${'%s@%s' % (c.other_repo.repo_name, c.other_ref)}
6 </%def>
6 </%def>
7
7
8 <%def name="breadcrumbs_links()">
8 <%def name="breadcrumbs_links()">
9 ${h.link_to(_(u'Home'),h.url('/'))}
9 ${h.link_to(_(u'Home'),h.url('/'))}
10 &raquo;
10 &raquo;
11 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
11 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
12 &raquo;
12 &raquo;
13 ${_('Compare')}
13 ${_('Compare')}
14 </%def>
14 </%def>
15
15
16 <%def name="page_nav()">
16 <%def name="page_nav()">
17 ${self.menu('changelog')}
17 ${self.menu('changelog')}
18 </%def>
18 </%def>
19
19
20 <%def name="main()">
20 <%def name="main()">
21 <div class="box">
21 <div class="box">
22 <!-- box / title -->
22 <!-- box / title -->
23 <div class="title">
23 <div class="title">
24 ${self.breadcrumbs()}
24 ${self.breadcrumbs()}
25 </div>
25 </div>
26 <div class="table">
26 <div class="table">
27 <div id="body" class="diffblock">
27 <div id="body" class="diffblock">
28 <div class="code-header cv">
28 <div class="code-header cv">
29 <h3 class="code-header-title">${_('Compare View')}</h3>
29 <h3 class="code-header-title">${_('Compare View')}</h3>
30 <div>
30 <div>
31 ${'%s@%s' % (c.org_repo.repo_name, c.org_ref)} -> ${'%s@%s' % (c.other_repo.repo_name, c.other_ref)} <a href="${c.swap_url}">[swap]</a>
31 ${'%s@%s' % (c.org_repo.repo_name, c.org_ref)} -> ${'%s@%s' % (c.other_repo.repo_name, c.other_ref)} <a href="${c.swap_url}">[swap]</a>
32 </div>
32 </div>
33 </div>
33 </div>
34 </div>
34 </div>
35 <div id="changeset_compare_view_content">
35 <div id="changeset_compare_view_content">
36 ##CS
36 ##CS
37 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Outgoing changesets')}</div>
37 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}</div>
38 <%include file="compare_cs.html" />
38 <%include file="compare_cs.html" />
39
39
40 ## FILES
40 ## FILES
41 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Files affected')}</div>
41 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
42
43 % if c.limited_diff:
44 ${ungettext('%s file changed', '%s files changed', len(c.files)) % len(c.files)}
45 % else:
46 ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.files)) % (len(c.files),c.lines_added,c.lines_deleted)}:
47 %endif
48
49 </div>
42 <div class="cs_files">
50 <div class="cs_files">
51 %if not c.files:
52 <span class="empty_data">${_('No files')}</span>
53 %endif
43 %for fid, change, f, stat in c.files:
54 %for fid, change, f, stat in c.files:
44 <div class="cs_${change}">
55 <div class="cs_${change}">
45 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
56 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
46 <div class="changes">${h.fancy_file_stats(stat)}</div>
57 <div class="changes">${h.fancy_file_stats(stat)}</div>
47 </div>
58 </div>
48 %endfor
59 %endfor
49 </div>
60 </div>
50 % if c.limited_diff:
61 % if c.limited_diff:
51 <h5>${_('Changeset was too big and was cut off...')}</h5>
62 <h5>${_('Changeset was too big and was cut off...')}</h5>
52 % endif
63 % endif
53 </div>
64 </div>
54 </div>
65 </div>
55
66
56 ## diff block
67 ## diff block
57 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
68 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
58 %for fid, change, f, stat in c.files:
69 %for fid, change, f, stat in c.files:
59 ${diff_block.diff_block_simple([c.changes[fid]])}
70 ${diff_block.diff_block_simple([c.changes[fid]])}
60 %endfor
71 %endfor
61 % if c.limited_diff:
72 % if c.limited_diff:
62 <h4>${_('Changeset was too big and was cut off...')}</h4>
73 <h4>${_('Changeset was too big and was cut off...')}</h4>
63 % endif
74 % endif
64 <script type="text/javascript">
75 <script type="text/javascript">
65
76
66 YUE.onDOMReady(function(){
77 YUE.onDOMReady(function(){
67
78
68 YUE.on(YUQ('.diff-menu-activate'),'click',function(e){
79 YUE.on(YUQ('.diff-menu-activate'),'click',function(e){
69 var act = e.currentTarget.nextElementSibling;
80 var act = e.currentTarget.nextElementSibling;
70
81
71 if(YUD.hasClass(act,'active')){
82 if(YUD.hasClass(act,'active')){
72 YUD.removeClass(act,'active');
83 YUD.removeClass(act,'active');
73 YUD.setStyle(act,'display','none');
84 YUD.setStyle(act,'display','none');
74 }else{
85 }else{
75 YUD.addClass(act,'active');
86 YUD.addClass(act,'active');
76 YUD.setStyle(act,'display','');
87 YUD.setStyle(act,'display','');
77 }
88 }
78 });
89 });
79 })
90 })
80 </script>
91 </script>
81 </div>
92 </div>
82 </%def>
93 </%def>
@@ -1,403 +1,563
1 from rhodecode.tests import *
1 from rhodecode.tests import *
2 from rhodecode.model.repo import RepoModel
2 from rhodecode.model.repo import RepoModel
3 from rhodecode.model.meta import Session
3 from rhodecode.model.meta import Session
4 from rhodecode.model.db import Repository
4 from rhodecode.model.db import Repository
5 from rhodecode.model.scm import ScmModel
5 from rhodecode.model.scm import ScmModel
6 from rhodecode.lib.vcs.backends.base import EmptyChangeset
6 from rhodecode.lib.vcs.backends.base import EmptyChangeset
7
7
8
8
9 class TestCompareController(TestController):
9 class TestCompareController(TestController):
10
10
11 def test_index_tag(self):
11 def test_compare_tag_hg(self):
12 self.log_user()
12 self.log_user()
13 tag1 = '0.1.3'
13 tag1 = '0.1.2'
14 tag2 = '0.1.2'
14 tag2 = '0.1.3'
15 response = self.app.get(url(controller='compare', action='index',
15 response = self.app.get(url(controller='compare', action='index',
16 repo_name=HG_REPO,
16 repo_name=HG_REPO,
17 org_ref_type="tag",
17 org_ref_type="tag",
18 org_ref=tag1,
18 org_ref=tag1,
19 other_ref_type="tag",
19 other_ref_type="tag",
20 other_ref=tag2,
20 other_ref=tag2,
21 ))
21 ))
22 response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, tag1, HG_REPO, tag2))
22 response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, tag1, HG_REPO, tag2))
23 ## outgoing changesets between tags
23 ## outgoing changesets between tags
24 response.mustcontain('''<a href="/%s/changeset/17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">r120:17544fbfcd33</a>''' % HG_REPO)
24 response.mustcontain('''<a href="/%s/changeset/c5ddebc06eaaba3010c2d66ea6ec9d074eb0f678">r112:c5ddebc06eaa</a>''' % HG_REPO)
25 response.mustcontain('''<a href="/%s/changeset/36e0fc9d2808c5022a24f49d6658330383ed8666">r119:36e0fc9d2808</a>''' % HG_REPO)
25 response.mustcontain('''<a href="/%s/changeset/70d4cef8a37657ee4cf5aabb3bd9f68879769816">r115:70d4cef8a376</a>''' % HG_REPO)
26 response.mustcontain('''<a href="/%s/changeset/bb1a3ab98cc45cb934a77dcabf87a5a598b59e97">r118:bb1a3ab98cc4</a>''' % HG_REPO)
26 response.mustcontain('''<a href="/%s/changeset/9749bfbfc0d2eba208d7947de266303b67c87cda">r116:9749bfbfc0d2</a>''' % HG_REPO)
27 response.mustcontain('''<a href="/%s/changeset/41fda979f02fda216374bf8edac4e83f69e7581c">r117:41fda979f02f</a>''' % HG_REPO)
27 response.mustcontain('''<a href="/%s/changeset/41fda979f02fda216374bf8edac4e83f69e7581c">r117:41fda979f02f</a>''' % HG_REPO)
28 response.mustcontain('''<a href="/%s/changeset/9749bfbfc0d2eba208d7947de266303b67c87cda">r116:9749bfbfc0d2</a>''' % HG_REPO)
28 response.mustcontain('''<a href="/%s/changeset/bb1a3ab98cc45cb934a77dcabf87a5a598b59e97">r118:bb1a3ab98cc4</a>''' % HG_REPO)
29 response.mustcontain('''<a href="/%s/changeset/70d4cef8a37657ee4cf5aabb3bd9f68879769816">r115:70d4cef8a376</a>''' % HG_REPO)
29 response.mustcontain('''<a href="/%s/changeset/36e0fc9d2808c5022a24f49d6658330383ed8666">r119:36e0fc9d2808</a>''' % HG_REPO)
30 response.mustcontain('''<a href="/%s/changeset/c5ddebc06eaaba3010c2d66ea6ec9d074eb0f678">r112:c5ddebc06eaa</a>''' % HG_REPO)
30 response.mustcontain('''<a href="/%s/changeset/17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">r120:17544fbfcd33</a>''' % HG_REPO)
31
31
32 ## files diff
32 ## files diff
33 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--1c5cf9e91c12">docs/api/utils/index.rst</a></div>''' % (HG_REPO, tag1, tag2))
33 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--1c5cf9e91c12">docs/api/utils/index.rst</a></div>''' % (HG_REPO, tag1, tag2))
34 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--e3305437df55">test_and_report.sh</a></div>''' % (HG_REPO, tag1, tag2))
34 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--e3305437df55">test_and_report.sh</a></div>''' % (HG_REPO, tag1, tag2))
35 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--c8e92ef85cd1">.hgignore</a></div>''' % (HG_REPO, tag1, tag2))
35 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--c8e92ef85cd1">.hgignore</a></div>''' % (HG_REPO, tag1, tag2))
36 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--6e08b694d687">.hgtags</a></div>''' % (HG_REPO, tag1, tag2))
36 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--6e08b694d687">.hgtags</a></div>''' % (HG_REPO, tag1, tag2))
37 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--2c14b00f3393">docs/api/index.rst</a></div>''' % (HG_REPO, tag1, tag2))
37 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--2c14b00f3393">docs/api/index.rst</a></div>''' % (HG_REPO, tag1, tag2))
38 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--430ccbc82bdf">vcs/__init__.py</a></div>''' % (HG_REPO, tag1, tag2))
38 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--430ccbc82bdf">vcs/__init__.py</a></div>''' % (HG_REPO, tag1, tag2))
39 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--9c390eb52cd6">vcs/backends/hg.py</a></div>''' % (HG_REPO, tag1, tag2))
39 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--9c390eb52cd6">vcs/backends/hg.py</a></div>''' % (HG_REPO, tag1, tag2))
40 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--ebb592c595c0">vcs/utils/__init__.py</a></div>''' % (HG_REPO, tag1, tag2))
40 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--ebb592c595c0">vcs/utils/__init__.py</a></div>''' % (HG_REPO, tag1, tag2))
41 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--7abc741b5052">vcs/utils/annotate.py</a></div>''' % (HG_REPO, tag1, tag2))
41 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--7abc741b5052">vcs/utils/annotate.py</a></div>''' % (HG_REPO, tag1, tag2))
42 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--2ef0ef106c56">vcs/utils/diffs.py</a></div>''' % (HG_REPO, tag1, tag2))
42 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--2ef0ef106c56">vcs/utils/diffs.py</a></div>''' % (HG_REPO, tag1, tag2))
43 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--3150cb87d4b7">vcs/utils/lazy.py</a></div>''' % (HG_REPO, tag1, tag2))
43 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--3150cb87d4b7">vcs/utils/lazy.py</a></div>''' % (HG_REPO, tag1, tag2))
44
44
45 def test_index_branch(self):
45 def test_compare_tag_git(self):
46 self.log_user()
47 tag1 = 'v0.1.2'
48 tag2 = 'v0.1.3'
49 response = self.app.get(url(controller='compare', action='index',
50 repo_name=GIT_REPO,
51 org_ref_type="tag",
52 org_ref=tag1,
53 other_ref_type="tag",
54 other_ref=tag2,
55 bundle=False
56 ))
57 response.mustcontain('%s@%s -> %s@%s' % (GIT_REPO, tag1, GIT_REPO, tag2))
58
59 ## outgoing changesets between tags
60 response.mustcontain('''<a href="/%s/changeset/794bbdd31545c199f74912709ea350dedcd189a2">r113:794bbdd31545</a>''' % GIT_REPO)
61 response.mustcontain('''<a href="/%s/changeset/e36d8c5025329bdd4212bd53d4ed8a70ff44985f">r115:e36d8c502532</a>''' % GIT_REPO)
62 response.mustcontain('''<a href="/%s/changeset/5c9ff4f6d7508db0e72b1d2991c357d0d8e07af2">r116:5c9ff4f6d750</a>''' % GIT_REPO)
63 response.mustcontain('''<a href="/%s/changeset/b7187fa2b8c1d773ec35e9dee12f01f74808c879">r117:b7187fa2b8c1</a>''' % GIT_REPO)
64 response.mustcontain('''<a href="/%s/changeset/5f3b74262014a8de2dc7dade1152de9fd0c8efef">r118:5f3b74262014</a>''' % GIT_REPO)
65 response.mustcontain('''<a href="/%s/changeset/17438a11f72b93f56d0e08e7d1fa79a378578a82">r119:17438a11f72b</a>''' % GIT_REPO)
66 response.mustcontain('''<a href="/%s/changeset/5a3a8fb005554692b16e21dee62bf02667d8dc3e">r120:5a3a8fb00555</a>''' % GIT_REPO)
67
68 #files
69 response.mustcontain('''<a href="/%s/compare/tag@%s...tag@%s#C--1c5cf9e91c12">docs/api/utils/index.rst</a>''' % (GIT_REPO, tag1, tag2))
70 response.mustcontain('''<a href="/%s/compare/tag@%s...tag@%s#C--e3305437df55">test_and_report.sh</a>''' % (GIT_REPO, tag1, tag2))
71 response.mustcontain('''<a href="/%s/compare/tag@%s...tag@%s#C--c8e92ef85cd1">.hgignore</a>''' % (GIT_REPO, tag1, tag2))
72 response.mustcontain('''<a href="/%s/compare/tag@%s...tag@%s#C--6e08b694d687">.hgtags</a>''' % (GIT_REPO, tag1, tag2))
73 response.mustcontain('''<a href="/%s/compare/tag@%s...tag@%s#C--2c14b00f3393">docs/api/index.rst</a>''' % (GIT_REPO, tag1, tag2))
74 response.mustcontain('''<a href="/%s/compare/tag@%s...tag@%s#C--430ccbc82bdf">vcs/__init__.py</a>''' % (GIT_REPO, tag1, tag2))
75 response.mustcontain('''<a href="/%s/compare/tag@%s...tag@%s#C--9c390eb52cd6">vcs/backends/hg.py</a>''' % (GIT_REPO, tag1, tag2))
76 response.mustcontain('''<a href="/%s/compare/tag@%s...tag@%s#C--ebb592c595c0">vcs/utils/__init__.py</a>''' % (GIT_REPO, tag1, tag2))
77 response.mustcontain('''<a href="/%s/compare/tag@%s...tag@%s#C--7abc741b5052">vcs/utils/annotate.py</a>''' % (GIT_REPO, tag1, tag2))
78 response.mustcontain('''<a href="/%s/compare/tag@%s...tag@%s#C--2ef0ef106c56">vcs/utils/diffs.py</a>''' % (GIT_REPO, tag1, tag2))
79 response.mustcontain('''<a href="/%s/compare/tag@%s...tag@%s#C--3150cb87d4b7">vcs/utils/lazy.py</a>''' % (GIT_REPO, tag1, tag2))
80
81 def test_index_branch_hg(self):
46 self.log_user()
82 self.log_user()
47 response = self.app.get(url(controller='compare', action='index',
83 response = self.app.get(url(controller='compare', action='index',
48 repo_name=HG_REPO,
84 repo_name=HG_REPO,
49 org_ref_type="branch",
85 org_ref_type="branch",
50 org_ref='default',
86 org_ref='default',
51 other_ref_type="branch",
87 other_ref_type="branch",
52 other_ref='default',
88 other_ref='default',
53 ))
89 ))
54
90
55 response.mustcontain('%s@default -> %s@default' % (HG_REPO, HG_REPO))
91 response.mustcontain('%s@default -> %s@default' % (HG_REPO, HG_REPO))
56 # branch are equal
92 # branch are equal
57 response.mustcontain('<tr><td>No changesets</td></tr>')
93 response.mustcontain('<span class="empty_data">No files</span>')
94 response.mustcontain('<span class="empty_data">No changesets</span>')
95
96 def test_index_branch_git(self):
97 self.log_user()
98 response = self.app.get(url(controller='compare', action='index',
99 repo_name=GIT_REPO,
100 org_ref_type="branch",
101 org_ref='master',
102 other_ref_type="branch",
103 other_ref='master',
104 ))
105
106 response.mustcontain('%s@master -> %s@master' % (GIT_REPO, GIT_REPO))
107 # branch are equal
108 response.mustcontain('<span class="empty_data">No files</span>')
109 response.mustcontain('<span class="empty_data">No changesets</span>')
58
110
59 def test_compare_revisions(self):
111 def test_compare_revisions(self):
60 self.log_user()
112 self.log_user()
61 rev1 = '3d8f361e72ab'
113 rev1 = 'b986218ba1c9'
62 rev2 = 'b986218ba1c9'
114 rev2 = '3d8f361e72ab'
115
63 response = self.app.get(url(controller='compare', action='index',
116 response = self.app.get(url(controller='compare', action='index',
64 repo_name=HG_REPO,
117 repo_name=HG_REPO,
65 org_ref_type="rev",
118 org_ref_type="rev",
66 org_ref=rev1,
119 org_ref=rev1,
67 other_ref_type="rev",
120 other_ref_type="rev",
68 other_ref=rev2,
121 other_ref=rev2,
69 ))
122 ))
70 response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, rev1, HG_REPO, rev2))
123 response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, rev1, HG_REPO, rev2))
71 ## outgoing changesets between those revisions
124 ## outgoing changesets between those revisions
72 response.mustcontain("""<a href="/%s/changeset/3d8f361e72ab303da48d799ff1ac40d5ac37c67e">r1:%s</a>""" % (HG_REPO, rev1))
125 response.mustcontain("""<a href="/%s/changeset/3d8f361e72ab303da48d799ff1ac40d5ac37c67e">r1:%s</a>""" % (HG_REPO, rev2))
73
74 ## files
126 ## files
75 response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--c8e92ef85cd1">.hgignore</a>""" % (HG_REPO, rev1, rev2))
127 response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--c8e92ef85cd1">.hgignore</a>""" % (HG_REPO, rev1, rev2))
76
128
77 def test_compare_remote_repos(self):
129 def test_compare_remote_repos(self):
78 self.log_user()
130 self.log_user()
79
131
80 form_data = dict(
132 form_data = dict(
81 repo_name=HG_FORK,
133 repo_name=HG_FORK,
82 repo_name_full=HG_FORK,
134 repo_name_full=HG_FORK,
83 repo_group=None,
135 repo_group=None,
84 repo_type='hg',
136 repo_type='hg',
85 description='',
137 description='',
86 private=False,
138 private=False,
87 copy_permissions=False,
139 copy_permissions=False,
88 landing_rev='tip',
140 landing_rev='tip',
89 update_after_clone=False,
141 update_after_clone=False,
90 fork_parent_id=Repository.get_by_repo_name(HG_REPO),
142 fork_parent_id=Repository.get_by_repo_name(HG_REPO),
91 )
143 )
92 RepoModel().create_fork(form_data, cur_user=TEST_USER_ADMIN_LOGIN)
144 RepoModel().create_fork(form_data, cur_user=TEST_USER_ADMIN_LOGIN)
93
145
94 Session().commit()
146 Session().commit()
95
147
96 rev1 = '7d4bc8ec6be5'
148 rev1 = '56349e29c2af'
97 rev2 = '56349e29c2af'
149 rev2 = '7d4bc8ec6be5'
150
151 response = self.app.get(url(controller='compare', action='index',
152 repo_name=HG_REPO,
153 org_ref_type="rev",
154 org_ref=rev1,
155 other_ref_type="rev",
156 other_ref=rev2,
157 repo=HG_FORK,
158 ))
159
160 try:
161 response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, rev1, HG_FORK, rev2))
162 ## outgoing changesets between those revisions
163
164 response.mustcontain("""<a href="/%s/changeset/2dda4e345facb0ccff1a191052dd1606dba6781d">r4:2dda4e345fac</a>""" % (HG_REPO))
165 response.mustcontain("""<a href="/%s/changeset/6fff84722075f1607a30f436523403845f84cd9e">r5:6fff84722075</a>""" % (HG_REPO))
166 response.mustcontain("""<a href="/%s/changeset/7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7">r6:%s</a>""" % (HG_REPO, rev2))
167
168 ## files
169 response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--9c390eb52cd6">vcs/backends/hg.py</a>""" % (HG_REPO, rev1, rev2))
170 response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--41b41c1f2796">vcs/backends/__init__.py</a>""" % (HG_REPO, rev1, rev2))
171 response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--2f574d260608">vcs/backends/base.py</a>""" % (HG_REPO, rev1, rev2))
172 finally:
173 RepoModel().delete(HG_FORK)
174
175 def test_compare_remote_repos_remote_flag_off(self):
176 self.log_user()
177
178 form_data = dict(
179 repo_name=HG_FORK,
180 repo_name_full=HG_FORK,
181 repo_group=None,
182 repo_type='hg',
183 description='',
184 private=False,
185 copy_permissions=False,
186 landing_rev='tip',
187 update_after_clone=False,
188 fork_parent_id=Repository.get_by_repo_name(HG_REPO),
189 )
190 RepoModel().create_fork(form_data, cur_user=TEST_USER_ADMIN_LOGIN)
191
192 Session().commit()
193
194 rev1 = '56349e29c2af'
195 rev2 = '7d4bc8ec6be5'
98
196
99 response = self.app.get(url(controller='compare', action='index',
197 response = self.app.get(url(controller='compare', action='index',
100 repo_name=HG_REPO,
198 repo_name=HG_REPO,
101 org_ref_type="rev",
199 org_ref_type="rev",
102 org_ref=rev1,
200 org_ref=rev1,
103 other_ref_type="rev",
201 other_ref_type="rev",
104 other_ref=rev2,
202 other_ref=rev2,
105 repo=HG_FORK
203 repo=HG_FORK,
204 bundle=False,
106 ))
205 ))
107
206
108 try:
207 try:
109 response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, rev1, HG_FORK, rev2))
208 response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, rev1, HG_FORK, rev2))
110 ## outgoing changesets between those revisions
209 ## outgoing changesets between those revisions
111
210
112 response.mustcontain("""<a href="/%s/changeset/7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7">r6:%s</a>""" % (HG_REPO, rev1))
211 response.mustcontain("""<a href="/%s/changeset/2dda4e345facb0ccff1a191052dd1606dba6781d">r4:2dda4e345fac</a>""" % (HG_REPO))
113 response.mustcontain("""<a href="/%s/changeset/6fff84722075f1607a30f436523403845f84cd9e">r5:6fff84722075</a>""" % (HG_REPO))
212 response.mustcontain("""<a href="/%s/changeset/6fff84722075f1607a30f436523403845f84cd9e">r5:6fff84722075</a>""" % (HG_REPO))
114 response.mustcontain("""<a href="/%s/changeset/2dda4e345facb0ccff1a191052dd1606dba6781d">r4:2dda4e345fac</a>""" % (HG_REPO))
213 response.mustcontain("""<a href="/%s/changeset/7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7">r6:%s</a>""" % (HG_REPO, rev2))
115
214
116 ## files
215 ## files
117 response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--9c390eb52cd6">vcs/backends/hg.py</a>""" % (HG_REPO, rev1, rev2))
216 response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--9c390eb52cd6">vcs/backends/hg.py</a>""" % (HG_REPO, rev1, rev2))
118 response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--41b41c1f2796">vcs/backends/__init__.py</a>""" % (HG_REPO, rev1, rev2))
217 response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--41b41c1f2796">vcs/backends/__init__.py</a>""" % (HG_REPO, rev1, rev2))
119 response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--2f574d260608">vcs/backends/base.py</a>""" % (HG_REPO, rev1, rev2))
218 response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--2f574d260608">vcs/backends/base.py</a>""" % (HG_REPO, rev1, rev2))
120 finally:
219 finally:
121 RepoModel().delete(HG_FORK)
220 RepoModel().delete(HG_FORK)
122
221
222 # def test_compare_origin_ahead_of_fork(self):
223 # self.log_user()
224 #
225 # form_data = dict(
226 # repo_name=HG_FORK,
227 # repo_name_full=HG_FORK,
228 # repo_group=None,
229 # repo_type='hg',
230 # description='',
231 # private=False,
232 # copy_permissions=False,
233 # landing_rev='tip',
234 # update_after_clone=False,
235 # fork_parent_id=Repository.get_by_repo_name(HG_REPO),
236 # )
237 # RepoModel().create_fork(form_data, cur_user=TEST_USER_ADMIN_LOGIN)
238 #
239 # Session().commit()
240 #
241 # repo1 = Repository.get_by_repo_name(HG_REPO)
242 # r1_name = HG_REPO
243 #
244 # #commit something !
245 # cs0 = ScmModel().create_node(
246 # repo=repo1.scm_instance, repo_name=r1_name,
247 # cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
248 # author=TEST_USER_ADMIN_LOGIN,
249 # message='extra commit1',
250 # content='line1',
251 # f_path='file1'
252 # )
253 #
254 #
255 # rev1 = '56349e29c2af'
256 # rev2 = '7d4bc8ec6be5'
257 #
258 # response = self.app.get(url(controller='compare', action='index',
259 # repo_name=HG_REPO,
260 # org_ref_type="rev",
261 # org_ref=rev1,
262 # other_ref_type="rev",
263 # other_ref=rev2,
264 # repo=HG_FORK,
265 # bundle=False,
266 # ))
267 #
268 # try:
269 # response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, rev1, HG_REPO, rev2))
270 # ## outgoing changesets between those revisions
271 #
272 # response.mustcontain("""<a href="/%s/changeset/2dda4e345facb0ccff1a191052dd1606dba6781d">r4:2dda4e345fac</a>""" % (HG_REPO))
273 # response.mustcontain("""<a href="/%s/changeset/6fff84722075f1607a30f436523403845f84cd9e">r5:6fff84722075</a>""" % (HG_REPO))
274 # response.mustcontain("""<a href="/%s/changeset/7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7">r6:%s</a>""" % (HG_REPO, rev2))
275 #
276 # ## files
277 # response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--9c390eb52cd6">vcs/backends/hg.py</a>""" % (HG_REPO, rev1, rev2))
278 # response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--41b41c1f2796">vcs/backends/__init__.py</a>""" % (HG_REPO, rev1, rev2))
279 # response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--2f574d260608">vcs/backends/base.py</a>""" % (HG_REPO, rev1, rev2))
280 # finally:
281 # RepoModel().delete(HG_FORK)
282
123 def test_compare_extra_commits(self):
283 def test_compare_extra_commits(self):
124 self.log_user()
284 self.log_user()
125
285
126 repo1 = RepoModel().create_repo(repo_name='one', repo_type='hg',
286 repo1 = RepoModel().create_repo(repo_name='one', repo_type='hg',
127 description='diff-test',
287 description='diff-test',
128 owner=TEST_USER_ADMIN_LOGIN)
288 owner=TEST_USER_ADMIN_LOGIN)
129
289
130 repo2 = RepoModel().create_repo(repo_name='one-fork', repo_type='hg',
290 repo2 = RepoModel().create_repo(repo_name='one-fork', repo_type='hg',
131 description='diff-test',
291 description='diff-test',
132 owner=TEST_USER_ADMIN_LOGIN)
292 owner=TEST_USER_ADMIN_LOGIN)
133
293
134 Session().commit()
294 Session().commit()
135 r1_id = repo1.repo_id
295 r1_id = repo1.repo_id
136 r1_name = repo1.repo_name
296 r1_name = repo1.repo_name
137 r2_id = repo2.repo_id
297 r2_id = repo2.repo_id
138 r2_name = repo2.repo_name
298 r2_name = repo2.repo_name
139
299
140 #commit something !
300 #commit something !
141 cs0 = ScmModel().create_node(
301 cs0 = ScmModel().create_node(
142 repo=repo1.scm_instance, repo_name=r1_name,
302 repo=repo1.scm_instance, repo_name=r1_name,
143 cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
303 cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
144 author=TEST_USER_ADMIN_LOGIN,
304 author=TEST_USER_ADMIN_LOGIN,
145 message='commit1',
305 message='commit1',
146 content='line1',
306 content='line1',
147 f_path='file1'
307 f_path='file1'
148 )
308 )
149
309
150 cs0_prim = ScmModel().create_node(
310 cs0_prim = ScmModel().create_node(
151 repo=repo2.scm_instance, repo_name=r2_name,
311 repo=repo2.scm_instance, repo_name=r2_name,
152 cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
312 cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
153 author=TEST_USER_ADMIN_LOGIN,
313 author=TEST_USER_ADMIN_LOGIN,
154 message='commit1',
314 message='commit1',
155 content='line1',
315 content='line1',
156 f_path='file1'
316 f_path='file1'
157 )
317 )
158
318
159 cs1 = ScmModel().commit_change(
319 cs1 = ScmModel().commit_change(
160 repo=repo2.scm_instance, repo_name=r2_name,
320 repo=repo2.scm_instance, repo_name=r2_name,
161 cs=cs0_prim, user=TEST_USER_ADMIN_LOGIN, author=TEST_USER_ADMIN_LOGIN,
321 cs=cs0_prim, user=TEST_USER_ADMIN_LOGIN, author=TEST_USER_ADMIN_LOGIN,
162 message='commit2',
322 message='commit2',
163 content='line1\nline2',
323 content='line1\nline2',
164 f_path='file1'
324 f_path='file1'
165 )
325 )
166
326
167 rev1 = 'default'
327 rev1 = 'default'
168 rev2 = 'default'
328 rev2 = 'default'
169 response = self.app.get(url(controller='compare', action='index',
329 response = self.app.get(url(controller='compare', action='index',
170 repo_name=r2_name,
330 repo_name=r2_name,
171 org_ref_type="branch",
331 org_ref_type="branch",
172 org_ref=rev1,
332 org_ref=rev1,
173 other_ref_type="branch",
333 other_ref_type="branch",
174 other_ref=rev2,
334 other_ref=rev2,
175 repo=r1_name
335 repo=r1_name
176 ))
336 ))
177
337
178 try:
338 try:
179 response.mustcontain('%s@%s -> %s@%s' % (r2_name, rev1, r1_name, rev2))
339 response.mustcontain('%s@%s -> %s@%s' % (r2_name, rev1, r1_name, rev2))
180
340
181 response.mustcontain("""<div class="message">commit2</div>""")
341 response.mustcontain("""<div class="message tooltip" title="commit2" style="white-space:normal">commit2</div>""")
182 response.mustcontain("""<a href="/%s/changeset/%s">r1:%s</a>""" % (r2_name, cs1.raw_id, cs1.short_id))
342 response.mustcontain("""<a href="/%s/changeset/%s">r1:%s</a>""" % (r2_name, cs1.raw_id, cs1.short_id))
183 ## files
343 ## files
184 response.mustcontain("""<a href="/%s/compare/branch@%s...branch@%s#C--826e8142e6ba">file1</a>""" % (r2_name, rev1, rev2))
344 response.mustcontain("""<a href="/%s/compare/branch@%s...branch@%s#C--826e8142e6ba">file1</a>""" % (r2_name, rev1, rev2))
185
345
186 finally:
346 finally:
187 RepoModel().delete(r1_id)
347 RepoModel().delete(r1_id)
188 RepoModel().delete(r2_id)
348 RepoModel().delete(r2_id)
189
349
190 def test_org_repo_new_commits_after_forking(self):
350 def test_org_repo_new_commits_after_forking(self):
191 self.log_user()
351 self.log_user()
192
352
193 repo1 = RepoModel().create_repo(repo_name='one', repo_type='hg',
353 repo1 = RepoModel().create_repo(repo_name='one', repo_type='hg',
194 description='diff-test',
354 description='diff-test',
195 owner=TEST_USER_ADMIN_LOGIN)
355 owner=TEST_USER_ADMIN_LOGIN)
196
356
197 Session().commit()
357 Session().commit()
198 r1_id = repo1.repo_id
358 r1_id = repo1.repo_id
199 r1_name = repo1.repo_name
359 r1_name = repo1.repo_name
200
360
201 #commit something initially !
361 #commit something initially !
202 cs0 = ScmModel().create_node(
362 cs0 = ScmModel().create_node(
203 repo=repo1.scm_instance, repo_name=r1_name,
363 repo=repo1.scm_instance, repo_name=r1_name,
204 cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
364 cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
205 author=TEST_USER_ADMIN_LOGIN,
365 author=TEST_USER_ADMIN_LOGIN,
206 message='commit1',
366 message='commit1',
207 content='line1',
367 content='line1',
208 f_path='file1'
368 f_path='file1'
209 )
369 )
210 Session().commit()
370 Session().commit()
211 self.assertEqual(repo1.scm_instance.revisions, [cs0.raw_id])
371 self.assertEqual(repo1.scm_instance.revisions, [cs0.raw_id])
212 #fork the repo1
372 #fork the repo1
213 repo2 = RepoModel().create_repo(repo_name='one-fork', repo_type='hg',
373 repo2 = RepoModel().create_repo(repo_name='one-fork', repo_type='hg',
214 description='compare-test',
374 description='compare-test',
215 clone_uri=repo1.repo_full_path,
375 clone_uri=repo1.repo_full_path,
216 owner=TEST_USER_ADMIN_LOGIN, fork_of='one')
376 owner=TEST_USER_ADMIN_LOGIN, fork_of='one')
217 Session().commit()
377 Session().commit()
218 self.assertEqual(repo2.scm_instance.revisions, [cs0.raw_id])
378 self.assertEqual(repo2.scm_instance.revisions, [cs0.raw_id])
219 r2_id = repo2.repo_id
379 r2_id = repo2.repo_id
220 r2_name = repo2.repo_name
380 r2_name = repo2.repo_name
221
381
222 #make 3 new commits in fork
382 #make 3 new commits in fork
223 cs1 = ScmModel().create_node(
383 cs1 = ScmModel().create_node(
224 repo=repo2.scm_instance, repo_name=r2_name,
384 repo=repo2.scm_instance, repo_name=r2_name,
225 cs=repo2.scm_instance[-1], user=TEST_USER_ADMIN_LOGIN,
385 cs=repo2.scm_instance[-1], user=TEST_USER_ADMIN_LOGIN,
226 author=TEST_USER_ADMIN_LOGIN,
386 author=TEST_USER_ADMIN_LOGIN,
227 message='commit1-fork',
387 message='commit1-fork',
228 content='file1-line1-from-fork',
388 content='file1-line1-from-fork',
229 f_path='file1-fork'
389 f_path='file1-fork'
230 )
390 )
231 cs2 = ScmModel().create_node(
391 cs2 = ScmModel().create_node(
232 repo=repo2.scm_instance, repo_name=r2_name,
392 repo=repo2.scm_instance, repo_name=r2_name,
233 cs=cs1, user=TEST_USER_ADMIN_LOGIN,
393 cs=cs1, user=TEST_USER_ADMIN_LOGIN,
234 author=TEST_USER_ADMIN_LOGIN,
394 author=TEST_USER_ADMIN_LOGIN,
235 message='commit2-fork',
395 message='commit2-fork',
236 content='file2-line1-from-fork',
396 content='file2-line1-from-fork',
237 f_path='file2-fork'
397 f_path='file2-fork'
238 )
398 )
239 cs3 = ScmModel().create_node(
399 cs3 = ScmModel().create_node(
240 repo=repo2.scm_instance, repo_name=r2_name,
400 repo=repo2.scm_instance, repo_name=r2_name,
241 cs=cs2, user=TEST_USER_ADMIN_LOGIN,
401 cs=cs2, user=TEST_USER_ADMIN_LOGIN,
242 author=TEST_USER_ADMIN_LOGIN,
402 author=TEST_USER_ADMIN_LOGIN,
243 message='commit3-fork',
403 message='commit3-fork',
244 content='file3-line1-from-fork',
404 content='file3-line1-from-fork',
245 f_path='file3-fork'
405 f_path='file3-fork'
246 )
406 )
247
407
248 #compare !
408 #compare !
249 rev1 = 'default'
409 rev1 = 'default'
250 rev2 = 'default'
410 rev2 = 'default'
251 response = self.app.get(url(controller='compare', action='index',
411 response = self.app.get(url(controller='compare', action='index',
252 repo_name=r2_name,
412 repo_name=r2_name,
253 org_ref_type="branch",
413 org_ref_type="branch",
254 org_ref=rev1,
414 org_ref=rev1,
255 other_ref_type="branch",
415 other_ref_type="branch",
256 other_ref=rev2,
416 other_ref=rev2,
257 repo=r1_name,
417 repo=r1_name,
258 bundle=True,
418 bundle=True,
259 ))
419 ))
260
420
261 try:
421 try:
262 response.mustcontain('%s@%s -> %s@%s' % (r2_name, rev1, r1_name, rev2))
422 response.mustcontain('%s@%s -> %s@%s' % (r2_name, rev1, r1_name, rev2))
263 response.mustcontain("""file1-line1-from-fork""")
423 response.mustcontain("""file1-line1-from-fork""")
264 response.mustcontain("""file2-line1-from-fork""")
424 response.mustcontain("""file2-line1-from-fork""")
265 response.mustcontain("""file3-line1-from-fork""")
425 response.mustcontain("""file3-line1-from-fork""")
266
426
267 #add new commit into parent !
427 #add new commit into parent !
268 cs0 = ScmModel().create_node(
428 cs0 = ScmModel().create_node(
269 repo=repo1.scm_instance, repo_name=r1_name,
429 repo=repo1.scm_instance, repo_name=r1_name,
270 cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
430 cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
271 author=TEST_USER_ADMIN_LOGIN,
431 author=TEST_USER_ADMIN_LOGIN,
272 message='commit2',
432 message='commit2',
273 content='line1-from-new-parent',
433 content='line1-from-new-parent',
274 f_path='file2'
434 f_path='file2'
275 )
435 )
276 #compare !
436 #compare !
277 rev1 = 'default'
437 rev1 = 'default'
278 rev2 = 'default'
438 rev2 = 'default'
279 response = self.app.get(url(controller='compare', action='index',
439 response = self.app.get(url(controller='compare', action='index',
280 repo_name=r2_name,
440 repo_name=r2_name,
281 org_ref_type="branch",
441 org_ref_type="branch",
282 org_ref=rev1,
442 org_ref=rev1,
283 other_ref_type="branch",
443 other_ref_type="branch",
284 other_ref=rev2,
444 other_ref=rev2,
285 repo=r1_name,
445 repo=r1_name,
286 bundle=True,
446 bundle=True,
287 ))
447 ))
288
448
289 response.mustcontain('%s@%s -> %s@%s' % (r2_name, rev1, r1_name, rev2))
449 response.mustcontain('%s@%s -> %s@%s' % (r2_name, rev1, r1_name, rev2))
290 response.mustcontain("""<a href="#">file2</a>""") # new commit from parent
450 response.mustcontain("""<a href="#">file2</a>""") # new commit from parent
291 response.mustcontain("""line1-from-new-parent""")
451 response.mustcontain("""line1-from-new-parent""")
292 response.mustcontain("""file1-line1-from-fork""")
452 response.mustcontain("""file1-line1-from-fork""")
293 response.mustcontain("""file2-line1-from-fork""")
453 response.mustcontain("""file2-line1-from-fork""")
294 response.mustcontain("""file3-line1-from-fork""")
454 response.mustcontain("""file3-line1-from-fork""")
295 finally:
455 finally:
296 RepoModel().delete(r2_id)
456 RepoModel().delete(r2_id)
297 RepoModel().delete(r1_id)
457 RepoModel().delete(r1_id)
298
458
299 def test_org_repo_new_commits_after_forking_simple_diff(self):
459 def test_org_repo_new_commits_after_forking_simple_diff(self):
300 self.log_user()
460 self.log_user()
301
461
302 repo1 = RepoModel().create_repo(repo_name='one', repo_type='hg',
462 repo1 = RepoModel().create_repo(repo_name='one', repo_type='hg',
303 description='diff-test',
463 description='diff-test',
304 owner=TEST_USER_ADMIN_LOGIN)
464 owner=TEST_USER_ADMIN_LOGIN)
305
465
306 Session().commit()
466 Session().commit()
307 r1_id = repo1.repo_id
467 r1_id = repo1.repo_id
308 r1_name = repo1.repo_name
468 r1_name = repo1.repo_name
309
469
310 #commit something initially !
470 #commit something initially !
311 cs0 = ScmModel().create_node(
471 cs0 = ScmModel().create_node(
312 repo=repo1.scm_instance, repo_name=r1_name,
472 repo=repo1.scm_instance, repo_name=r1_name,
313 cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
473 cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
314 author=TEST_USER_ADMIN_LOGIN,
474 author=TEST_USER_ADMIN_LOGIN,
315 message='commit1',
475 message='commit1',
316 content='line1',
476 content='line1',
317 f_path='file1'
477 f_path='file1'
318 )
478 )
319 Session().commit()
479 Session().commit()
320 self.assertEqual(repo1.scm_instance.revisions, [cs0.raw_id])
480 self.assertEqual(repo1.scm_instance.revisions, [cs0.raw_id])
321 #fork the repo1
481 #fork the repo1
322 repo2 = RepoModel().create_repo(repo_name='one-fork', repo_type='hg',
482 repo2 = RepoModel().create_repo(repo_name='one-fork', repo_type='hg',
323 description='compare-test',
483 description='compare-test',
324 clone_uri=repo1.repo_full_path,
484 clone_uri=repo1.repo_full_path,
325 owner=TEST_USER_ADMIN_LOGIN, fork_of='one')
485 owner=TEST_USER_ADMIN_LOGIN, fork_of='one')
326 Session().commit()
486 Session().commit()
327 self.assertEqual(repo2.scm_instance.revisions, [cs0.raw_id])
487 self.assertEqual(repo2.scm_instance.revisions, [cs0.raw_id])
328 r2_id = repo2.repo_id
488 r2_id = repo2.repo_id
329 r2_name = repo2.repo_name
489 r2_name = repo2.repo_name
330
490
331 #make 3 new commits in fork
491 #make 3 new commits in fork
332 cs1 = ScmModel().create_node(
492 cs1 = ScmModel().create_node(
333 repo=repo2.scm_instance, repo_name=r2_name,
493 repo=repo2.scm_instance, repo_name=r2_name,
334 cs=repo2.scm_instance[-1], user=TEST_USER_ADMIN_LOGIN,
494 cs=repo2.scm_instance[-1], user=TEST_USER_ADMIN_LOGIN,
335 author=TEST_USER_ADMIN_LOGIN,
495 author=TEST_USER_ADMIN_LOGIN,
336 message='commit1-fork',
496 message='commit1-fork',
337 content='file1-line1-from-fork',
497 content='file1-line1-from-fork',
338 f_path='file1-fork'
498 f_path='file1-fork'
339 )
499 )
340 cs2 = ScmModel().create_node(
500 cs2 = ScmModel().create_node(
341 repo=repo2.scm_instance, repo_name=r2_name,
501 repo=repo2.scm_instance, repo_name=r2_name,
342 cs=cs1, user=TEST_USER_ADMIN_LOGIN,
502 cs=cs1, user=TEST_USER_ADMIN_LOGIN,
343 author=TEST_USER_ADMIN_LOGIN,
503 author=TEST_USER_ADMIN_LOGIN,
344 message='commit2-fork',
504 message='commit2-fork',
345 content='file2-line1-from-fork',
505 content='file2-line1-from-fork',
346 f_path='file2-fork'
506 f_path='file2-fork'
347 )
507 )
348 cs3 = ScmModel().create_node(
508 cs3 = ScmModel().create_node(
349 repo=repo2.scm_instance, repo_name=r2_name,
509 repo=repo2.scm_instance, repo_name=r2_name,
350 cs=cs2, user=TEST_USER_ADMIN_LOGIN,
510 cs=cs2, user=TEST_USER_ADMIN_LOGIN,
351 author=TEST_USER_ADMIN_LOGIN,
511 author=TEST_USER_ADMIN_LOGIN,
352 message='commit3-fork',
512 message='commit3-fork',
353 content='file3-line1-from-fork',
513 content='file3-line1-from-fork',
354 f_path='file3-fork'
514 f_path='file3-fork'
355 )
515 )
356
516
357 #compare !
517 #compare !
358 rev1 = 'default'
518 rev1 = 'default'
359 rev2 = 'default'
519 rev2 = 'default'
360 response = self.app.get(url(controller='compare', action='index',
520 response = self.app.get(url(controller='compare', action='index',
361 repo_name=r2_name,
521 repo_name=r2_name,
362 org_ref_type="branch",
522 org_ref_type="branch",
363 org_ref=rev1,
523 org_ref=rev1,
364 other_ref_type="branch",
524 other_ref_type="branch",
365 other_ref=rev2,
525 other_ref=rev2,
366 repo=r1_name,
526 repo=r1_name,
367 bundle=False,
527 bundle=False,
368 ))
528 ))
369
529
370 try:
530 try:
371 #response.mustcontain('%s@%s -> %s@%s' % (r2_name, rev1, r1_name, rev2))
531 #response.mustcontain('%s@%s -> %s@%s' % (r2_name, rev1, r1_name, rev2))
372
532
373 #add new commit into parent !
533 #add new commit into parent !
374 cs0 = ScmModel().create_node(
534 cs0 = ScmModel().create_node(
375 repo=repo1.scm_instance, repo_name=r1_name,
535 repo=repo1.scm_instance, repo_name=r1_name,
376 cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
536 cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
377 author=TEST_USER_ADMIN_LOGIN,
537 author=TEST_USER_ADMIN_LOGIN,
378 message='commit2',
538 message='commit2',
379 content='line1',
539 content='line1',
380 f_path='file2'
540 f_path='file2'
381 )
541 )
382 #compare !
542 #compare !
383 rev1 = 'default'
543 rev1 = 'default'
384 rev2 = 'default'
544 rev2 = 'default'
385 response = self.app.get(url(controller='compare', action='index',
545 response = self.app.get(url(controller='compare', action='index',
386 repo_name=r2_name,
546 repo_name=r2_name,
387 org_ref_type="branch",
547 org_ref_type="branch",
388 org_ref=rev1,
548 org_ref=rev1,
389 other_ref_type="branch",
549 other_ref_type="branch",
390 other_ref=rev2,
550 other_ref=rev2,
391 repo=r1_name,
551 repo=r1_name,
392 bundle=False
552 bundle=False
393 ))
553 ))
394 rev2 = cs0.parents[0].raw_id
554
395 response.mustcontain('%s@%s -> %s@%s' % (r2_name, rev1, r1_name, rev2))
555 response.mustcontain('%s@%s -> %s@%s' % (r2_name, rev1, r1_name, rev2))
396 response.mustcontain("""file1-line1-from-fork""")
556 response.mustcontain("""file1-line1-from-fork""")
397 response.mustcontain("""file2-line1-from-fork""")
557 response.mustcontain("""file2-line1-from-fork""")
398 response.mustcontain("""file3-line1-from-fork""")
558 response.mustcontain("""file3-line1-from-fork""")
399 self.assertFalse("""<a href="#">file2</a>""" in response.body) # new commit from parent
559 self.assertFalse("""<a href="#">file2</a>""" in response.body) # new commit from parent
400 self.assertFalse("""line1-from-new-parent""" in response.body)
560 self.assertFalse("""line1-from-new-parent""" in response.body)
401 finally:
561 finally:
402 RepoModel().delete(r2_id)
562 RepoModel().delete(r2_id)
403 RepoModel().delete(r1_id)
563 RepoModel().delete(r1_id)
General Comments 0
You need to be logged in to leave comments. Login now