Skip to content

Commit ac4133f

Browse files
committed
SymbolicRefence base is now fully aware of pack files in all operations.
Ref(anytype) Iteration was improved such that automatic filtering now also works for SymbolicReferences ( which only return symbolic refs)
1 parent 8e29a91 commit ac4133f

File tree

3 files changed

+210
-97
lines changed

3 files changed

+210
-97
lines changed

lib/git/refs.py

Lines changed: 171 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,17 @@ def name(self):
5454
def _get_path(self):
5555
return join_path_native(self.repo.git_dir, self.path)
5656

57+
@classmethod
58+
def _get_packed_refs_path(cls, repo):
59+
return os.path.join(repo.git_dir, 'packed-refs')
60+
5761
@classmethod
5862
def _iter_packed_refs(cls, repo):
5963
"""Returns an iterator yielding pairs of sha1/path pairs for the corresponding
6064
refs.
6165
NOTE: The packed refs file will be kept open as long as we iterate"""
6266
try:
63-
fp = open(os.path.join(repo.git_dir, 'packed-refs'), 'r')
67+
fp = open(cls._get_packed_refs_path(repo), 'r')
6468
for line in fp:
6569
line = line.strip()
6670
if not line:
@@ -87,13 +91,10 @@ def _iter_packed_refs(cls, repo):
8791
# I believe files are closing themselves on destruction, so it is
8892
# alright.
8993

90-
def _get_commit(self):
91-
"""
92-
Returns:
93-
Commit object we point to, works for detached and non-detached
94-
SymbolicReferences
95-
"""
96-
# we partially reimplement it to prevent unnecessary file access
94+
def _get_ref_info(self):
95+
"""Return: (sha, target_ref_path) if available, the sha the file at
96+
rela_path points to, or None. target_ref_path is the reference we
97+
point to, or None"""
9798
tokens = None
9899
try:
99100
fp = open(self._get_path(), 'r')
@@ -111,15 +112,30 @@ def _get_commit(self):
111112
# END for each packed ref
112113
# END handle packed refs
113114

114-
# it is a detached reference
115+
# is it a reference ?
116+
if tokens[0] == 'ref:':
117+
return (None, tokens[1])
118+
119+
# its a commit
115120
if self.repo.re_hexsha_only.match(tokens[0]):
116-
return Commit(self.repo, tokens[0])
121+
return (tokens[0], None)
122+
123+
raise ValueError("Failed to parse reference information from %r" % self.path)
124+
125+
def _get_commit(self):
126+
"""
127+
Returns:
128+
Commit object we point to, works for detached and non-detached
129+
SymbolicReferences
130+
"""
131+
# we partially reimplement it to prevent unnecessary file access
132+
sha, target_ref_path = self._get_ref_info()
117133

118-
# must be a head ! Git does not allow symbol refs to other things than heads
119-
# Otherwise it would have detached it
120-
if tokens[0] != "ref:":
121-
raise ValueError("Failed to parse symbolic refernce: wanted 'ref: <hexsha>', got %r" % value)
122-
return Reference.from_path(self.repo, tokens[1]).commit
134+
# it is a detached reference
135+
if sha:
136+
return Commit(self.repo, sha)
137+
138+
return Reference.from_path(self.repo, target_ref_path).commit
123139

124140
def _set_commit(self, commit):
125141
"""
@@ -138,14 +154,10 @@ def _get_reference(self):
138154
Returns
139155
Reference Object we point to
140156
"""
141-
fp = open(self._get_path(), 'r')
142-
try:
143-
tokens = fp.readline().rstrip().split(' ')
144-
if tokens[0] != 'ref:':
145-
raise TypeError("%s is a detached symbolic reference as it points to %r" % (self, tokens[0]))
146-
return Reference.from_path(self.repo, tokens[1])
147-
finally:
148-
fp.close()
157+
sha, target_ref_path = self._get_ref_info()
158+
if target_ref_path is None:
159+
raise TypeError("%s is a detached symbolic reference as it points to %r" % (self, sha))
160+
return Reference.from_path(self.repo, target_ref_path)
149161

