Skip to content

Commit da12df9

Browse files
committed
Merge branch 'remote-fixes'
2 parents b7ae99c + 3379127 commit da12df9

File tree

7 files changed

+146
-29
lines changed

7 files changed

+146
-29
lines changed

git/db/cmd/base.py

+29-2
Original file line numberDiff line numberDiff line change
@@ -432,20 +432,47 @@ def _from_line(cls, repo, line, fetch_line):
432432
ref_type = None
433433
if remote_local_ref == "FETCH_HEAD":
434434
ref_type = SymbolicReference
435-
elif ref_type_name == "branch":
435+
elif ref_type_name in ("remote-tracking", "branch"):
436+
# note: remote-tracking is just the first part of the 'remote-tracking branch' token.
437+
# We don't parse it correctly, but its enough to know what to do, and its new in git 1.7something
436438
ref_type = RemoteReference
437439
elif ref_type_name == "tag":
438440
ref_type = TagReference
439441
else:
440442
raise TypeError("Cannot handle reference type: %r" % ref_type_name)
443+
#END handle ref type
441444

442445
# create ref instance
443446
if ref_type is SymbolicReference:
444447
remote_local_ref = ref_type(repo, "FETCH_HEAD")
445448
else:
446-
remote_local_ref = Reference.from_path(repo, join_path(ref_type._common_path_default, remote_local_ref.strip()))
449+
# determine prefix. Tags are usually pulled into refs/tags, they may have subdirectories.
450+
# It is not clear sometimes where exactly the item is, unless we have an absolute path as indicated
451+
# by the 'ref/' prefix. Otherwise even a tag could be in refs/remotes, which is when it will have the
452+
# 'tags/' subdirectory in its path.
453+
# We don't want to test for actual existence, but try to figure everything out analytically.
454+
ref_path = None
455+
remote_local_ref = remote_local_ref.strip()
456+
if remote_local_ref.startswith(Reference._common_path_default + "/"):
457+
# always use actual type if we get absolute paths
458+
# Will always be the case if something is fetched outside of refs/remotes (if its not a tag)
459+
ref_path = remote_local_ref
460+
if ref_type is not TagReference and not remote_local_ref.startswith(RemoteReference._common_path_default + "/"):
461+
ref_type = Reference
462+
#END downgrade remote reference
463+
elif ref_type is TagReference and 'tags/' in remote_local_ref:
464+
# even though its a tag, it is located in refs/remotes
465+
ref_path = join_path(RemoteReference._common_path_default, remote_local_ref)
466+
else:
467+
ref_path = join_path(ref_type._common_path_default, remote_local_ref)
468+
#END obtain refpath
469+
470+
# even though the path could be within the git conventions, we make
471+
# sure we respect whatever the user wanted, and disabled path checking
472+
remote_local_ref = ref_type(repo, ref_path, check_path=False)
447473
# END create ref instance
448474

475+
449476
note = ( note and note.strip() ) or ''
450477

451478
# parse flags from control_character

git/refs/reference.py

+47-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@
1111

1212
__all__ = ["Reference"]
1313

14+
#{ Utilities
15+
def require_remote_ref_path(func):
16+
"""A decorator raising a TypeError if we are not a valid remote, based on the path"""
17+
def wrapper(self, *args):
18+
if not self.path.startswith(self._remote_common_path_default + "/"):
19+
raise ValueError("ref path does not point to a remote reference: %s" % path)
20+
return func(self, *args)
21+
#END wrapper
22+
wrapper.__name__ = func.__name__
23+
return wrapper
24+
#}END utilites
25+
1426

1527
class Reference(SymbolicReference, LazyMixin, Iterable):
1628
"""Represents a named reference to any object. Subclasses may apply restrictions though,
@@ -20,20 +32,24 @@ class Reference(SymbolicReference, LazyMixin, Iterable):
2032
_resolve_ref_on_create = True
2133
_common_path_default = "refs"
2234

23-
def __init__(self, repo, path):
35+
def __init__(self, repo, path, check_path = True):
2436
"""Initialize this instance
2537
:param repo: Our parent repository
2638
2739
:param path:
2840
Path relative to the .git/ directory pointing to the ref in question, i.e.
29-
refs/heads/master"""
30-
if not path.startswith(self._common_path_default+'/'):
31-
raise ValueError("Cannot instantiate %r from path %s, maybe use %s.to_full_path(name) to safely generate a valid full path from a name" % ( self.__class__.__name__, path, type(self).__name__))
41+
refs/heads/master
42+
:param check_path: if False, you can provide any path. Otherwise the path must start with the
43+
default path prefix of this type."""
44+
if check_path and not path.startswith(self._common_path_default+'/'):
45+
raise ValueError("Cannot instantiate %r from path %s" % (self.__class__.__name__, path))
3246
super(Reference, self).__init__(repo, path)
3347

