-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathutils.py
190 lines (153 loc) · 5.95 KB
/
utils.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
from pathlib import Path
import click
from git import Repo, InvalidGitRepositoryError, Git
from config import config
from exceptions import UnknownProject
class PairedProject(object):
"""
Represents a pair of repositories that are linked together
"""
def __init__(self, parent_dir: Path, child_dir: Path, working_from_parent: bool):
"""
:param parent_dir: the project repo
:param child_dir: the et repo where we keep tracked files
"""
self.parent_dir = parent_dir
self.child_dir = child_dir
self.working_from_parent = working_from_parent
@classmethod
def from_path(cls, input_path: Path) -> 'PairedProject':
"""
Identify a ProjectPair given a path.
An error will be raised if the input_path is not in a valid
project directory
"""
try:
repo = Repo(input_path, search_parent_directories=True)
except InvalidGitRepositoryError:
raise
working_repo = Path(repo.working_dir)
if config.ET_HOME in working_repo.parents:
# We are in a child directory
parent_dir = (working_repo / config.PARENT_SYMLINK_NAME).resolve()
return cls(parent_dir=parent_dir, child_dir=working_repo, working_from_parent=False)
else:
# We are in a parent directory
child_dir = find_child_dir(working_repo)
return cls(parent_dir=working_repo, child_dir=child_dir, working_from_parent=True)
@property
def parent_repo(self):
return Repo(str(self.parent_dir))
@property
def child_repo(self):
return Repo(str(self.child_dir))
class PairedObject(object):
"""
Represents a filesystem object (file, directory, other?)
whose path is under a ProjectPair.
"""
def __init__(self, project: PairedProject, relative_path: Path):
self.project = project
self.relative_path = relative_path
@classmethod
def from_path(cls, input_path: Path) -> 'PairedObject':
"""
Constructor method that creates a ProjectPair instance from the path too.
"""
project = PairedProject.from_path(input_path)
relative_path = get_relative_path(project, input_path.absolute())
return cls(project, relative_path=relative_path)
@property
def working_from_parent(self) -> bool:
"""
:return: Whether not the original referenced file is in the parent dir
"""
return self.project.working_from_parent
@property
def child_path(self) -> Path:
return self.project.child_dir / self.relative_path
@property
def parent_path(self) -> Path:
return self.project.parent_dir / self.relative_path
@property
def is_linked(self) -> bool:
"""
A path is considered linked if the path
meets the following qualifications:
- child path exists
- child path is not a symlink
- parent path exists
- parent path is a symlink to the child
"""
return self.parent_path.is_symlink() \
and (not self.child_path.is_symlink()) \
and self.child_path.samefile(self.parent_path)
def link(self):
"""
1. Moves the file/directory from the parent dir into
the same relative location in the child dir.
2. Symlink the file/directory back to its original location
"""
self.parent_path.replace(self.child_path)
self.parent_path.symlink_to(self.child_path)
def unlink(self):
"""
Completely reverts changes made by PairedPath.link.
"""
# No special case needed for file vs dir types, since this is always a symlink
self.parent_path.unlink()
self.child_path.replace(self.parent_path)
def get_relative_path(project: PairedProject, input_path: Path) -> Path:
"""
Finds the relative path for a path under either of the project dirs
:param project: A PairedProject instance
:param input_path: A path that should be under one of the projects two directories.
:raises ValueError: if input_path is not found under one of the project directories
:return:
"""
abs_path = input_path.absolute()
try:
return abs_path.relative_to(project.parent_dir)
except ValueError:
pass
try:
return abs_path.relative_to(project.child_dir)
except ValueError:
pass
raise ValueError(f'"{input_path}" not found in "{project.parent_dir}" '
f'or "{project.child_dir}"')
def find_child_dir(parent_dir: Path) -> Path:
"""
Browse the ET_HOME directory to find the directory
that symlinks to the parent dir
"""
for child_dir in config.ET_HOME.iterdir():
parent_path = (child_dir / config.PARENT_SYMLINK_NAME).resolve()
if parent_dir == parent_path:
return child_dir
# User needs to run `et init` on the parent directory.
raise UnknownProject('Could not find an associated project '
'for the current directory')
def get_current_project():
try:
return PairedProject.from_path(Path('.'))
except InvalidGitRepositoryError:
raise click.BadParameter('Not in a git repository')
except UnknownProject as e:
raise click.BadParameter(e)
class PathType(click.Path):
"""
Click argument parser for returning filepath locations as Path objects
"""
def convert(self, value, param, ctx):
return Path(super().convert(value, param, ctx))
# TODO: I forget what I was going to use this for... should this be a method on PairedObject?
def file_is_git_tracked(repo: Repo, file: Path) -> bool:
"""
:param repo: any git Repo instance
:param file: path must be relative to the repo
:return: Whether git is tracking the file in question
"""
# TODO: is there a more canonical way of doing this?
# Git knows about this file
return bool(Git(repo.working_dir).ls_files(file))