##// END OF EJS Templates
git: fixed issue with git submodules detection.
marcink -
r4115:4f985c11 default
parent child Browse files
Show More
@@ -1,484 +1,484 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2019 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 101 value = value[0] if value else None
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 159 @LazyProperty
160 160 def branch(self):
161 161 branches = self._remote.branch(self.raw_id)
162 162
163 163 if branches:
164 164 # actually commit can have multiple branches in git
165 165 return safe_unicode(branches[0])
166 166
167 167 def _get_tree_id_for_path(self, path):
168 168 path = safe_str(path)
169 169 if path in self._paths:
170 170 return self._paths[path]
171 171
172 172 tree_id = self._tree_id
173 173
174 174 path = path.strip('/')
175 175 if path == '':
176 176 data = [tree_id, "tree"]
177 177 self._paths[''] = data
178 178 return data
179 179
180 180 tree_id, tree_type, tree_mode = \
181 181 self._remote.tree_and_type_for_path(self.raw_id, path)
182 182 if tree_id is None:
183 183 raise self.no_node_at_path(path)
184 184
185 185 self._paths[path] = [tree_id, tree_type]
186 186 self._stat_modes[path] = tree_mode
187 187
188 188 if path not in self._paths:
189 189 raise self.no_node_at_path(path)
190 190
191 191 return self._paths[path]
192 192
193 193 def _get_kind(self, path):
194 194 tree_id, type_ = self._get_tree_id_for_path(path)
195 195 if type_ == 'blob':
196 196 return NodeKind.FILE
197 197 elif type_ == 'tree':
198 198 return NodeKind.DIR
199 199 elif type_ == 'link':
200 200 return NodeKind.SUBMODULE
201 201 return None
202 202
203 203 def _get_filectx(self, path):
204 204 path = self._fix_path(path)
205 205 if self._get_kind(path) != NodeKind.FILE:
206 206 raise CommitError(
207 207 "File does not exist for commit %s at '%s'" % (self.raw_id, path))
208 208 return path
209 209
210 210 def _get_file_nodes(self):
211 211 return chain(*(t[2] for t in self.walk()))
212 212
213 213 @LazyProperty
214 214 def parents(self):
215 215 """
216 216 Returns list of parent commits.
217 217 """
218 218 parent_ids = self._remote.parents(self.id)
219 219 return self._make_commits(parent_ids)
220 220
221 221 @LazyProperty
222 222 def children(self):
223 223 """
224 224 Returns list of child commits.
225 225 """
226 226
227 227 children = self._remote.children(self.raw_id)
228 228 return self._make_commits(children)
229 229
230 230 def _make_commits(self, commit_ids):
231 231 def commit_maker(_commit_id):
232 232 return self.repository.get_commit(commit_id=commit_id)
233 233
234 234 return [commit_maker(commit_id) for commit_id in commit_ids]
235 235
236 236 def get_file_mode(self, path):
237 237 """
238 238 Returns stat mode of the file at the given `path`.
239 239 """
240 240 path = safe_str(path)
241 241 # ensure path is traversed
242 242 self._get_tree_id_for_path(path)
243 243 return self._stat_modes[path]
244 244
245 245 def is_link(self, path):
246 246 return stat.S_ISLNK(self.get_file_mode(path))
247 247
248 248 def is_node_binary(self, path):
249 249 tree_id, _ = self._get_tree_id_for_path(path)
250 250 return self._remote.is_binary(tree_id)
251 251
252 252 def get_file_content(self, path):
253 253 """
254 254 Returns content of the file at given `path`.
255 255 """
256 256 tree_id, _ = self._get_tree_id_for_path(path)
257 257 return self._remote.blob_as_pretty_string(tree_id)
258 258
259 259 def get_file_content_streamed(self, path):
260 260 tree_id, _ = self._get_tree_id_for_path(path)
261 261 stream_method = getattr(self._remote, 'stream:blob_as_pretty_string')
262 262 return stream_method(tree_id)
263 263
264 264 def get_file_size(self, path):
265 265 """
266 266 Returns size of the file at given `path`.
267 267 """
268 268 tree_id, _ = self._get_tree_id_for_path(path)
269 269 return self._remote.blob_raw_length(tree_id)
270 270
271 271 def get_path_history(self, path, limit=None, pre_load=None):
272 272 """
273 273 Returns history of file as reversed list of `GitCommit` objects for
274 274 which file at given `path` has been modified.
275 275 """
276 276
277 277 path = self._get_filectx(path)
278 278 hist = self._remote.node_history(self.raw_id, path, limit)
279 279 return [
280 280 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
281 281 for commit_id in hist]
282 282
283 283 def get_file_annotate(self, path, pre_load=None):
284 284 """
285 285 Returns a generator of four element tuples with
286 286 lineno, commit_id, commit lazy loader and line
287 287 """
288 288
289 289 result = self._remote.node_annotate(self.raw_id, path)
290 290
291 291 for ln_no, commit_id, content in result:
292 292 yield (
293 293 ln_no, commit_id,
294 294 lambda: self.repository.get_commit(commit_id=commit_id, pre_load=pre_load),
295 295 content)
296 296
297 297 def get_nodes(self, path):
298 298
299 299 if self._get_kind(path) != NodeKind.DIR:
300 300 raise CommitError(
301 301 "Directory does not exist for commit %s at '%s'" % (self.raw_id, path))
302 302 path = self._fix_path(path)
303 303
304 304 tree_id, _ = self._get_tree_id_for_path(path)
305 305
306 306 dirnodes = []
307 307 filenodes = []
308 308
309 309 # extracted tree ID gives us our files...
310 310 for name, stat_, id_, type_ in self._remote.tree_items(tree_id):
311 311 if type_ == 'link':
312 312 url = self._get_submodule_url('/'.join((path, name)))
313 313 dirnodes.append(SubModuleNode(
314 314 name, url=url, commit=id_, alias=self.repository.alias))
315 315 continue
316 316
317 317 if path != '':
318 318 obj_path = '/'.join((path, name))
319 319 else:
320 320 obj_path = name
321 321 if obj_path not in self._stat_modes:
322 322 self._stat_modes[obj_path] = stat_
323 323
324 324 if type_ == 'tree':
325 325 dirnodes.append(DirNode(obj_path, commit=self))
326 326 elif type_ == 'blob':
327 327 filenodes.append(FileNode(obj_path, commit=self, mode=stat_))
328 328 else:
329 329 raise CommitError(
330 330 "Requested object should be Tree or Blob, is %s", type_)
331 331
332 332 nodes = dirnodes + filenodes
333 333 for node in nodes:
334 334 if node.path not in self.nodes:
335 335 self.nodes[node.path] = node
336 336 nodes.sort()
337 337 return nodes
338 338
339 339 def get_node(self, path, pre_load=None):
340 340 if isinstance(path, unicode):
341 341 path = path.encode('utf-8')
342 342 path = self._fix_path(path)
343 343 if path not in self.nodes:
344 344 try:
345 345 tree_id, type_ = self._get_tree_id_for_path(path)
346 346 except CommitError:
347 347 raise NodeDoesNotExistError(
348 348 "Cannot find one of parents' directories for a given "
349 349 "path: %s" % path)
350 350
351 if type_ == 'link':
351 if type_ in ['link', 'commit']:
352 352 url = self._get_submodule_url(path)
353 353 node = SubModuleNode(path, url=url, commit=tree_id,
354 354 alias=self.repository.alias)
355 355 elif type_ == 'tree':
356 356 if path == '':
357 357 node = RootNode(commit=self)
358 358 else:
359 359 node = DirNode(path, commit=self)
360 360 elif type_ == 'blob':
361 361 node = FileNode(path, commit=self, pre_load=pre_load)
362 362 self._stat_modes[path] = node.mode
363 363 else:
364 364 raise self.no_node_at_path(path)
365 365
366 366 # cache node
367 367 self.nodes[path] = node
368 368
369 369 return self.nodes[path]
370 370
371 371 def get_largefile_node(self, path):
372 372 tree_id, _ = self._get_tree_id_for_path(path)
373 373 pointer_spec = self._remote.is_large_file(tree_id)
374 374
375 375 if pointer_spec:
376 376 # content of that file regular FileNode is the hash of largefile
377 377 file_id = pointer_spec.get('oid_hash')
378 378 if self._remote.in_largefiles_store(file_id):
379 379 lf_path = self._remote.store_path(file_id)
380 380 return LargeFileNode(lf_path, commit=self, org_path=path)
381 381
382 382 @LazyProperty
383 383 def affected_files(self):
384 384 """
385 385 Gets a fast accessible file changes for given commit
386 386 """
387 387 added, modified, deleted = self._changes_cache
388 388 return list(added.union(modified).union(deleted))
389 389
390 390 @LazyProperty
391 391 def _changes_cache(self):
392 392 added = set()
393 393 modified = set()
394 394 deleted = set()
395 395 _r = self._remote
396 396
397 397 parents = self.parents
398 398 if not self.parents:
399 399 parents = [base.EmptyCommit()]
400 400 for parent in parents:
401 401 if isinstance(parent, base.EmptyCommit):
402 402 oid = None
403 403 else:
404 404 oid = parent.raw_id
405 405 changes = _r.tree_changes(oid, self.raw_id)
406 406 for (oldpath, newpath), (_, _), (_, _) in changes:
407 407 if newpath and oldpath:
408 408 modified.add(newpath)
409 409 elif newpath and not oldpath:
410 410 added.add(newpath)
411 411 elif not newpath and oldpath:
412 412 deleted.add(oldpath)
413 413 return added, modified, deleted
414 414
415 415 def _get_paths_for_status(self, status):
416 416 """
417 417 Returns sorted list of paths for given ``status``.
418 418
419 419 :param status: one of: *added*, *modified* or *deleted*
420 420 """
421 421 added, modified, deleted = self._changes_cache
422 422 return sorted({
423 423 'added': list(added),
424 424 'modified': list(modified),
425 425 'deleted': list(deleted)}[status]
426 426 )
427 427
428 428 @LazyProperty
429 429 def added(self):
430 430 """
431 431 Returns list of added ``FileNode`` objects.
432 432 """
433 433 if not self.parents:
434 434 return list(self._get_file_nodes())
435 435 return AddedFileNodesGenerator(
436 436 [n for n in self._get_paths_for_status('added')], self)
437 437
438 438 @LazyProperty
439 439 def changed(self):
440 440 """
441 441 Returns list of modified ``FileNode`` objects.
442 442 """
443 443 if not self.parents:
444 444 return []
445 445 return ChangedFileNodesGenerator(
446 446 [n for n in self._get_paths_for_status('modified')], self)
447 447
448 448 @LazyProperty
449 449 def removed(self):
450 450 """
451 451 Returns list of removed ``FileNode`` objects.
452 452 """
453 453 if not self.parents:
454 454 return []
455 455 return RemovedFileNodesGenerator(
456 456 [n for n in self._get_paths_for_status('deleted')], self)
457 457
458 458 def _get_submodule_url(self, submodule_path):
459 459 git_modules_path = '.gitmodules'
460 460
461 461 if self._submodules is None:
462 462 self._submodules = {}
463 463
464 464 try:
465 465 submodules_node = self.get_node(git_modules_path)
466 466 except NodeDoesNotExistError:
467 467 return None
468 468
469 469 # ConfigParser fails if there are whitespaces, also it needs an iterable
470 470 # file like content
471 471 def iter_content(_content):
472 472 for line in _content.splitlines():
473 473 yield line
474 474
475 475 parser = configparser.ConfigParser()
476 476 parser.read_file(iter_content(submodules_node.content))
477 477
478 478 for section in parser.sections():
479 479 path = parser.get(section, 'path')
480 480 url = parser.get(section, 'url')
481 481 if path and url:
482 482 self._submodules[path.strip('/')] = url
483 483
484 484 return self._submodules.get(submodule_path.strip('/'))
General Comments 0
You need to be logged in to leave comments. Login now