3448

3549
def __str__(self):
3650
return self.name
51+
52+
#{ Interface
3753

3854
def set_object(self, object, logmsg = None):
3955
"""Special version which checks if the head-log needs an update as well"""
@@ -80,3 +96,30 @@ def iter_items(cls, repo, common_path = None):
8096
"""Equivalent to SymbolicReference.iter_items, but will return non-detached
8197
references as well."""
8298
return cls._iter_items(repo, common_path)
99+
100+
#}END interface
101+
102+
103+
#{ Remote Interface
104+
105+
@property
106+
@require_remote_ref_path
107+
def remote_name(self):
108+
"""
109+
:return:
110+
Name of the remote we are a reference of, such as 'origin' for a reference
111+
named 'origin/master'"""
112+
tokens = self.path.split('/')
113+
# /refs/remotes/<remote name>/<branch_name>
114+
return tokens[2]
115+
116+
@property
117+
@require_remote_ref_path
118+
def remote_head(self):
119+
""":return: Name of the remote head itself, i.e. master.
120+
:note: The returned name is usually not qualified enough to uniquely identify
121+
a branch"""
122+
tokens = self.path.split('/')
123+
return '/'.join(tokens[3:])
124+
125+
#} END remote interface

git/refs/remote.py

+1-19
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class RemoteReference(Head):
1212
"""Represents a reference pointing to a remote head."""
1313
__slots__ = tuple()
1414

15-
_common_path_default = "refs/remotes"
15+
_common_path_default = Head._remote_common_path_default
1616

1717

1818
@classmethod
@@ -24,24 +24,6 @@ def iter_items(cls, repo, common_path = None, remote=None):
2424
# END handle remote constraint
2525
return super(RemoteReference, cls).iter_items(repo, common_path)
2626

27-
@property
28-
def remote_name(self):
29-
"""
30-
:return:
31-
Name of the remote we are a reference of, such as 'origin' for a reference
32-
named 'origin/master'"""
33-
tokens = self.path.split('/')
34-
# /refs/remotes/<remote name>/<branch_name>
35-
return tokens[2]
36-
37-
@property
38-
def remote_head(self):
39-
""":return: Name of the remote head itself, i.e. master.
40-
:note: The returned name is usually not qualified enough to uniquely identify
41-
a branch"""
42-
tokens = self.path.split('/')
43-
return '/'.join(tokens[3:])
44-
4527
@classmethod
4628
def create(cls, *args, **kwargs):
4729
"""Used to disable this method"""

git/refs/symbolic.py

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class SymbolicReference(object):
3636
_resolve_ref_on_create = False
3737
_points_to_commits_only = True
3838
_common_path_default = ""
39+
_remote_common_path_default = "refs/remotes"
3940
_id_attribute_ = "name"
4041

4142
re_hexsha_only = re.compile('^[0-9A-Fa-f]{40}$')

git/test/db/cmd/test_base.py

+61-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from git.db.complex import CmdCompatibilityGitDB
1212
from git.db.cmd.base import *
1313

14+
from git.refs import TagReference, Reference, RemoteReference
15+
1416
class TestBase(RepoBase):
1517
RepoCls = CmdCompatibilityGitDB
1618

@@ -28,5 +30,62 @@ def test_basics(self):
2830
self.failUnlessRaises(BadObject, gdb.partial_to_complete_sha_hex, invalid_rev)
2931

