##// END OF EJS Templates
git: fix for unicode branches
milka -
r4659:8a9c9ffb default
parent child Browse files
Show More
@@ -1,358 +1,358 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23
24 24 from pyramid.httpexceptions import HTTPNotFound, HTTPFound
25 25
26 26 from pyramid.renderers import render
27 27 from pyramid.response import Response
28 28
29 29 from rhodecode.apps._base import RepoAppView
30 30 import rhodecode.lib.helpers as h
31 31 from rhodecode.lib.auth import (
32 32 LoginRequired, HasRepoPermissionAnyDecorator)
33 33
34 34 from rhodecode.lib.ext_json import json
35 35 from rhodecode.lib.graphmod import _colored, _dagwalker
36 36 from rhodecode.lib.helpers import RepoPage
37 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool
37 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
38 38 from rhodecode.lib.vcs.exceptions import (
39 39 RepositoryError, CommitDoesNotExistError,
40 40 CommitError, NodeDoesNotExistError, EmptyRepositoryError)
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44 DEFAULT_CHANGELOG_SIZE = 20
45 45
46 46
47 47 class RepoChangelogView(RepoAppView):
48 48
49 49 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
50 50 """
51 51 This is a safe way to get commit. If an error occurs it redirects to
52 52 tip with proper message
53 53
54 54 :param commit_id: id of commit to fetch
55 55 :param redirect_after: toggle redirection
56 56 """
57 57 _ = self.request.translate
58 58
59 59 try:
60 60 return self.rhodecode_vcs_repo.get_commit(commit_id)
61 61 except EmptyRepositoryError:
62 62 if not redirect_after:
63 63 return None
64 64
65 65 h.flash(h.literal(
66 66 _('There are no commits yet')), category='warning')
67 67 raise HTTPFound(
68 68 h.route_path('repo_summary', repo_name=self.db_repo_name))
69 69
70 70 except (CommitDoesNotExistError, LookupError):
71 71 msg = _('No such commit exists for this repository')
72 72 h.flash(msg, category='error')
73 73 raise HTTPNotFound()
74 74 except RepositoryError as e:
75 75 h.flash(safe_str(h.escape(e)), category='error')
76 76 raise HTTPNotFound()
77 77
78 78 def _graph(self, repo, commits, prev_data=None, next_data=None):
79 79 """
80 80 Generates a DAG graph for repo
81 81
82 82 :param repo: repo instance
83 83 :param commits: list of commits
84 84 """
85 85 if not commits:
86 86 return json.dumps([]), json.dumps([])
87 87
88 88 def serialize(commit, parents=True):
89 89 data = dict(
90 90 raw_id=commit.raw_id,
91 91 idx=commit.idx,
92 92 branch=None,
93 93 )
94 94 if parents:
95 95 data['parents'] = [
96 96 serialize(x, parents=False) for x in commit.parents]
97 97 return data
98 98
99 99 prev_data = prev_data or []
100 100 next_data = next_data or []
101 101
102 102 current = [serialize(x) for x in commits]
103 103 commits = prev_data + current + next_data
104 104
105 105 dag = _dagwalker(repo, commits)
106 106
107 107 data = [[commit_id, vtx, edges, branch]
108 108 for commit_id, vtx, edges, branch in _colored(dag)]
109 109 return json.dumps(data), json.dumps(current)
110 110
111 111 def _check_if_valid_branch(self, branch_name, repo_name, f_path):
112 112 if branch_name not in self.rhodecode_vcs_repo.branches_all:
113 h.flash('Branch {} is not found.'.format(h.escape(branch_name)),
113 h.flash(u'Branch {} is not found.'.format(h.escape(safe_unicode(branch_name))),
114 114 category='warning')
115 115 redirect_url = h.route_path(
116 116 'repo_commits_file', repo_name=repo_name,
117 117 commit_id=branch_name, f_path=f_path or '')
118 118 raise HTTPFound(redirect_url)
119 119
120 120 def _load_changelog_data(
121 121 self, c, collection, page, chunk_size, branch_name=None,
122 122 dynamic=False, f_path=None, commit_id=None):
123 123
124 124 def url_generator(page_num):
125 125 query_params = {
126 126 'page': page_num
127 127 }
128 128
129 129 if branch_name:
130 130 query_params.update({
131 131 'branch': branch_name
132 132 })
133 133
134 134 if f_path:
135 135 # changelog for file
136 136 return h.route_path(
137 137 'repo_commits_file',
138 138 repo_name=c.rhodecode_db_repo.repo_name,
139 139 commit_id=commit_id, f_path=f_path,
140 140 _query=query_params)
141 141 else:
142 142 return h.route_path(
143 143 'repo_commits',
144 144 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
145 145
146 146 c.total_cs = len(collection)
147 147 c.showing_commits = min(chunk_size, c.total_cs)
148 148 c.pagination = RepoPage(collection, page=page, item_count=c.total_cs,
149 149 items_per_page=chunk_size, url_maker=url_generator)
150 150
151 151 c.next_page = c.pagination.next_page
152 152 c.prev_page = c.pagination.previous_page
153 153
154 154 if dynamic:
155 155 if self.request.GET.get('chunk') != 'next':
156 156 c.next_page = None
157 157 if self.request.GET.get('chunk') != 'prev':
158 158 c.prev_page = None
159 159
160 160 page_commit_ids = [x.raw_id for x in c.pagination]
161 161 c.comments = c.rhodecode_db_repo.get_comments(page_commit_ids)
162 162 c.statuses = c.rhodecode_db_repo.statuses(page_commit_ids)
163 163
164 164 def load_default_context(self):
165 165 c = self._get_local_tmpl_context(include_app_defaults=True)
166 166
167 167 c.rhodecode_repo = self.rhodecode_vcs_repo
168 168
169 169 return c
170 170
171 171 def _get_preload_attrs(self):
172 172 pre_load = ['author', 'branch', 'date', 'message', 'parents',
173 173 'obsolete', 'phase', 'hidden']
174 174 return pre_load
175 175
176 176 @LoginRequired()
177 177 @HasRepoPermissionAnyDecorator(
178 178 'repository.read', 'repository.write', 'repository.admin')
179 179 def repo_changelog(self):
180 180 c = self.load_default_context()
181 181
182 182 commit_id = self.request.matchdict.get('commit_id')
183 183 f_path = self._get_f_path(self.request.matchdict)
184 184 show_hidden = str2bool(self.request.GET.get('evolve'))
185 185
186 186 chunk_size = 20
187 187
188 188 c.branch_name = branch_name = self.request.GET.get('branch') or ''
189 189 c.book_name = book_name = self.request.GET.get('bookmark') or ''
190 190 c.f_path = f_path
191 191 c.commit_id = commit_id
192 192 c.show_hidden = show_hidden
193 193
194 194 hist_limit = safe_int(self.request.GET.get('limit')) or None
195 195
196 196 p = safe_int(self.request.GET.get('page', 1), 1)
197 197
198 198 c.selected_name = branch_name or book_name
199 199 if not commit_id and branch_name:
200 200 self._check_if_valid_branch(branch_name, self.db_repo_name, f_path)
201 201
202 202 c.changelog_for_path = f_path
203 203 pre_load = self._get_preload_attrs()
204 204
205 205 partial_xhr = self.request.environ.get('HTTP_X_PARTIAL_XHR')
206 206
207 207 try:
208 208 if f_path:
209 209 log.debug('generating changelog for path %s', f_path)
210 210 # get the history for the file !
211 211 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
212 212
213 213 try:
214 214 collection = base_commit.get_path_history(
215 215 f_path, limit=hist_limit, pre_load=pre_load)
216 216 if collection and partial_xhr:
217 217 # for ajax call we remove first one since we're looking
218 218 # at it right now in the context of a file commit
219 219 collection.pop(0)
220 220 except (NodeDoesNotExistError, CommitError):
221 221 # this node is not present at tip!
222 222 try:
223 223 commit = self._get_commit_or_redirect(commit_id)
224 224 collection = commit.get_path_history(f_path)
225 225 except RepositoryError as e:
226 226 h.flash(safe_str(e), category='warning')
227 227 redirect_url = h.route_path(
228 228 'repo_commits', repo_name=self.db_repo_name)
229 229 raise HTTPFound(redirect_url)
230 230 collection = list(reversed(collection))
231 231 else:
232 232 collection = self.rhodecode_vcs_repo.get_commits(
233 233 branch_name=branch_name, show_hidden=show_hidden,
234 234 pre_load=pre_load, translate_tags=False)
235 235
236 236 self._load_changelog_data(
237 237 c, collection, p, chunk_size, c.branch_name,
238 238 f_path=f_path, commit_id=commit_id)
239 239
240 240 except EmptyRepositoryError as e:
241 241 h.flash(safe_str(h.escape(e)), category='warning')
242 242 raise HTTPFound(
243 243 h.route_path('repo_summary', repo_name=self.db_repo_name))
244 244 except HTTPFound:
245 245 raise
246 246 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
247 247 log.exception(safe_str(e))
248 248 h.flash(safe_str(h.escape(e)), category='error')
249 249
250 250 if commit_id:
251 251 # from single commit page, we redirect to main commits
252 252 raise HTTPFound(
253 253 h.route_path('repo_commits', repo_name=self.db_repo_name))
254 254 else:
255 255 # otherwise we redirect to summary
256 256 raise HTTPFound(
257 257 h.route_path('repo_summary', repo_name=self.db_repo_name))
258 258
259 259 if partial_xhr or self.request.environ.get('HTTP_X_PJAX'):
260 260 # case when loading dynamic file history in file view
261 261 # loading from ajax, we don't want the first result, it's popped
262 262 # in the code above
263 263 html = render(
264 264 'rhodecode:templates/commits/changelog_file_history.mako',
265 265 self._get_template_context(c), self.request)
266 266 return Response(html)
267 267
268 268 commit_ids = []
269 269 if not f_path:
270 270 # only load graph data when not in file history mode
271 271 commit_ids = c.pagination
272 272
273 273 c.graph_data, c.graph_commits = self._graph(
274 274 self.rhodecode_vcs_repo, commit_ids)
275 275
276 276 return self._get_template_context(c)
277 277
278 278 @LoginRequired()
279 279 @HasRepoPermissionAnyDecorator(
280 280 'repository.read', 'repository.write', 'repository.admin')
281 281 def repo_commits_elements(self):
282 282 c = self.load_default_context()
283 283 commit_id = self.request.matchdict.get('commit_id')
284 284 f_path = self._get_f_path(self.request.matchdict)
285 285 show_hidden = str2bool(self.request.GET.get('evolve'))
286 286
287 287 chunk_size = 20
288 288 hist_limit = safe_int(self.request.GET.get('limit')) or None
289 289
290 290 def wrap_for_error(err):
291 291 html = '<tr>' \
292 292 '<td colspan="9" class="alert alert-error">ERROR: {}</td>' \
293 293 '</tr>'.format(err)
294 294 return Response(html)
295 295
296 296 c.branch_name = branch_name = self.request.GET.get('branch') or ''
297 297 c.book_name = book_name = self.request.GET.get('bookmark') or ''
298 298 c.f_path = f_path
299 299 c.commit_id = commit_id
300 300 c.show_hidden = show_hidden
301 301
302 302 c.selected_name = branch_name or book_name
303 303 if branch_name and branch_name not in self.rhodecode_vcs_repo.branches_all:
304 304 return wrap_for_error(
305 305 safe_str('Branch: {} is not valid'.format(branch_name)))
306 306
307 307 pre_load = self._get_preload_attrs()
308 308
309 309 if f_path:
310 310 try:
311 311 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
312 312 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
313 313 log.exception(safe_str(e))
314 314 raise HTTPFound(
315 315 h.route_path('repo_commits', repo_name=self.db_repo_name))
316 316
317 317 collection = base_commit.get_path_history(
318 318 f_path, limit=hist_limit, pre_load=pre_load)
319 319 collection = list(reversed(collection))
320 320 else:
321 321 collection = self.rhodecode_vcs_repo.get_commits(
322 322 branch_name=branch_name, show_hidden=show_hidden, pre_load=pre_load,
323 323 translate_tags=False)
324 324
325 325 p = safe_int(self.request.GET.get('page', 1), 1)
326 326 try:
327 327 self._load_changelog_data(
328 328 c, collection, p, chunk_size, dynamic=True,
329 329 f_path=f_path, commit_id=commit_id)
330 330 except EmptyRepositoryError as e:
331 331 return wrap_for_error(safe_str(e))
332 332 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
333 333 log.exception('Failed to fetch commits')
334 334 return wrap_for_error(safe_str(e))
335 335
336 336 prev_data = None
337 337 next_data = None
338 338
339 339 try:
340 340 prev_graph = json.loads(self.request.POST.get('graph') or '{}')
341 341 except json.JSONDecodeError:
342 342 prev_graph = {}
343 343
344 344 if self.request.GET.get('chunk') == 'prev':
345 345 next_data = prev_graph
346 346 elif self.request.GET.get('chunk') == 'next':
347 347 prev_data = prev_graph
348 348
349 349 commit_ids = []
350 350 if not f_path:
351 351 # only load graph data when not in file history mode
352 352 commit_ids = c.pagination
353 353
354 354 c.graph_data, c.graph_commits = self._graph(
355 355 self.rhodecode_vcs_repo, commit_ids,
356 356 prev_data=prev_data, next_data=next_data)
357 357
358 358 return self._get_template_context(c)
@@ -1,494 +1,496 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 GIT commit module
23 23 """
24 24
25 25 import re
26 26 import stat
27 27 from itertools import chain
28 28 from StringIO import StringIO
29 29
30 30 from zope.cachedescriptors.property import Lazy as LazyProperty
31 31
32 32 from rhodecode.lib.datelib import utcdate_fromtimestamp
33 33 from rhodecode.lib.utils import safe_unicode, safe_str
34 34 from rhodecode.lib.utils2 import safe_int
35 35 from rhodecode.lib.vcs.conf import settings
36 36 from rhodecode.lib.vcs.backends import base
37 37 from rhodecode.lib.vcs.exceptions import CommitError, NodeDoesNotExistError
38 38 from rhodecode.lib.vcs.nodes import (
39 39 FileNode, DirNode, NodeKind, RootNode, SubModuleNode,
40 40 ChangedFileNodesGenerator, AddedFileNodesGenerator,
41 41 RemovedFileNodesGenerator, LargeFileNode)
42 42 from rhodecode.lib.vcs.compat import configparser
43 43
44 44
45 45 class GitCommit(base.BaseCommit):
46 46 """
47 47 Represents state of the repository at single commit id.
48 48 """
49 49
50 50 _filter_pre_load = [
51 51 # done through a more complex tree walk on parents
52 52 "affected_files",
53 53 # done through subprocess not remote call
54 54 "children",
55 55 # done through a more complex tree walk on parents
56 56 "status",
57 57 # mercurial specific property not supported here
58 58 "_file_paths",
59 59 # mercurial specific property not supported here
60 60 'obsolete',
61 61 # mercurial specific property not supported here
62 62 'phase',
63 63 # mercurial specific property not supported here
64 64 'hidden'
65 65 ]
66 66
67 67 def __init__(self, repository, raw_id, idx, pre_load=None):
68 68 self.repository = repository
69 69 self._remote = repository._remote
70 70 # TODO: johbo: Tweak of raw_id should not be necessary
71 71 self.raw_id = safe_str(raw_id)
72 72 self.idx = idx
73 73
74 74 self._set_bulk_properties(pre_load)
75 75
76 76 # caches
77 77 self._stat_modes = {} # stat info for paths
78 78 self._paths = {} # path processed with parse_tree
79 79 self.nodes = {}
80 80 self._submodules = None
81 81
82 82 def _set_bulk_properties(self, pre_load):
83 83
84 84 if not pre_load:
85 85 return
86 86 pre_load = [entry for entry in pre_load
87 87 if entry not in self._filter_pre_load]
88 88 if not pre_load:
89 89 return
90 90
91 91 result = self._remote.bulk_request(self.raw_id, pre_load)
92 92 for attr, value in result.items():
93 93 if attr in ["author", "message"]:
94 94 if value:
95 95 value = safe_unicode(value)
96 96 elif attr == "date":
97 97 value = utcdate_fromtimestamp(*value)
98 98 elif attr == "parents":
99 99 value = self._make_commits(value)
100 100 elif attr == "branch":
101 value = value[0] if value else None
101 value = self._set_branch(value)
102 102 self.__dict__[attr] = value
103 103
104 104 @LazyProperty
105 105 def _commit(self):
106 106 return self._remote[self.raw_id]
107 107
108 108 @LazyProperty
109 109 def _tree_id(self):
110 110 return self._remote[self._commit['tree']]['id']
111 111
112 112 @LazyProperty
113 113 def id(self):
114 114 return self.raw_id
115 115
116 116 @LazyProperty
117 117 def short_id(self):
118 118 return self.raw_id[:12]
119 119
120 120 @LazyProperty
121 121 def message(self):
122 122 return safe_unicode(self._remote.message(self.id))
123 123
124 124 @LazyProperty
125 125 def committer(self):
126 126 return safe_unicode(self._remote.author(self.id))
127 127
128 128 @LazyProperty
129 129 def author(self):
130 130 return safe_unicode(self._remote.author(self.id))
131 131
132 132 @LazyProperty
133 133 def date(self):
134 134 unix_ts, tz = self._remote.date(self.raw_id)
135 135 return utcdate_fromtimestamp(unix_ts, tz)
136 136
137 137 @LazyProperty
138 138 def status(self):
139 139 """
140 140 Returns modified, added, removed, deleted files for current commit
141 141 """
142 142 return self.changed, self.added, self.removed
143 143
144 144 @LazyProperty
145 145 def tags(self):
146 146 tags = [safe_unicode(name) for name,
147 147 commit_id in self.repository.tags.iteritems()
148 148 if commit_id == self.raw_id]
149 149 return tags
150 150
151 151 @LazyProperty
152 152 def commit_branches(self):
153 153 branches = []
154 154 for name, commit_id in self.repository.branches.iteritems():
155 155 if commit_id == self.raw_id:
156 156 branches.append(name)
157 157 return branches
158 158
159 def _set_branch(self, branches):
160 if branches:
161 # actually commit can have multiple branches in git
162 return safe_unicode(branches[0])
163
159 164 @LazyProperty
160 165 def branch(self):
161 166 branches = self._remote.branch(self.raw_id)
162
163 if branches:
164 # actually commit can have multiple branches in git
165 return safe_unicode(branches[0])
167 return self._set_branch(branches)
166 168
167 169 def _get_tree_id_for_path(self, path):
168 170 path = safe_str(path)
169 171 if path in self._paths:
170 172 return self._paths[path]
171 173
172 174 tree_id = self._tree_id
173 175
174 176 path = path.strip('/')
175 177 if path == '':
176 178 data = [tree_id, "tree"]
177 179 self._paths[''] = data
178 180 return data
179 181
180 182 tree_id, tree_type, tree_mode = \
181 183 self._remote.tree_and_type_for_path(self.raw_id, path)
182 184 if tree_id is None:
183 185 raise self.no_node_at_path(path)
184 186
185 187 self._paths[path] = [tree_id, tree_type]
186 188 self._stat_modes[path] = tree_mode
187 189
188 190 if path not in self._paths:
189 191 raise self.no_node_at_path(path)
190 192
191 193 return self._paths[path]
192 194
193 195 def _get_kind(self, path):
194 196 tree_id, type_ = self._get_tree_id_for_path(path)
195 197 if type_ == 'blob':
196 198 return NodeKind.FILE
197 199 elif type_ == 'tree':
198 200 return NodeKind.DIR
199 201 elif type_ == 'link':
200 202 return NodeKind.SUBMODULE
201 203 return None
202 204
203 205 def _get_filectx(self, path):
204 206 path = self._fix_path(path)
205 207 if self._get_kind(path) != NodeKind.FILE:
206 208 raise CommitError(
207 209 "File does not exist for commit %s at '%s'" % (self.raw_id, path))
208 210 return path
209 211
210 212 def _get_file_nodes(self):
211 213 return chain(*(t[2] for t in self.walk()))
212 214
213 215 @LazyProperty
214 216 def parents(self):
215 217 """
216 218 Returns list of parent commits.
217 219 """
218 220 parent_ids = self._remote.parents(self.id)
219 221 return self._make_commits(parent_ids)
220 222
221 223 @LazyProperty
222 224 def children(self):
223 225 """
224 226 Returns list of child commits.
225 227 """
226 228
227 229 children = self._remote.children(self.raw_id)
228 230 return self._make_commits(children)
229 231
230 232 def _make_commits(self, commit_ids):
231 233 def commit_maker(_commit_id):
232 234 return self.repository.get_commit(commit_id=commit_id)
233 235
234 236 return [commit_maker(commit_id) for commit_id in commit_ids]
235 237
236 238 def get_file_mode(self, path):
237 239 """
238 240 Returns stat mode of the file at the given `path`.
239 241 """
240 242 path = safe_str(path)
241 243 # ensure path is traversed
242 244 self._get_tree_id_for_path(path)
243 245 return self._stat_modes[path]
244 246
245 247 def is_link(self, path):
246 248 return stat.S_ISLNK(self.get_file_mode(path))
247 249
248 250 def is_node_binary(self, path):
249 251 tree_id, _ = self._get_tree_id_for_path(path)
250 252 return self._remote.is_binary(tree_id)
251 253
252 254 def get_file_content(self, path):
253 255 """
254 256 Returns content of the file at given `path`.
255 257 """
256 258 tree_id, _ = self._get_tree_id_for_path(path)
257 259 return self._remote.blob_as_pretty_string(tree_id)
258 260
259 261 def get_file_content_streamed(self, path):
260 262 tree_id, _ = self._get_tree_id_for_path(path)
261 263 stream_method = getattr(self._remote, 'stream:blob_as_pretty_string')
262 264 return stream_method(tree_id)
263 265
264 266 def get_file_size(self, path):
265 267 """
266 268 Returns size of the file at given `path`.
267 269 """
268 270 tree_id, _ = self._get_tree_id_for_path(path)
269 271 return self._remote.blob_raw_length(tree_id)
270 272
271 273 def get_path_history(self, path, limit=None, pre_load=None):
272 274 """
273 275 Returns history of file as reversed list of `GitCommit` objects for
274 276 which file at given `path` has been modified.
275 277 """
276 278
277 279 path = self._get_filectx(path)
278 280 hist = self._remote.node_history(self.raw_id, path, limit)
279 281 return [
280 282 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
281 283 for commit_id in hist]
282 284
283 285 def get_file_annotate(self, path, pre_load=None):
284 286 """
285 287 Returns a generator of four element tuples with
286 288 lineno, commit_id, commit lazy loader and line
287 289 """
288 290
289 291 result = self._remote.node_annotate(self.raw_id, path)
290 292
291 293 for ln_no, commit_id, content in result:
292 294 yield (
293 295 ln_no, commit_id,
294 296 lambda: self.repository.get_commit(commit_id=commit_id, pre_load=pre_load),
295 297 content)
296 298
297 299 def get_nodes(self, path):
298 300
299 301 if self._get_kind(path) != NodeKind.DIR:
300 302 raise CommitError(
301 303 "Directory does not exist for commit %s at '%s'" % (self.raw_id, path))
302 304 path = self._fix_path(path)
303 305
304 306 tree_id, _ = self._get_tree_id_for_path(path)
305 307
306 308 dirnodes = []
307 309 filenodes = []
308 310
309 311 # extracted tree ID gives us our files...
310 312 bytes_path = safe_str(path) # libgit operates on bytes
311 313 for name, stat_, id_, type_ in self._remote.tree_items(tree_id):
312 314 if type_ == 'link':
313 315 url = self._get_submodule_url('/'.join((bytes_path, name)))
314 316 dirnodes.append(SubModuleNode(
315 317 name, url=url, commit=id_, alias=self.repository.alias))
316 318 continue
317 319
318 320 if bytes_path != '':
319 321 obj_path = '/'.join((bytes_path, name))
320 322 else:
321 323 obj_path = name
322 324 if obj_path not in self._stat_modes:
323 325 self._stat_modes[obj_path] = stat_
324 326
325 327 if type_ == 'tree':
326 328 dirnodes.append(DirNode(obj_path, commit=self))
327 329 elif type_ == 'blob':
328 330 filenodes.append(FileNode(obj_path, commit=self, mode=stat_))
329 331 else:
330 332 raise CommitError(
331 333 "Requested object should be Tree or Blob, is %s", type_)
332 334
333 335 nodes = dirnodes + filenodes
334 336 for node in nodes:
335 337 if node.path not in self.nodes:
336 338 self.nodes[node.path] = node
337 339 nodes.sort()
338 340 return nodes
339 341
340 342 def get_node(self, path, pre_load=None):
341 343 if isinstance(path, unicode):
342 344 path = path.encode('utf-8')
343 345 path = self._fix_path(path)
344 346 if path not in self.nodes:
345 347 try:
346 348 tree_id, type_ = self._get_tree_id_for_path(path)
347 349 except CommitError:
348 350 raise NodeDoesNotExistError(
349 351 "Cannot find one of parents' directories for a given "
350 352 "path: %s" % path)
351 353
352 354 if type_ in ['link', 'commit']:
353 355 url = self._get_submodule_url(path)
354 356 node = SubModuleNode(path, url=url, commit=tree_id,
355 357 alias=self.repository.alias)
356 358 elif type_ == 'tree':
357 359 if path == '':
358 360 node = RootNode(commit=self)
359 361 else:
360 362 node = DirNode(path, commit=self)
361 363 elif type_ == 'blob':
362 364 node = FileNode(path, commit=self, pre_load=pre_load)
363 365 self._stat_modes[path] = node.mode
364 366 else:
365 367 raise self.no_node_at_path(path)
366 368
367 369 # cache node
368 370 self.nodes[path] = node
369 371
370 372 return self.nodes[path]
371 373
372 374 def get_largefile_node(self, path):
373 375 tree_id, _ = self._get_tree_id_for_path(path)
374 376 pointer_spec = self._remote.is_large_file(tree_id)
375 377
376 378 if pointer_spec:
377 379 # content of that file regular FileNode is the hash of largefile
378 380 file_id = pointer_spec.get('oid_hash')
379 381 if self._remote.in_largefiles_store(file_id):
380 382 lf_path = self._remote.store_path(file_id)
381 383 return LargeFileNode(lf_path, commit=self, org_path=path)
382 384
383 385 @LazyProperty
384 386 def affected_files(self):
385 387 """
386 388 Gets a fast accessible file changes for given commit
387 389 """
388 390 added, modified, deleted = self._changes_cache
389 391 return list(added.union(modified).union(deleted))
390 392
391 393 @LazyProperty
392 394 def _changes_cache(self):
393 395 added = set()
394 396 modified = set()
395 397 deleted = set()
396 398 _r = self._remote
397 399
398 400 parents = self.parents
399 401 if not self.parents:
400 402 parents = [base.EmptyCommit()]
401 403 for parent in parents:
402 404 if isinstance(parent, base.EmptyCommit):
403 405 oid = None
404 406 else:
405 407 oid = parent.raw_id
406 408 changes = _r.tree_changes(oid, self.raw_id)
407 409 for (oldpath, newpath), (_, _), (_, _) in changes:
408 410 if newpath and oldpath:
409 411 modified.add(newpath)
410 412 elif newpath and not oldpath:
411 413 added.add(newpath)
412 414 elif not newpath and oldpath:
413 415 deleted.add(oldpath)
414 416 return added, modified, deleted
415 417
416 418 def _get_paths_for_status(self, status):
417 419 """
418 420 Returns sorted list of paths for given ``status``.
419 421
420 422 :param status: one of: *added*, *modified* or *deleted*
421 423 """
422 424 added, modified, deleted = self._changes_cache
423 425 return sorted({
424 426 'added': list(added),
425 427 'modified': list(modified),
426 428 'deleted': list(deleted)}[status]
427 429 )
428 430
429 431 @LazyProperty
430 432 def added(self):
431 433 """
432 434 Returns list of added ``FileNode`` objects.
433 435 """
434 436 if not self.parents:
435 437 return list(self._get_file_nodes())
436 438 return AddedFileNodesGenerator(self.added_paths, self)
437 439
438 440 @LazyProperty
439 441 def added_paths(self):
440 442 return [n for n in self._get_paths_for_status('added')]
441 443
442 444 @LazyProperty
443 445 def changed(self):
444 446 """
445 447 Returns list of modified ``FileNode`` objects.
446 448 """
447 449 if not self.parents:
448 450 return []
449 451 return ChangedFileNodesGenerator(self.changed_paths, self)
450 452
451 453 @LazyProperty
452 454 def changed_paths(self):
453 455 return [n for n in self._get_paths_for_status('modified')]
454 456
455 457 @LazyProperty
456 458 def removed(self):
457 459 """
458 460 Returns list of removed ``FileNode`` objects.
459 461 """
460 462 if not self.parents:
461 463 return []
462 464 return RemovedFileNodesGenerator(self.removed_paths, self)
463 465
464 466 @LazyProperty
465 467 def removed_paths(self):
466 468 return [n for n in self._get_paths_for_status('deleted')]
467 469
468 470 def _get_submodule_url(self, submodule_path):
469 471 git_modules_path = '.gitmodules'
470 472
471 473 if self._submodules is None:
472 474 self._submodules = {}
473 475
474 476 try:
475 477 submodules_node = self.get_node(git_modules_path)
476 478 except NodeDoesNotExistError:
477 479 return None
478 480
479 481 # ConfigParser fails if there are whitespaces, also it needs an iterable
480 482 # file like content
481 483 def iter_content(_content):
482 484 for line in _content.splitlines():
483 485 yield line
484 486
485 487 parser = configparser.RawConfigParser()
486 488 parser.read_file(iter_content(submodules_node.content))
487 489
488 490 for section in parser.sections():
489 491 path = parser.get(section, 'path')
490 492 url = parser.get(section, 'url')
491 493 if path and url:
492 494 self._submodules[path.strip('/')] = url
493 495
494 496 return self._submodules.get(submodule_path.strip('/'))
General Comments 0
You need to be logged in to leave comments. Login now