1 | 1 | import os |
|
2 | 1 | import logging |
|
3 | 1 | import warnings |
|
4 | |||
5 | 1 | import yorm |
|
6 | 1 | from yorm.types import String, NullableString, List, AttributeDictionary |
|
7 | |||
8 | 1 | from .. import common, exceptions, shell, git |
|
9 | |||
10 | |||
11 | 1 | log = logging.getLogger(__name__) |
|
12 | |||
13 | |||
14 | 1 | @yorm.attr(name=String) |
|
15 | 1 | @yorm.attr(repo=String) |
|
16 | 1 | @yorm.attr(sparse_paths=List.of_type(String)) |
|
17 | 1 | @yorm.attr(rev=String) |
|
18 | 1 | @yorm.attr(link=NullableString) |
|
19 | 1 | @yorm.attr(scripts=List.of_type(String)) |
|
20 | class Source(AttributeDictionary): |
||
21 | """A dictionary of `git` and `ln` arguments.""" |
||
22 | 1 | ||
23 | 1 | DIRTY = '<dirty>' |
|
24 | UNKNOWN = '<unknown>' |
||
25 | 1 | ||
26 | 1 | def __init__(self, repo, name=None, rev='master', link=None, scripts=None, sparse_paths=None): |
|
27 | 1 | super().__init__() |
|
28 | 1 | self.repo = repo |
|
29 | 1 | self.name = self._infer_name(repo) if name is None else name |
|
30 | 1 | self.rev = rev |
|
31 | 1 | self.link = link |
|
32 | self.scripts = scripts or [] |
||
33 | 1 | self.sparse_paths = sparse_paths or [] |
|
34 | 1 | ||
35 | 1 | for key in ['name', 'repo', 'rev']: |
|
36 | 1 | if not self[key]: |
|
37 | msg = "'{}' required for {}".format(key, repr(self)) |
||
38 | 1 | raise exceptions.InvalidConfig(msg) |
|
39 | 1 | ||
40 | def __repr__(self): |
||
41 | 1 | return "<source {}>".format(self) |
|
42 | 1 | ||
43 | 1 | def __str__(self): |
|
44 | 1 | pattern = "'{r}' @ '{v}' in '{d}'" |
|
45 | 1 | if self.link: |
|
46 | pattern += " <- '{s}'" |
||
47 | 1 | return pattern.format(r=self.repo, v=self.rev, d=self.name, s=self.link) |
|
48 | 1 | ||
49 | def __eq__(self, other): |
||
50 | 1 | return self.name == other.name |
|
51 | 1 | ||
52 | def __ne__(self, other): |
||
53 | 1 | return self.name != other.name |
|
54 | 1 | ||
55 | def __lt__(self, other): |
||
56 | 1 | return self.name < other.name |
|
57 | |||
58 | 1 | def update_files(self, force=False, fetch=False, clean=True): |
|
59 | """Ensure the source matches the specified revision.""" |
||
60 | log.info("Updating source files...") |
||
61 | 1 | ||
62 | 1 | # Clone the repository if needed |
|
63 | if not os.path.exists(self.name): |
||
64 | git.clone(self.repo, self.name, sparse_paths=self.sparse_paths, rev=self.rev) |
||
65 | 1 | ||
66 | 1 | # Enter the working tree |
|
67 | 1 | shell.cd(self.name) |
|
68 | if not git.valid(): |
||
69 | raise self._invalid_repository |
||
70 | 1 | ||
71 | 1 | # Check for uncommitted changes |
|
72 | 1 | if not force: |
|
73 | log.debug("Confirming there are no uncommitted changes...") |
||
74 | if git.changes(include_untracked=clean): |
||
75 | msg = "Uncommitted changes in {}".format(os.getcwd()) |
||
76 | raise exceptions.UncommittedChanges(msg) |
||
77 | 1 | ||
78 | # Fetch the desired revision |
||
79 | if fetch or self.rev not in (git.get_branch(), |
||
80 | 1 | git.get_hash(), |
|
81 | git.get_tag()): |
||
82 | git.fetch(self.repo, self.rev) |
||
83 | 1 | ||
84 | # Update the working tree to the desired revision |
||
85 | 1 | git.update(self.rev, fetch=fetch, clean=clean) |
|
86 | |||
87 | 1 | def create_link(self, root, force=False): |
|
88 | 1 | """Create a link from the target name to the current directory.""" |
|
89 | if not self.link: |
||
90 | 1 | return |
|
91 | |||
92 | 1 | log.info("Creating a symbolic link...") |
|
93 | |||
94 | if os.name == 'nt': |
||
95 | warnings.warn("Symbolic links are not supported on Windows") |
||
96 | 1 | return |
|
97 | 1 | ||
98 | target = os.path.join(root, self.link) |
||
99 | 1 | source = os.path.relpath(os.getcwd(), os.path.dirname(target)) |
|
100 | 1 | ||
101 | 1 | if os.path.islink(target): |
|
102 | 1 | os.remove(target) |
|
103 | 1 | elif os.path.exists(target): |
|
104 | if force: |
||
105 | 1 | shell.rm(target) |
|
106 | 1 | else: |
|
107 | msg = "Preexisting link location at {}".format(target) |
||
108 | 1 | raise exceptions.UncommittedChanges(msg) |
|
109 | |||
110 | 1 | shell.ln(source, target) |
|
111 | 1 | ||
112 | def run_scripts(self, force=False): |
||
113 | log.info("Running install scripts...") |
||
114 | 1 | ||
115 | 1 | # Enter the working tree |
|
116 | shell.cd(self.name) |
||
117 | if not git.valid(): |
||
118 | raise self._invalid_repository |
||
119 | 1 | ||
120 | 1 | # Check for scripts |
|
121 | 1 | if not self.scripts: |
|
122 | 1 | common.show("(no scripts to run)", color='shell_info') |
|
123 | common.newline() |
||
124 | return |
||
125 | 1 | ||
126 | 1 | # Run all scripts |
|
127 | 1 | for script in self.scripts: |
|
128 | 1 | try: |
|
129 | 1 | lines = shell.call(script, _shell=True) |
|
130 | 1 | except exceptions.ShellError as exc: |
|
131 | 1 | common.show(*exc.output, color='shell_error') |
|
132 | 1 | cmd = exc.program |
|
133 | if force: |
||
134 | 1 | log.debug("Ignored error from call to '%s'", cmd) |
|
135 | 1 | else: |
|
136 | msg = "Command '{}' failed in {}".format(cmd, os.getcwd()) |
||
137 | raise exceptions.ScriptFailure(msg) |
||
138 | 1 | else: |
|
139 | common.show(*lines, color='shell_output') |
||
140 | 1 | common.newline() |
|
141 | |||
142 | 1 | def identify(self, allow_dirty=True, allow_missing=True): |
|
143 | """Get the path and current repository URL and hash.""" |
||
144 | 1 | if os.path.isdir(self.name): |
|
0 ignored issues
–
show
unused-code
introduced
by
Loading history...
|
|||
145 | 1 | ||
146 | 1 | shell.cd(self.name) |
|
147 | if not git.valid(): |
||
148 | 1 | raise self._invalid_repository |
|
149 | 1 | ||
150 | 1 | path = os.getcwd() |
|
151 | 1 | url = git.get_url() |
|
152 | 1 | if git.changes(display_status=not allow_dirty, _show=True): |
|
0 ignored issues
–
show
|
|||
153 | 1 | if not allow_dirty: |
|
154 | msg = "Uncommitted changes in {}".format(os.getcwd()) |
||
155 | raise exceptions.UncommittedChanges(msg) |
||
156 | |||
157 | common.show(self.DIRTY, color='git_dirty', log=False) |
||
158 | common.newline() |
||
159 | 1 | return path, url, self.DIRTY |
|
160 | 1 | else: |
|
161 | 1 | rev = git.get_hash(_show=True) |
|
162 | 1 | common.show(rev, color='git_rev', log=False) |
|
163 | common.newline() |
||
164 | 1 | return path, url, rev |
|
165 | |||
166 | 1 | elif allow_missing: |
|
167 | |||
168 | return os.getcwd(), '<missing>', self.UNKNOWN |
||
169 | |||
170 | 1 | else: |
|
171 | |||
172 | 1 | raise self._invalid_repository |
|
173 | |||
174 | 1 | def lock(self, rev=None): |
|
175 | 1 | """Return a locked version of the current source.""" |
|
176 | 1 | if rev is None: |
|
177 | _, _, rev = self.identify(allow_dirty=False, allow_missing=False) |
||
178 | 1 | source = self.__class__(self.repo, self.name, rev, |
|
179 | self.link, self.scripts) |
||
180 | 1 | return source |
|
181 | |||
182 | 1 | @property |
|
183 | 1 | def _invalid_repository(self): |
|
184 | 1 | path = os.path.join(os.getcwd(), self.name) |
|
185 | msg = "Not a valid repository: {}".format(path) |
||
186 | 1 | return exceptions.InvalidRepository(msg) |
|
187 | |||
188 | 1 | @staticmethod |
|
189 | 1 | def _infer_name(repo): |
|
190 | 1 | filename = repo.split('/')[-1] |
|
191 | name = filename.split('.')[0] |
||
192 | return name |
||
193 |