##// END OF EJS Templates
git: fix submodule handling for git repositories
super-admin -
r5126:de59bf70 default
parent child Browse files
Show More
@@ -1,488 +1,484 b''
1 # Copyright (C) 2014-2023 RhodeCode GmbH
1 # Copyright (C) 2014-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 """
19 """
20 GIT commit module
20 GIT commit module
21 """
21 """
22
22
23 import io
23 import stat
24 import stat
24 import configparser
25 import configparser
25 from itertools import chain
26 from itertools import chain
26
27
27 from zope.cachedescriptors.property import Lazy as LazyProperty
28 from zope.cachedescriptors.property import Lazy as LazyProperty
28
29
29 from rhodecode.lib.datelib import utcdate_fromtimestamp
30 from rhodecode.lib.datelib import utcdate_fromtimestamp
30 from rhodecode.lib.str_utils import safe_bytes, safe_str
31 from rhodecode.lib.str_utils import safe_bytes, safe_str
31 from rhodecode.lib.vcs.backends import base
32 from rhodecode.lib.vcs.backends import base
32 from rhodecode.lib.vcs.exceptions import CommitError, NodeDoesNotExistError
33 from rhodecode.lib.vcs.exceptions import CommitError, NodeDoesNotExistError
33 from rhodecode.lib.vcs.nodes import (
34 from rhodecode.lib.vcs.nodes import (
34 FileNode, DirNode, NodeKind, RootNode, SubModuleNode,
35 FileNode, DirNode, NodeKind, RootNode, SubModuleNode,
35 ChangedFileNodesGenerator, AddedFileNodesGenerator,
36 ChangedFileNodesGenerator, AddedFileNodesGenerator,
36 RemovedFileNodesGenerator, LargeFileNode)
37 RemovedFileNodesGenerator, LargeFileNode)
37
38
38
39
39 class GitCommit(base.BaseCommit):
40 class GitCommit(base.BaseCommit):
40 """
41 """
41 Represents state of the repository at single commit id.
42 Represents state of the repository at single commit id.
42 """
43 """
43
44
44 _filter_pre_load = [
45 _filter_pre_load = [
45 # done through a more complex tree walk on parents
46 # done through a more complex tree walk on parents
46 "affected_files",
47 "affected_files",
47 # done through subprocess not remote call
48 # done through subprocess not remote call
48 "children",
49 "children",
49 # done through a more complex tree walk on parents
50 # done through a more complex tree walk on parents
50 "status",
51 "status",
51 # mercurial specific property not supported here
52 # mercurial specific property not supported here
52 "_file_paths",
53 "_file_paths",
53 # mercurial specific property not supported here
54 # mercurial specific property not supported here
54 'obsolete',
55 'obsolete',
55 # mercurial specific property not supported here
56 # mercurial specific property not supported here
56 'phase',
57 'phase',
57 # mercurial specific property not supported here
58 # mercurial specific property not supported here
58 'hidden'
59 'hidden'
59 ]
60 ]
60
61
61 def __init__(self, repository, raw_id, idx, pre_load=None):
62 def __init__(self, repository, raw_id, idx, pre_load=None):
62 self.repository = repository
63 self.repository = repository
63 self._remote = repository._remote
64 self._remote = repository._remote
64 # TODO: johbo: Tweak of raw_id should not be necessary
65 # TODO: johbo: Tweak of raw_id should not be necessary
65 self.raw_id = safe_str(raw_id)
66 self.raw_id = safe_str(raw_id)
66 self.idx = idx
67 self.idx = idx
67
68
68 self._set_bulk_properties(pre_load)
69 self._set_bulk_properties(pre_load)
69
70
70 # caches
71 # caches
71 self._stat_modes = {} # stat info for paths
72 self._stat_modes = {} # stat info for paths
72 self._paths = {} # path processed with parse_tree
73 self._paths = {} # path processed with parse_tree
73 self.nodes = {}
74 self.nodes = {}
74 self._submodules = None
75 self._submodules = None
75
76
76 def _set_bulk_properties(self, pre_load):
77 def _set_bulk_properties(self, pre_load):
77
78
78 if not pre_load:
79 if not pre_load:
79 return
80 return
80 pre_load = [entry for entry in pre_load
81 pre_load = [entry for entry in pre_load
81 if entry not in self._filter_pre_load]
82 if entry not in self._filter_pre_load]
82 if not pre_load:
83 if not pre_load:
83 return
84 return
84
85
85 result = self._remote.bulk_request(self.raw_id, pre_load)
86 result = self._remote.bulk_request(self.raw_id, pre_load)
86 for attr, value in result.items():
87 for attr, value in result.items():
87 if attr in ["author", "message"]:
88 if attr in ["author", "message"]:
88 if value:
89 if value:
89 value = safe_str(value)
90 value = safe_str(value)
90 elif attr == "date":
91 elif attr == "date":
91 value = utcdate_fromtimestamp(*value)
92 value = utcdate_fromtimestamp(*value)
92 elif attr == "parents":
93 elif attr == "parents":
93 value = self._make_commits(value)
94 value = self._make_commits(value)
94 elif attr == "branch":
95 elif attr == "branch":
95 value = self._set_branch(value)
96 value = self._set_branch(value)
96 self.__dict__[attr] = value
97 self.__dict__[attr] = value
97
98
98 @LazyProperty
99 @LazyProperty
99 def _commit(self):
100 def _commit(self):
100 return self._remote[self.raw_id]
101 return self._remote[self.raw_id]
101
102
102 @LazyProperty
103 @LazyProperty
103 def _tree_id(self):
104 def _tree_id(self):
104 return self._remote[self._commit['tree']]['id']
105 return self._remote[self._commit['tree']]['id']
105
106
106 @LazyProperty
107 @LazyProperty
107 def id(self):
108 def id(self):
108 return self.raw_id
109 return self.raw_id
109
110
110 @LazyProperty
111 @LazyProperty
111 def short_id(self):
112 def short_id(self):
112 return self.raw_id[:12]
113 return self.raw_id[:12]
113
114
114 @LazyProperty
115 @LazyProperty
115 def message(self):
116 def message(self):
116 return safe_str(self._remote.message(self.id))
117 return safe_str(self._remote.message(self.id))
117
118
118 @LazyProperty
119 @LazyProperty
119 def committer(self):
120 def committer(self):
120 return safe_str(self._remote.author(self.id))
121 return safe_str(self._remote.author(self.id))
121
122
122 @LazyProperty
123 @LazyProperty
123 def author(self):
124 def author(self):
124 return safe_str(self._remote.author(self.id))
125 return safe_str(self._remote.author(self.id))
125
126
126 @LazyProperty
127 @LazyProperty
127 def date(self):
128 def date(self):
128 unix_ts, tz = self._remote.date(self.raw_id)
129 unix_ts, tz = self._remote.date(self.raw_id)
129 return utcdate_fromtimestamp(unix_ts, tz)
130 return utcdate_fromtimestamp(unix_ts, tz)
130
131
131 @LazyProperty
132 @LazyProperty
132 def status(self):
133 def status(self):
133 """
134 """
134 Returns modified, added, removed, deleted files for current commit
135 Returns modified, added, removed, deleted files for current commit
135 """
136 """
136 return self.changed, self.added, self.removed
137 return self.changed, self.added, self.removed
137
138
138 @LazyProperty
139 @LazyProperty
139 def tags(self):
140 def tags(self):
140 tags = [safe_str(name) for name,
141 tags = [safe_str(name) for name,
141 commit_id in self.repository.tags.items()
142 commit_id in self.repository.tags.items()
142 if commit_id == self.raw_id]
143 if commit_id == self.raw_id]
143 return tags
144 return tags
144
145
145 @LazyProperty
146 @LazyProperty
146 def commit_branches(self):
147 def commit_branches(self):
147 branches = []
148 branches = []
148 for name, commit_id in self.repository.branches.items():
149 for name, commit_id in self.repository.branches.items():
149 if commit_id == self.raw_id:
150 if commit_id == self.raw_id:
150 branches.append(name)
151 branches.append(name)
151 return branches
152 return branches
152
153
153 def _set_branch(self, branches):
154 def _set_branch(self, branches):
154 if branches:
155 if branches:
155 # actually commit can have multiple branches in git
156 # actually commit can have multiple branches in git
156 return safe_str(branches[0])
157 return safe_str(branches[0])
157
158
158 @LazyProperty
159 @LazyProperty
159 def branch(self):
160 def branch(self):
160 branches = self._remote.branch(self.raw_id)
161 branches = self._remote.branch(self.raw_id)
161 return self._set_branch(branches)
162 return self._set_branch(branches)
162
163
163 def _get_tree_id_for_path(self, path):
164 def _get_tree_id_for_path(self, path):
164
165
165 path = safe_str(path)
166 path = safe_str(path)
166 if path in self._paths:
167 if path in self._paths:
167 return self._paths[path]
168 return self._paths[path]
168
169
169 tree_id = self._tree_id
170 tree_id = self._tree_id
170
171
171 path = path.strip('/')
172 path = path.strip('/')
172 if path == '':
173 if path == '':
173 data = [tree_id, "tree"]
174 data = [tree_id, "tree"]
174 self._paths[''] = data
175 self._paths[''] = data
175 return data
176 return data
176
177
177 tree_id, tree_type, tree_mode = \
178 tree_id, tree_type, tree_mode = \
178 self._remote.tree_and_type_for_path(self.raw_id, path)
179 self._remote.tree_and_type_for_path(self.raw_id, path)
179 if tree_id is None:
180 if tree_id is None:
180 raise self.no_node_at_path(path)
181 raise self.no_node_at_path(path)
181
182
182 self._paths[path] = [tree_id, tree_type]
183 self._paths[path] = [tree_id, tree_type]
183 self._stat_modes[path] = tree_mode
184 self._stat_modes[path] = tree_mode
184
185
185 if path not in self._paths:
186 if path not in self._paths:
186 raise self.no_node_at_path(path)
187 raise self.no_node_at_path(path)
187
188
188 return self._paths[path]
189 return self._paths[path]
189
190
190 def _get_kind(self, path):
191 def _get_kind(self, path):
191 tree_id, type_ = self._get_tree_id_for_path(path)
192 tree_id, type_ = self._get_tree_id_for_path(path)
192 if type_ == 'blob':
193 if type_ == 'blob':
193 return NodeKind.FILE
194 return NodeKind.FILE
194 elif type_ == 'tree':
195 elif type_ == 'tree':
195 return NodeKind.DIR
196 return NodeKind.DIR
196 elif type_ == 'link':
197 elif type_ == 'link':
197 return NodeKind.SUBMODULE
198 return NodeKind.SUBMODULE
198 return None
199 return None
199
200
200 def _assert_is_path(self, path):
201 def _assert_is_path(self, path):
201 path = self._fix_path(path)
202 path = self._fix_path(path)
202 if self._get_kind(path) != NodeKind.FILE:
203 if self._get_kind(path) != NodeKind.FILE:
203 raise CommitError(f"File does not exist for commit {self.raw_id} at '{path}'")
204 raise CommitError(f"File does not exist for commit {self.raw_id} at '{path}'")
204 return path
205 return path
205
206
206 def _get_file_nodes(self):
207 def _get_file_nodes(self):
207 return chain(*(t[2] for t in self.walk()))
208 return chain(*(t[2] for t in self.walk()))
208
209
209 @LazyProperty
210 @LazyProperty
210 def parents(self):
211 def parents(self):
211 """
212 """
212 Returns list of parent commits.
213 Returns list of parent commits.
213 """
214 """
214 parent_ids = self._remote.parents(self.id)
215 parent_ids = self._remote.parents(self.id)
215 return self._make_commits(parent_ids)
216 return self._make_commits(parent_ids)
216
217
217 @LazyProperty
218 @LazyProperty
218 def children(self):
219 def children(self):
219 """
220 """
220 Returns list of child commits.
221 Returns list of child commits.
221 """
222 """
222
223
223 children = self._remote.children(self.raw_id)
224 children = self._remote.children(self.raw_id)
224 return self._make_commits(children)
225 return self._make_commits(children)
225
226
226 def _make_commits(self, commit_ids):
227 def _make_commits(self, commit_ids):
227 def commit_maker(_commit_id):
228 def commit_maker(_commit_id):
228 return self.repository.get_commit(commit_id=_commit_id)
229 return self.repository.get_commit(commit_id=_commit_id)
229
230
230 return [commit_maker(commit_id) for commit_id in commit_ids]
231 return [commit_maker(commit_id) for commit_id in commit_ids]
231
232
232 def get_file_mode(self, path: bytes):
233 def get_file_mode(self, path: bytes):
233 """
234 """
234 Returns stat mode of the file at the given `path`.
235 Returns stat mode of the file at the given `path`.
235 """
236 """
236 path = self._assert_is_path(path)
237 path = self._assert_is_path(path)
237
238
238 # ensure path is traversed
239 # ensure path is traversed
239 self._get_tree_id_for_path(path)
240 self._get_tree_id_for_path(path)
240
241
241 return self._stat_modes[path]
242 return self._stat_modes[path]
242
243
243 def is_link(self, path):
244 def is_link(self, path):
244 return stat.S_ISLNK(self.get_file_mode(path))
245 return stat.S_ISLNK(self.get_file_mode(path))
245
246
246 def is_node_binary(self, path):
247 def is_node_binary(self, path):
247 tree_id, _ = self._get_tree_id_for_path(path)
248 tree_id, _ = self._get_tree_id_for_path(path)
248 return self._remote.is_binary(tree_id)
249 return self._remote.is_binary(tree_id)
249
250
250 def node_md5_hash(self, path):
251 def node_md5_hash(self, path):
251 path = self._assert_is_path(path)
252 path = self._assert_is_path(path)
252 return self._remote.md5_hash(self.raw_id, path)
253 return self._remote.md5_hash(self.raw_id, path)
253
254
254 def get_file_content(self, path):
255 def get_file_content(self, path):
255 """
256 """
256 Returns content of the file at given `path`.
257 Returns content of the file at given `path`.
257 """
258 """
258 tree_id, _ = self._get_tree_id_for_path(path)
259 tree_id, _ = self._get_tree_id_for_path(path)
259 return self._remote.blob_as_pretty_string(tree_id)
260 return self._remote.blob_as_pretty_string(tree_id)
260
261
261 def get_file_content_streamed(self, path):
262 def get_file_content_streamed(self, path):
262 tree_id, _ = self._get_tree_id_for_path(path)
263 tree_id, _ = self._get_tree_id_for_path(path)
263 stream_method = getattr(self._remote, 'stream:blob_as_pretty_string')
264 stream_method = getattr(self._remote, 'stream:blob_as_pretty_string')
264 return stream_method(tree_id)
265 return stream_method(tree_id)
265
266
266 def get_file_size(self, path):
267 def get_file_size(self, path):
267 """
268 """
268 Returns size of the file at given `path`.
269 Returns size of the file at given `path`.
269 """
270 """
270 tree_id, _ = self._get_tree_id_for_path(path)
271 tree_id, _ = self._get_tree_id_for_path(path)
271 return self._remote.blob_raw_length(tree_id)
272 return self._remote.blob_raw_length(tree_id)
272
273
273 def get_path_history(self, path, limit=None, pre_load=None):
274 def get_path_history(self, path, limit=None, pre_load=None):
274 """
275 """
275 Returns history of file as reversed list of `GitCommit` objects for
276 Returns history of file as reversed list of `GitCommit` objects for
276 which file at given `path` has been modified.
277 which file at given `path` has been modified.
277 """
278 """
278
279
279 path = self._assert_is_path(path)
280 path = self._assert_is_path(path)
280 hist = self._remote.node_history(self.raw_id, path, limit)
281 hist = self._remote.node_history(self.raw_id, path, limit)
281 return [
282 return [
282 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
283 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
283 for commit_id in hist]
284 for commit_id in hist]
284
285
285 def get_file_annotate(self, path, pre_load=None):
286 def get_file_annotate(self, path, pre_load=None):
286 """
287 """
287 Returns a generator of four element tuples with
288 Returns a generator of four element tuples with
288 lineno, commit_id, commit lazy loader and line
289 lineno, commit_id, commit lazy loader and line
289 """
290 """
290
291
291 result = self._remote.node_annotate(self.raw_id, path)
292 result = self._remote.node_annotate(self.raw_id, path)
292
293
293 for ln_no, commit_id, content in result:
294 for ln_no, commit_id, content in result:
294 yield (
295 yield (
295 ln_no, commit_id,
296 ln_no, commit_id,
296 lambda: self.repository.get_commit(commit_id=commit_id, pre_load=pre_load),
297 lambda: self.repository.get_commit(commit_id=commit_id, pre_load=pre_load),
297 content)
298 content)
298
299
299 def get_nodes(self, path, pre_load=None):
300 def get_nodes(self, path, pre_load=None):
300
301
301 if self._get_kind(path) != NodeKind.DIR:
302 if self._get_kind(path) != NodeKind.DIR:
302 raise CommitError(
303 raise CommitError(
303 f"Directory does not exist for commit {self.raw_id} at '{path}'")
304 f"Directory does not exist for commit {self.raw_id} at '{path}'")
304 path = self._fix_path(path)
305 path = self._fix_path(path)
305
306
306 tree_id, _ = self._get_tree_id_for_path(path)
307 tree_id, _ = self._get_tree_id_for_path(path)
307
308
308 dirnodes = []
309 dirnodes = []
309 filenodes = []
310 filenodes = []
310
311
311 # extracted tree ID gives us our files...
312 # extracted tree ID gives us our files...
312 bytes_path = safe_str(path) # libgit operates on bytes
313 str_path = safe_str(path) # libgit operates on bytes
313 for name, stat_, id_, type_ in self._remote.tree_items(tree_id):
314 for name, stat_, id_, type_ in self._remote.tree_items(tree_id):
314 if type_ == 'link':
315 if type_ == 'link':
315 url = self._get_submodule_url('/'.join((bytes_path, name)))
316 url = self._get_submodule_url('/'.join((str_path, name)))
316 dirnodes.append(SubModuleNode(
317 dirnodes.append(SubModuleNode(
317 name, url=url, commit=id_, alias=self.repository.alias))
318 name, url=url, commit=id_, alias=self.repository.alias))
318 continue
319 continue
319
320
320 if bytes_path != '':
321 if str_path != '':
321 obj_path = '/'.join((bytes_path, name))
322 obj_path = '/'.join((str_path, name))
322 else:
323 else:
323 obj_path = name
324 obj_path = name
324 if obj_path not in self._stat_modes:
325 if obj_path not in self._stat_modes:
325 self._stat_modes[obj_path] = stat_
326 self._stat_modes[obj_path] = stat_
326
327
327 if type_ == 'tree':
328 if type_ == 'tree':
328 dirnodes.append(DirNode(safe_bytes(obj_path), commit=self))
329 dirnodes.append(DirNode(safe_bytes(obj_path), commit=self))
329 elif type_ == 'blob':
330 elif type_ == 'blob':
330 filenodes.append(FileNode(safe_bytes(obj_path), commit=self, mode=stat_, pre_load=pre_load))
331 filenodes.append(FileNode(safe_bytes(obj_path), commit=self, mode=stat_, pre_load=pre_load))
331 else:
332 else:
332 raise CommitError(f"Requested object should be Tree or Blob, is {type_}")
333 raise CommitError(f"Requested object should be Tree or Blob, is {type_}")
333
334
334 nodes = dirnodes + filenodes
335 nodes = dirnodes + filenodes
335 for node in nodes:
336 for node in nodes:
336 if node.path not in self.nodes:
337 if node.path not in self.nodes:
337 self.nodes[node.path] = node
338 self.nodes[node.path] = node
338 nodes.sort()
339 nodes.sort()
339 return nodes
340 return nodes
340
341
341 def get_node(self, path, pre_load=None):
342 def get_node(self, path, pre_load=None):
342 path = self._fix_path(path)
343 path = self._fix_path(path)
343 if path not in self.nodes:
344 if path not in self.nodes:
344 try:
345 try:
345 tree_id, type_ = self._get_tree_id_for_path(path)
346 tree_id, type_ = self._get_tree_id_for_path(path)
346 except CommitError:
347 except CommitError:
347 raise NodeDoesNotExistError(
348 raise NodeDoesNotExistError(
348 f"Cannot find one of parents' directories for a given "
349 f"Cannot find one of parents' directories for a given "
349 f"path: {path}")
350 f"path: {path}")
350
351
351 if type_ in ['link', 'commit']:
352 if type_ in ['link', 'commit']:
352 url = self._get_submodule_url(path)
353 url = self._get_submodule_url(path)
353 node = SubModuleNode(path, url=url, commit=tree_id,
354 node = SubModuleNode(path, url=url, commit=tree_id,
354 alias=self.repository.alias)
355 alias=self.repository.alias)
355 elif type_ == 'tree':
356 elif type_ == 'tree':
356 if path == '':
357 if path == '':
357 node = RootNode(commit=self)
358 node = RootNode(commit=self)
358 else:
359 else:
359 node = DirNode(safe_bytes(path), commit=self)
360 node = DirNode(safe_bytes(path), commit=self)
360 elif type_ == 'blob':
361 elif type_ == 'blob':
361 node = FileNode(safe_bytes(path), commit=self, pre_load=pre_load)
362 node = FileNode(safe_bytes(path), commit=self, pre_load=pre_load)
362 self._stat_modes[path] = node.mode
363 self._stat_modes[path] = node.mode
363 else:
364 else:
364 raise self.no_node_at_path(path)
365 raise self.no_node_at_path(path)
365
366
366 # cache node
367 # cache node
367 self.nodes[path] = node
368 self.nodes[path] = node
368
369
369 return self.nodes[path]
370 return self.nodes[path]
370
371
371 def get_largefile_node(self, path):
372 def get_largefile_node(self, path):
372 tree_id, _ = self._get_tree_id_for_path(path)
373 tree_id, _ = self._get_tree_id_for_path(path)
373 pointer_spec = self._remote.is_large_file(tree_id)
374 pointer_spec = self._remote.is_large_file(tree_id)
374
375
375 if pointer_spec:
376 if pointer_spec:
376 # content of that file regular FileNode is the hash of largefile
377 # content of that file regular FileNode is the hash of largefile
377 file_id = pointer_spec.get('oid_hash')
378 file_id = pointer_spec.get('oid_hash')
378 if self._remote.in_largefiles_store(file_id):
379 if self._remote.in_largefiles_store(file_id):
379 lf_path = self._remote.store_path(file_id)
380 lf_path = self._remote.store_path(file_id)
380 return LargeFileNode(safe_bytes(lf_path), commit=self, org_path=path)
381 return LargeFileNode(safe_bytes(lf_path), commit=self, org_path=path)
381
382
382 @LazyProperty
383 @LazyProperty
383 def affected_files(self):
384 def affected_files(self):
384 """
385 """
385 Gets a fast accessible file changes for given commit
386 Gets a fast accessible file changes for given commit
386 """
387 """
387 added, modified, deleted = self._changes_cache
388 added, modified, deleted = self._changes_cache
388 return list(added.union(modified).union(deleted))
389 return list(added.union(modified).union(deleted))
389
390
390 @LazyProperty
391 @LazyProperty
391 def _changes_cache(self):
392 def _changes_cache(self):
392 added = set()
393 added = set()
393 modified = set()
394 modified = set()
394 deleted = set()
395 deleted = set()
395
396
396 parents = self.parents
397 parents = self.parents
397 if not self.parents:
398 if not self.parents:
398 parents = [base.EmptyCommit()]
399 parents = [base.EmptyCommit()]
399 for parent in parents:
400 for parent in parents:
400 if isinstance(parent, base.EmptyCommit):
401 if isinstance(parent, base.EmptyCommit):
401 oid = None
402 oid = None
402 else:
403 else:
403 oid = parent.raw_id
404 oid = parent.raw_id
404 _added, _modified, _deleted = self._remote.tree_changes(oid, self.raw_id)
405 _added, _modified, _deleted = self._remote.tree_changes(oid, self.raw_id)
405 added = added | set(_added)
406 added = added | set(_added)
406 modified = modified | set(_modified)
407 modified = modified | set(_modified)
407 deleted = deleted | set(_deleted)
408 deleted = deleted | set(_deleted)
408
409
409 return added, modified, deleted
410 return added, modified, deleted
410
411
411 def _get_paths_for_status(self, status):
412 def _get_paths_for_status(self, status):
412 """
413 """
413 Returns sorted list of paths for given ``status``.
414 Returns sorted list of paths for given ``status``.
414
415
415 :param status: one of: *added*, *modified* or *deleted*
416 :param status: one of: *added*, *modified* or *deleted*
416 """
417 """
417 added, modified, deleted = self._changes_cache
418 added, modified, deleted = self._changes_cache
418 return sorted({
419 return sorted({
419 'added': list(added),
420 'added': list(added),
420 'modified': list(modified),
421 'modified': list(modified),
421 'deleted': list(deleted)}[status]
422 'deleted': list(deleted)}[status]
422 )
423 )
423
424
424 @LazyProperty
425 @LazyProperty
425 def added(self):
426 def added(self):
426 """
427 """
427 Returns list of added ``FileNode`` objects.
428 Returns list of added ``FileNode`` objects.
428 """
429 """
429 if not self.parents:
430 if not self.parents:
430 return list(self._get_file_nodes())
431 return list(self._get_file_nodes())
431 return AddedFileNodesGenerator(self.added_paths, self)
432 return AddedFileNodesGenerator(self.added_paths, self)
432
433
433 @LazyProperty
434 @LazyProperty
434 def added_paths(self):
435 def added_paths(self):
435 return [n for n in self._get_paths_for_status('added')]
436 return [n for n in self._get_paths_for_status('added')]
436
437
437 @LazyProperty
438 @LazyProperty
438 def changed(self):
439 def changed(self):
439 """
440 """
440 Returns list of modified ``FileNode`` objects.
441 Returns list of modified ``FileNode`` objects.
441 """
442 """
442 if not self.parents:
443 if not self.parents:
443 return []
444 return []
444 return ChangedFileNodesGenerator(self.changed_paths, self)
445 return ChangedFileNodesGenerator(self.changed_paths, self)
445
446
446 @LazyProperty
447 @LazyProperty
447 def changed_paths(self):
448 def changed_paths(self):
448 return [n for n in self._get_paths_for_status('modified')]
449 return [n for n in self._get_paths_for_status('modified')]
449
450
450 @LazyProperty
451 @LazyProperty
451 def removed(self):
452 def removed(self):
452 """
453 """
453 Returns list of removed ``FileNode`` objects.
454 Returns list of removed ``FileNode`` objects.
454 """
455 """
455 if not self.parents:
456 if not self.parents:
456 return []
457 return []
457 return RemovedFileNodesGenerator(self.removed_paths, self)
458 return RemovedFileNodesGenerator(self.removed_paths, self)
458
459
459 @LazyProperty
460 @LazyProperty
460 def removed_paths(self):
461 def removed_paths(self):
461 return [n for n in self._get_paths_for_status('deleted')]
462 return [n for n in self._get_paths_for_status('deleted')]
462
463
463 def _get_submodule_url(self, submodule_path):
464 def _get_submodule_url(self, submodule_path):
464 git_modules_path = '.gitmodules'
465 git_modules_path = '.gitmodules'
465
466
466 if self._submodules is None:
467 if self._submodules is None:
467 self._submodules = {}
468 self._submodules = {}
468
469
469 try:
470 try:
470 submodules_node = self.get_node(git_modules_path)
471 submodules_node = self.get_node(git_modules_path)
471 except NodeDoesNotExistError:
472 except NodeDoesNotExistError:
472 return None
473 return None
473
474
474 # ConfigParser fails if there are whitespaces, also it needs an iterable
475 # file like content
476 def iter_content(_content):
477 yield from _content.splitlines()
478
479 parser = configparser.RawConfigParser()
475 parser = configparser.RawConfigParser()
480 parser.read_file(iter_content(submodules_node.content))
476 parser.read_file(io.StringIO(submodules_node.str_content))
481
477
482 for section in parser.sections():
478 for section in parser.sections():
483 path = parser.get(section, 'path')
479 path = parser.get(section, 'path')
484 url = parser.get(section, 'url')
480 url = parser.get(section, 'url')
485 if path and url:
481 if path and url:
486 self._submodules[path.strip('/')] = url
482 self._submodules[path.strip('/')] = url
487
483
488 return self._submodules.get(submodule_path.strip('/'))
484 return self._submodules.get(submodule_path.strip('/'))
General Comments 0
You need to be logged in to leave comments. Login now