150162
def _set_reference(self, ref):
151163
"""
@@ -261,6 +273,40 @@ def delete(cls, repo, path):
261273
abs_path = os.path.join(repo.git_dir, full_ref_path)
262274
if os.path.exists(abs_path):
263275
os.remove(abs_path)
276+
else:
277+
# check packed refs
278+
pack_file_path = cls._get_packed_refs_path(repo)
279+
try:
280+
reader = open(pack_file_path)
281+
except (OSError,IOError):
282+
pass # it didnt exist at all
283+
else:
284+
new_lines = list()
285+
made_change = False
286+
dropped_last_line = False
287+
for line in reader:
288+
# keep line if it is a comment or if the ref to delete is not
289+
# in the line
290+
# If we deleted the last line and this one is a tag-reference object,
291+
# we drop it as well
292+
if ( line.startswith('#') or full_ref_path not in line ) and \
293+
( not dropped_last_line or dropped_last_line and not line.startswith('^') ):
294+
new_lines.append(line)
295+
dropped_last_line = False
296+
continue
297+
# END skip comments and lines without our path
298+
299+
# drop this line
300+
made_change = True
301+
dropped_last_line = True
302+
# END for each line in packed refs
303+
reader.close()
304+
305+
# write the new lines
306+
if made_change:
307+
open(pack_file_path, 'w').writelines(new_lines)
308+
# END open exception handling
309+
# END handle deletion
264310

265311
@classmethod
266312
def _create(cls, repo, path, resolve, reference, force):
@@ -367,6 +413,88 @@ def rename(self, new_path, force=False):
367413

368414
return self
369415

416+
@classmethod
417+
def _iter_items(cls, repo, common_path = None):
418+
if common_path is None:
419+
common_path = cls._common_path_default
420+
421+
rela_paths = set()
422+
423+
# walk loose refs
424+
# Currently we do not follow links
425+
for root, dirs, files in os.walk(join_path_native(repo.git_dir, common_path)):
426+
if 'refs/' not in root: # skip non-refs subfolders
427+
refs_id = [ i for i,d in enumerate(dirs) if d == 'refs' ]
428+
if refs_id:
429+
dirs[0:] = ['refs']
430+
# END prune non-refs folders
431+
432+
for f in files:
433+
abs_path = to_native_path_linux(join_path(root, f))
434+
rela_paths.add(abs_path.replace(to_native_path_linux(repo.git_dir) + '/', ""))
435+
# END for each file in root directory
436+
# END for each directory to walk
437+
438+
# read packed refs
439+
for sha, rela_path in cls._iter_packed_refs(repo):
440+
if rela_path.startswith(common_path):
441+
rela_paths.add(rela_path)
442+
# END relative path matches common path
443+
# END packed refs reading
444+
445+
# return paths in sorted order
446+
for path in sorted(rela_paths):
447+
try:
448+
yield cls.from_path(repo, path)
449+
except ValueError:
450+
continue
451+
# END for each sorted relative refpath
452+
453+
@classmethod
454+
def iter_items(cls, repo, common_path = None):
455+
"""
456+
Find all refs in the repository
457+
458+
``repo``
459+
is the Repo
460+
461+
``common_path``
462+
Optional keyword argument to the path which is to be shared by all
463+
returned Ref objects.
464+
Defaults to class specific portion if None assuring that only
465+
refs suitable for the actual class are returned.
466+
467+
Returns
468+
git.SymbolicReference[], each of them is guaranteed to be a symbolic
469+
ref which is not detached.
470+
471+
List is lexigraphically sorted
472+
The returned objects represent actual subclasses, such as Head or TagReference
473+
"""
474+
return ( r for r in cls._iter_items(repo, common_path) if r.__class__ == SymbolicReference or not r.is_detached )
475+
476+
@classmethod
477+
def from_path(cls, repo, path):
478+
"""
479+
Return
480+
Instance of type Reference, Head, or Tag
481+
depending on the given path
482+
"""
483+
if not path:
484+
raise ValueError("Cannot create Reference from %r" % path)
485+
486+
for ref_type in (HEAD, Head, RemoteReference, TagReference, Reference, SymbolicReference):
487+
try:
488+
instance = ref_type(repo, path)
489+
if instance.__class__ == SymbolicReference and instance.is_detached:
490+
raise ValueError("SymbolRef was detached, we drop it")
491+
return instance
492+
except ValueError:
493+
pass
494+
# END exception handling
495+
# END for each type to try
496+
raise ValueError("Could not find reference type suitable to handle path %r" % path)
497+
370498

371499
class Reference(SymbolicReference, LazyMixin, Iterable):
372500
"""
@@ -431,75 +559,6 @@ def name(self):
431559
return self.path # could be refs/HEAD
432560
return '/'.join(tokens[2:])
433561

434-
@classmethod
435-
def iter_items(cls, repo, common_path = None, **kwargs):
436-
"""
437-
Find all refs in the repository
438-
439-
``repo``
440-
is the Repo
441-
442-
``common_path``
443-
Optional keyword argument to the path which is to be shared by all
444-
returned Ref objects.
445-
Defaults to class specific portion if None assuring that only
446-
refs suitable for the actual class are returned.
447-
448-
Returns
449-
git.Reference[]
450-
451-
List is lexigraphically sorted
452-
The returned objects represent actual subclasses, such as Head or TagReference
453-
"""
454-
if common_path is None:
455-
common_path = cls._common_path_default
456-
457-
rela_paths = set()
458-
459-
# walk loose refs
460-
# Currently we do not follow links
461-
for root, dirs, files in os.walk(join_path_native(repo.git_dir, common_path)):
462-
for f in files:
463-
abs_path = to_native_path_linux(join_path(root, f))
464-
rela_paths.add(abs_path.replace(to_native_path_linux(repo.git_dir) + '/', ""))
465-
# END for each file in root directory
466-
# END for each directory to walk
467-
468-
# read packed refs
469-
for sha, rela_path in cls._iter_packed_refs(repo):
470-
if rela_path.startswith(common_path):
471-
rela_paths.add(rela_path)
472-
# END relative path matches common path
473-
# END packed refs reading
474-
475-
# return paths in sorted order
476-
for path in sorted(rela_paths):
477-
if path.endswith('/HEAD'):
478-
continue
479-
# END skip remote heads
480-
yield cls.from_path(repo, path)
481-
# END for each sorted relative refpath
482-
483-
484-
@classmethod
485-
def from_path(cls, repo, path):
486-
"""
487-
Return
488-
Instance of type Reference, Head, or Tag
489-
depending on the given path
490-
"""
491-
if not path:
492-
raise ValueError("Cannot create Reference from %r" % path)
493-
494-
for ref_type in (Head, RemoteReference, TagReference, Reference):
495-
try:
496-
return ref_type(repo, path)
497-
except ValueError:
498-
pass
499-
# END exception handling
500-
# END for each type to try
501-
raise ValueError("Could not find reference type suitable to handle path %r" % path)
502-
503562

