Skip to content

Commit ad906ae

Browse files
committed
checkout dirs + misc cleanups
1 parent bd6761c commit ad906ae

File tree

4 files changed

+85
-35
lines changed

4 files changed

+85
-35
lines changed

gitless/cli/gl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def print_help(parser):
5858
# https://stackoverflow.com/questions/20094215/argparse-subparser-monolithic-help-output
5959
# retrieve subparsers from parser
6060
subparsers_actions = [
61-
action for action in parser._actions
61+
action for action in parser._actions
6262
if isinstance(action, argparse._SubParsersAction)]
6363
# there will probably only be one subparser_action,
6464
# but better safe than sorry

gitless/cli/gl_checkout.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def parser(subparsers, repo):
2323
dest='cp', default='HEAD')
2424
checkout_parser.add_argument(
2525
'files', nargs='+', help='the file(s) to checkout',
26-
action=helpers.PathProcessor, repo=repo)
26+
action=helpers.PathProcessor, repo=repo, recursive=False)
2727
checkout_parser.set_defaults(func=main)
2828

2929

@@ -51,6 +51,13 @@ def main(args, repo):
5151
pprint.ok(
5252
'File {0} checked out successfully to its state at {1}'.format(
5353
fp, cp))
54+
except core.PathIsDirectoryError:
55+
commit = repo.revparse_single(cp)
56+
for fp in curr_b.get_paths(fp, commit):
57+
curr_b.checkout_file(fp, commit)
58+
pprint.ok(
59+
'File {0} checked out successfully to its state at {1}'.format(
60+
fp, cp))
5461
except KeyError:
5562
pprint.err('Checkout aborted')
5663
pprint.err('There\'s no file {0} at {1}'.format(fp, cp))

gitless/cli/helpers.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,11 @@ class PathProcessor(argparse.Action):
105105

106106
def __init__(
107107
self, option_strings, dest, repo=None, skip_dir_test=None,
108-
skip_dir_cb=None, **kwargs):
108+
skip_dir_cb=None, recursive=True, **kwargs):
109109
self.repo = repo
110110
self.skip_dir_test = skip_dir_test
111111
self.skip_dir_cb = skip_dir_cb
112+
self.recursive = recursive
112113
super(PathProcessor, self).__init__(option_strings, dest, **kwargs)
113114

114115
def __call__(self, parser, namespace, paths, option_string=None):
@@ -117,7 +118,7 @@ def __call__(self, parser, namespace, paths, option_string=None):
117118
def process_paths():
118119
for path in paths:
119120
path = os.path.abspath(path)
120-
if os.path.isdir(path):
121+
if self.recursive and os.path.isdir(path):
121122
for curr_dir, dirs, fps in os.walk(path, topdown=True):
122123
if curr_dir.startswith(repo_dir):
123124
dirs[:] = []
@@ -134,6 +135,8 @@ def process_paths():
134135
else:
135136
if not path.startswith(repo_dir):
136137
yield os.path.relpath(path, root)
138+
else:
139+
yield path
137140

138141
setattr(namespace, self.dest, process_paths())
139142

gitless/core.py

Lines changed: 71 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from __future__ import unicode_literals
99

1010
import collections
11+
import errno
1112
import io
1213
try:
1314
from itertools import izip as zip
@@ -47,6 +48,8 @@ class BranchIsCurrentError(GlError): pass
4748

4849
class 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

Comments
 (0)