88from __future__ import unicode_literals
99
1010import collections
11+ import errno
1112import io
1213try :
1314 from itertools import izip as zip
@@ -47,6 +48,8 @@ class BranchIsCurrentError(GlError): pass
4748
4849class ApplyFailedError (GlError ): pass
4950
51+ class PathIsDirectoryError (ValueError ): pass
52+
5053
5154# File status
5255
@@ -165,7 +168,7 @@ def _ref_target(self, ref):
165168 return self .git_repo .lookup_reference (ref ).target
166169
167170
168- # Tag related methods
171+ # Tag- related methods
169172
170173 def create_tag (self , name , commit ):
171174 tagger = self .git_repo .default_signature
@@ -200,7 +203,7 @@ def listall_tags(self):
200203 yield ref [10 :]
201204
202205
203- # Branch related methods
206+ # Branch- related methods
204207
205208 @property
206209 def current_branch (self ):
@@ -450,7 +453,7 @@ def __init__(self, git_remote, gl_repo):
450453 self .url = self .git_remote .url
451454
452455
453- # Branch related methods
456+ # Branch- related methods
454457
455458 def create_branch (self , name , head ):
456459 if self .lookup_branch (name ):
@@ -489,7 +492,7 @@ def lookup_branch(self, branch_name):
489492 return RemoteBranch (git_branch , self .gl_repo )
490493
491494
492- # Tag related methods
495+ # Tag- related methods
493496
494497 def create_tag (self , name , commit ):
495498 if self .lookup_tag (name ):
@@ -774,11 +777,9 @@ def status_file(self, path):
774777 return self ._status_file (path )[0 ]
775778
776779 def _status_file (self , path ):
777- assert not os .path .isabs (path )
778-
779- git_path = path if sys .platform != 'win32' else path .replace ('\\ ' , '/' )
780+ _check_path_is_repo_relative (path )
780781
781- git_st = self .gl_repo .git_repo .status_file (git_path )
782+ git_st = self .gl_repo .git_repo .status_file (_get_git_path ( path ) )
782783 root = self .gl_repo .root
783784 cmd_out = stdout (git ('ls-files' , '-v' , '--full-name' , path , _cwd = root ))
784785 is_au = cmd_out and cmd_out [0 ] == 'h'
@@ -791,17 +792,17 @@ def _status_file(self, path):
791792 return f_st , git_st , is_au
792793
793794 def path_is_ignored (self , path ):
794- assert not os . path . isabs (path )
795+ _check_path_is_repo_relative (path )
795796
796- git_path = path if sys . platform != 'win32' else path . replace ( ' \\ ' , '/' )
797+ git_path = _get_git_path ( path )
797798 return self .gl_repo .git_repo .path_is_ignored (git_path )
798799
799800
800- # File related methods
801+ # File- related methods
801802
802803 def track_file (self , path ):
803804 """Start tracking changes to path."""
804- assert not os . path . isabs (path )
805+ _check_path_is_repo_relative (path )
805806
806807 gl_st , git_st , is_au = self ._status_file (path )
807808
@@ -818,7 +819,7 @@ def track_file(self, path):
818819 # (ii) an assumed unchanged file => unmark it.
819820 if git_st == pygit2 .GIT_STATUS_WT_NEW : # Case (i)
820821 with self ._index as index :
821- git_path = path if sys . platform != 'win32' else path . replace ( ' \\ ' , '/' )
822+ git_path = _get_git_path ( path )
822823 index .add (git_path )
823824 elif is_au : # Case (ii)
824825 git ('update-index' , '--no-assume-unchanged' , path ,
@@ -828,7 +829,7 @@ def track_file(self, path):
828829
829830 def untrack_file (self , path ):
830831 """Stop tracking changes to path."""
831- assert not os . path . isabs (path )
832+ _check_path_is_repo_relative (path )
832833
833834 gl_st , git_st , is_au = self ._status_file (path )
834835
@@ -849,7 +850,7 @@ def untrack_file(self, path):
849850 # unchanged.
850851 if git_st == pygit2 .GIT_STATUS_INDEX_NEW : # Case (i)
851852 with self ._index as index :
852- git_path = path if sys . platform != 'win32' else path . replace ( ' \\ ' , '/' )
853+ git_path = _get_git_path ( path )
853854 index .remove (git_path )
854855 elif not is_au : # Case (ii)
855856 git ('update-index' , '--assume-unchanged' , path ,
@@ -859,35 +860,66 @@ def untrack_file(self, path):
859860
860861 def resolve_file (self , path ):
861862 """Mark the given path as resolved."""
862- assert not os . path . isabs (path )
863+ _check_path_is_repo_relative (path )
863864
864865 gl_st , _ , _ = self ._status_file (path )
865866 if not gl_st .in_conflict :
866867 raise ValueError ('File {0} has no conflicts' .format (path ))
867868
868869 with self ._index as index :
869- git_path = path if sys . platform != 'win32' else path . replace ( ' \\ ' , '/' )
870+ git_path = _get_git_path ( path )
870871 index .add (git_path )
871872
872873 def checkout_file (self , path , commit ):
873874 """Checkouts the given path at the given commit."""
874- assert not os .path .isabs (path )
875+ _check_path_is_repo_relative (path )
876+
877+ git_path = _get_git_path (path )
878+ o = self .gl_repo .git_repo [commit .tree [git_path ].id ]
879+ assert o .type != pygit2 .GIT_OBJ_COMMIT
880+ assert o .type != pygit2 .GIT_OBJ_TAG
881+
882+ if o .type == pygit2 .GIT_OBJ_BLOB :
883+ full_path = os .path .join (self .gl_repo .root , path )
884+ dirname = os .path .dirname (full_path )
885+ if not os .path .exists (dirname ):
886+ try :
887+ os .makedirs (dirname )
888+ except OSError as exc : # guard against race condition
889+ if exc .errno != errno .EEXIST :
890+ raise
891+ with io .open (full_path , mode = 'wb' ) as dst :
892+ dst .write (o .data )
893+
894+ elif o .type == pygit2 .GIT_OBJ_TREE :
895+ raise PathIsDirectoryError (
896+ 'Path {0} at {1} is a directory and not a file' .format (
897+ path , commit .id ))
898+ else :
899+ raise Exception ('Unexpected object type {0}' .format (o .type ))
875900
876- git_path = path if sys .platform != 'win32' else path .replace ('\\ ' , '/' )
877- data = self .gl_repo .git_repo [commit .tree [git_path ].id ].data
878- with io .open (os .path .join (self .gl_repo .root , path ), mode = 'wb' ) as dst :
879- dst .write (data )
901+ def get_paths (self , path , commit ):
902+ """Return a generator of all filepaths under path at commit."""
903+ _check_path_is_repo_relative (path )
880904
881- # So as to not get confused with the status of the file we also add it
882- with self ._index as index :
883- index .add (git_path )
905+ git_path = _get_git_path (path )
906+ tree = self .gl_repo .git_repo [commit .tree [git_path ].id ]
907+ assert tree .type == pygit2 .GIT_OBJ_TREE
908+
909+ for tree_entry in tree :
910+ tree_entry_path = os .path .join (path , tree_entry .name )
911+ if tree_entry .type == 'tree' :
912+ for fp in self .get_paths (tree_entry_path , commit ):
913+ yield fp
914+ else :
915+ yield tree_entry_path
884916
885917 def diff_file (self , path ):
886918 """Diff the working version of path with its committed version."""
887- assert not os . path . isabs (path )
919+ _check_path_is_repo_relative (path )
888920
889921 git_repo = self .gl_repo .git_repo
890- git_path = path if sys . platform != 'win32' else path . replace ( ' \\ ' , '/' )
922+ git_path = _get_git_path ( path )
891923 try :
892924 blob_at_head = git_repo [git_repo .head .peel ().tree [git_path ].id ]
893925 except KeyError : # no blob at head
@@ -904,7 +936,7 @@ def diff_file(self, path):
904936 return blob_at_head .diff (wt_blob , 0 , git_path , git_path )
905937
906938
907- # Merge related methods
939+ # Merge- related methods
908940
909941 def merge (self , src , op_cb = None ):
910942 """Merges the divergent changes of the src branch onto this one."""
@@ -949,7 +981,7 @@ def abort_merge(self):
949981 git .merge (abort = True )
950982
951983
952- # Fuse related methods
984+ # Fuse- related methods
953985
954986 @property
955987 def _fuse_commits_fp (self ):
@@ -1162,7 +1194,7 @@ def update():
11621194 """Add/remove files to the index."""
11631195 for f in files :
11641196 assert not os .path .isabs (f )
1165- git_f = f if sys . platform != 'win32' else f . replace ( ' \\ ' , '/' )
1197+ git_f = _get_git_path ( f )
11661198 if not os .path .exists (os .path .join (self .gl_repo .root , f )):
11671199 index .remove (git_f )
11681200 elif f not in partials :
@@ -1173,7 +1205,7 @@ def update():
11731205 with index :
11741206 update ()
11751207 for f in partials :
1176- git_f = f if sys . platform != 'win32' else f . replace ( ' \\ ' , '/' )
1208+ git_f = _get_git_path ( f )
11771209 partial_entries [f ] = index ._git_index [git_f ]
11781210
11791211 # To create the commit tree with only the changes to the given files we:
@@ -1302,3 +1334,11 @@ def walker(git_repo, target, reverse):
13021334 if reverse :
13031335 flags = flags | pygit2 .GIT_SORT_REVERSE
13041336 return git_repo .walk (target , flags )
1337+
1338+ def _get_git_path (path ):
1339+ return path if sys .platform != 'win32' else path .replace ('\\ ' , '/' )
1340+
1341+ def _check_path_is_repo_relative (path ):
1342+ if os .path .isabs (path ):
1343+ raise ValueError (
1344+ "path {0} is absolute but should be relative to the repo root" .format (path ))
0 commit comments