504563
@classmethod
505564
def create(cls, repo, path, commit='HEAD', force=False ):
@@ -528,8 +587,15 @@ def create(cls, repo, path, commit='HEAD', force=False ):
528587
This does not alter the current HEAD, index or Working Tree
529588
"""
530589
return cls._create(repo, path, True, commit, force)
531-
532-
590+
591+
@classmethod
592+
def iter_items(cls, repo, common_path = None):
593+
"""
594+
Equivalent to SymbolicReference.iter_items, but will return non-detached
595+
references as well.
596+
"""
597+
return cls._iter_items(repo, common_path)
598+
533599

534600
class HEAD(SymbolicReference):
535601
"""
@@ -850,12 +916,21 @@ def remote_head(self):
850916
return '/'.join(tokens[3:])
851917

852918
@classmethod
853-
def delete(cls, repo, *remotes, **kwargs):
919+
def delete(cls, repo, *refs, **kwargs):
854920
"""
855921
Delete the given remote references.
856922
857923
Note
858924
kwargs are given for compatability with the base class method as we
859925
should not narrow the signature.
860926
"""
861-
repo.git.branch("-d", "-r", *remotes)
927+
repo.git.branch("-d", "-r", *refs)
928+
# the official deletion method will ignore remote symbolic refs - these
929+
# are generally ignored in the refs/ folder. We don't though
930+
# and delete remainders manually
931+
for ref in refs:
932+
try:
933+
os.remove(os.path.join(repo.git_dir, ref.path))
934+
except OSError:
935+
pass
936+
# END for each ref

lib/git/utils.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,6 @@ def list_items(cls, repo, *args, **kwargs):
347347
Returns:
348348
list(Item,...) list of item instances
349349
"""
350-
#return list(cls.iter_items(repo, *args, **kwargs))
351350
out_list = IterableList( cls._id_attribute_ )
352351
out_list.extend(cls.iter_items(repo, *args, **kwargs))
353352
return out_list

test/git/test_refs.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,9 @@ def test_head_reset(self, rw_repo):
318318
assert not symref.is_detached
319319
# END for each ref
320320

321+
# create a new non-head ref just to be sure we handle it even if packed
322+
Reference.create(rw_repo, full_ref)
323+
321324
# test ref listing - assure we have packed refs
322325
rw_repo.git.pack_refs(all=True, prune=True)
323326
heads = rw_repo.heads
@@ -326,3 +329,39 @@ def test_head_reset(self, rw_repo):
326329
assert active_branch in heads
327330
assert rw_repo.tags
328331

332+
# we should be able to iterate all symbolic refs as well - in that case
333+
# we should expect only symbolic references to be returned
334+
for symref in SymbolicReference.iter_items(rw_repo):
335+
assert not symref.is_detached
336+
337+
# when iterating references, we can get references and symrefs
338+
# when deleting all refs, I'd expect them to be gone ! Even from
339+
# the packed ones
340+
# For this to work, we must not be on any branch
341+
rw_repo.head.reference = rw_repo.head.commit
342+
deleted_refs = set()
343+
for ref in Reference.iter_items(rw_repo):
344+
if ref.is_detached:
345+
ref.delete(rw_repo, ref)
346+
deleted_refs.add(ref)
347+
# END delete ref
348+
# END for each ref to iterate and to delete
349+
assert deleted_refs
350+
351+
for ref in Reference.iter_items(rw_repo):
352+
if ref.is_detached:
353+
assert ref not in deleted_refs
354+
# END for each ref
355+
356+
# reattach head - head will not be returned if it is not a symbolic
357+
# ref
358+
rw_repo.head.reference = Head.create(rw_repo, "master")
359+
360+
# At least the head should still exist
361+
assert os.path.isfile(os.path.join(rw_repo.git_dir, 'HEAD'))
362+
refs = list(SymbolicReference.iter_items(rw_repo))
363+
assert len(refs) == 1
364+
365+
366+
367+

0 commit comments

Comments
 (0)