3032
def test_fetch_info(self):
31-
self.failUnlessRaises(ValueError, CmdFetchInfo._from_line, self.rorepo, "nonsense", '')
32-
self.failUnlessRaises(ValueError, CmdFetchInfo._from_line, self.rorepo, "? [up to date] 0.1.7RC -> origin/0.1.7RC", '')
33+
self.failUnlessRaises(ValueError, CmdCmdFetchInfo._from_line, self.rorepo, "nonsense", '')
34+
self.failUnlessRaises(ValueError, CmdCmdFetchInfo._from_line, self.rorepo, "? [up to date] 0.1.7RC -> origin/0.1.7RC", '')
35+
36+
37+
def test_fetch_info(self):
38+
# assure we can handle remote-tracking branches
39+
fetch_info_line_fmt = "c437ee5deb8d00cf02f03720693e4c802e99f390 not-for-merge %s '0.3' of git://github.com/gitpython-developers/GitPython"
40+
remote_info_line_fmt = "* [new branch] nomatter -> %s"
41+
fi = CmdFetchInfo._from_line(self.rorepo,
42+
remote_info_line_fmt % "local/master",
43+
fetch_info_line_fmt % 'remote-tracking branch')
44+
45+
# we wouldn't be here if it wouldn't have worked
46+
47+
# handles non-default refspecs: One can specify a different path in refs/remotes
48+
# or a special path just in refs/something for instance
49+
50+
fi = CmdFetchInfo._from_line(self.rorepo,
51+
remote_info_line_fmt % "subdir/tagname",
52+
fetch_info_line_fmt % 'tag')
53+
54+
assert isinstance(fi.ref, TagReference)
55+
assert fi.ref.path.startswith('refs/tags')
56+
57+
# it could be in a remote direcftory though
58+
fi = CmdFetchInfo._from_line(self.rorepo,
59+
remote_info_line_fmt % "remotename/tags/tagname",
60+
fetch_info_line_fmt % 'tag')
61+
62+
assert isinstance(fi.ref, TagReference)
63+
assert fi.ref.path.startswith('refs/remotes/')
64+
65+
# it can also be anywhere !
66+
tag_path = "refs/something/remotename/tags/tagname"
67+
fi = CmdFetchInfo._from_line(self.rorepo,
68+
remote_info_line_fmt % tag_path,
69+
fetch_info_line_fmt % 'tag')
70+
71+
assert isinstance(fi.ref, TagReference)
72+
assert fi.ref.path == tag_path
73+
74+
# branches default to refs/remotes
75+
fi = CmdFetchInfo._from_line(self.rorepo,
76+
remote_info_line_fmt % "remotename/branch",
77+
fetch_info_line_fmt % 'branch')
78+
79+
assert isinstance(fi.ref, RemoteReference)
80+
assert fi.ref.remote_name == 'remotename'
81+
82+
# but you can force it anywhere, in which case we only have a references
83+
fi = CmdFetchInfo._from_line(self.rorepo,
84+
remote_info_line_fmt % "refs/something/branch",
85+
fetch_info_line_fmt % 'branch')
86+
87+
assert type(fi.ref) is Reference
88+
assert fi.ref.path == "refs/something/branch"
89+
90+
91+

git/test/refs/test_refs.py

+5
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ def test_from_path(self):
2929
assert isinstance(instance, ref_type)
3030
# END for each name
3131
# END for each type
32+
33+
# invalid path
34+
self.failUnlessRaises(ValueError, TagReference, self.rorepo, "refs/invalid/tag")
35+
# works without path check
36+
TagReference(self.rorepo, "refs/invalid/tag", check_path=False)
3237

3338
def test_tag_base(self):
3439
tag_object_refs = list()

git/test/test_remote.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ def get_info(res, remote, name):
256256
shutil.rmtree(other_repo_dir)
257257
# END test and cleanup
258258

259-
def _test_push_and_pull(self,remote, rw_repo, remote_repo):
259+
def _verify_push_and_pull(self,remote, rw_repo, remote_repo):
260260
# push our changes
261261
lhead = rw_repo.head
262262
lindex = rw_repo.index
@@ -407,7 +407,7 @@ def test_base(self, rw_repo, remote_repo):
407407
# END for each rename ( back to prev_name )
408408

409409
# PUSH/PULL TESTING
410-
self._test_push_and_pull(remote, rw_repo, remote_repo)
410+
self._verify_push_and_pull(remote, rw_repo, remote_repo)
411411

412412
# FETCH TESTING
413413
# Only for remotes - local cases are the same or less complicated

0 commit comments

Comments
 (0)