jolt 0.9.76__py3-none-any.whl → 0.9.429__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of jolt might be problematic. Click here for more details.

Files changed (201) hide show
  1. jolt/__init__.py +88 -7
  2. jolt/__main__.py +9 -1
  3. jolt/bin/fstree-darwin-x86_64 +0 -0
  4. jolt/bin/fstree-linux-x86_64 +0 -0
  5. jolt/cache.py +839 -367
  6. jolt/chroot.py +156 -0
  7. jolt/cli.py +362 -143
  8. jolt/common_pb2.py +63 -0
  9. jolt/common_pb2_grpc.py +4 -0
  10. jolt/config.py +99 -42
  11. jolt/error.py +19 -4
  12. jolt/expires.py +2 -2
  13. jolt/filesystem.py +8 -6
  14. jolt/graph.py +705 -117
  15. jolt/hooks.py +63 -1
  16. jolt/influence.py +129 -6
  17. jolt/loader.py +369 -121
  18. jolt/log.py +225 -63
  19. jolt/manifest.py +28 -38
  20. jolt/options.py +35 -10
  21. jolt/pkgs/abseil.py +42 -0
  22. jolt/pkgs/asio.py +25 -0
  23. jolt/pkgs/autoconf.py +41 -0
  24. jolt/pkgs/automake.py +41 -0
  25. jolt/pkgs/b2.py +31 -0
  26. jolt/pkgs/boost.py +111 -0
  27. jolt/pkgs/boringssl.py +32 -0
  28. jolt/pkgs/busybox.py +39 -0
  29. jolt/pkgs/bzip2.py +43 -0
  30. jolt/pkgs/cares.py +29 -0
  31. jolt/pkgs/catch2.py +36 -0
  32. jolt/pkgs/cbindgen.py +17 -0
  33. jolt/pkgs/cista.py +19 -0
  34. jolt/pkgs/clang.py +44 -0
  35. jolt/pkgs/cli11.py +23 -0
  36. jolt/pkgs/cmake.py +48 -0
  37. jolt/pkgs/cpython.py +196 -0
  38. jolt/pkgs/crun.py +29 -0
  39. jolt/pkgs/curl.py +38 -0
  40. jolt/pkgs/dbus.py +18 -0
  41. jolt/pkgs/double_conversion.py +24 -0
  42. jolt/pkgs/fastfloat.py +21 -0
  43. jolt/pkgs/ffmpeg.py +28 -0
  44. jolt/pkgs/flatbuffers.py +29 -0
  45. jolt/pkgs/fmt.py +27 -0
  46. jolt/pkgs/fstree.py +20 -0
  47. jolt/pkgs/gflags.py +18 -0
  48. jolt/pkgs/glib.py +18 -0
  49. jolt/pkgs/glog.py +25 -0
  50. jolt/pkgs/glslang.py +21 -0
  51. jolt/pkgs/golang.py +16 -11
  52. jolt/pkgs/googlebenchmark.py +18 -0
  53. jolt/pkgs/googletest.py +46 -0
  54. jolt/pkgs/gperf.py +15 -0
  55. jolt/pkgs/grpc.py +73 -0
  56. jolt/pkgs/hdf5.py +19 -0
  57. jolt/pkgs/help2man.py +14 -0
  58. jolt/pkgs/inja.py +28 -0
  59. jolt/pkgs/jsoncpp.py +31 -0
  60. jolt/pkgs/libarchive.py +43 -0
  61. jolt/pkgs/libcap.py +44 -0
  62. jolt/pkgs/libdrm.py +44 -0
  63. jolt/pkgs/libedit.py +42 -0
  64. jolt/pkgs/libevent.py +31 -0
  65. jolt/pkgs/libexpat.py +27 -0
  66. jolt/pkgs/libfastjson.py +21 -0
  67. jolt/pkgs/libffi.py +16 -0
  68. jolt/pkgs/libglvnd.py +30 -0
  69. jolt/pkgs/libogg.py +28 -0
  70. jolt/pkgs/libpciaccess.py +18 -0
  71. jolt/pkgs/libseccomp.py +21 -0
  72. jolt/pkgs/libtirpc.py +24 -0
  73. jolt/pkgs/libtool.py +42 -0
  74. jolt/pkgs/libunwind.py +35 -0
  75. jolt/pkgs/libva.py +18 -0
  76. jolt/pkgs/libvorbis.py +33 -0
  77. jolt/pkgs/libxml2.py +35 -0
  78. jolt/pkgs/libxslt.py +17 -0
  79. jolt/pkgs/libyajl.py +16 -0
  80. jolt/pkgs/llvm.py +81 -0
  81. jolt/pkgs/lua.py +54 -0
  82. jolt/pkgs/lz4.py +26 -0
  83. jolt/pkgs/m4.py +14 -0
  84. jolt/pkgs/make.py +17 -0
  85. jolt/pkgs/mesa.py +81 -0
  86. jolt/pkgs/meson.py +17 -0
  87. jolt/pkgs/mstch.py +28 -0
  88. jolt/pkgs/mysql.py +60 -0
  89. jolt/pkgs/nasm.py +49 -0
  90. jolt/pkgs/ncurses.py +30 -0
  91. jolt/pkgs/ng_log.py +25 -0
  92. jolt/pkgs/ninja.py +45 -0
  93. jolt/pkgs/nlohmann_json.py +25 -0
  94. jolt/pkgs/nodejs.py +19 -11
  95. jolt/pkgs/opencv.py +24 -0
  96. jolt/pkgs/openjdk.py +26 -0
  97. jolt/pkgs/openssl.py +103 -0
  98. jolt/pkgs/paho.py +76 -0
  99. jolt/pkgs/patchelf.py +16 -0
  100. jolt/pkgs/perl.py +42 -0
  101. jolt/pkgs/pkgconfig.py +64 -0
  102. jolt/pkgs/poco.py +39 -0
  103. jolt/pkgs/protobuf.py +77 -0
  104. jolt/pkgs/pugixml.py +27 -0
  105. jolt/pkgs/python.py +19 -0
  106. jolt/pkgs/qt.py +35 -0
  107. jolt/pkgs/rapidjson.py +26 -0
  108. jolt/pkgs/rapidyaml.py +28 -0
  109. jolt/pkgs/re2.py +30 -0
  110. jolt/pkgs/re2c.py +17 -0
  111. jolt/pkgs/readline.py +15 -0
  112. jolt/pkgs/rust.py +41 -0
  113. jolt/pkgs/sdl.py +28 -0
  114. jolt/pkgs/simdjson.py +27 -0
  115. jolt/pkgs/soci.py +46 -0
  116. jolt/pkgs/spdlog.py +29 -0
  117. jolt/pkgs/spirv_llvm.py +21 -0
  118. jolt/pkgs/spirv_tools.py +24 -0
  119. jolt/pkgs/sqlite.py +83 -0
  120. jolt/pkgs/ssl.py +12 -0
  121. jolt/pkgs/texinfo.py +15 -0
  122. jolt/pkgs/tomlplusplus.py +22 -0
  123. jolt/pkgs/wayland.py +26 -0
  124. jolt/pkgs/x11.py +58 -0
  125. jolt/pkgs/xerces_c.py +20 -0
  126. jolt/pkgs/xorg.py +360 -0
  127. jolt/pkgs/xz.py +29 -0
  128. jolt/pkgs/yamlcpp.py +30 -0
  129. jolt/pkgs/zeromq.py +47 -0
  130. jolt/pkgs/zlib.py +69 -0
  131. jolt/pkgs/zstd.py +33 -0
  132. jolt/plugins/alias.py +3 -0
  133. jolt/plugins/allure.py +5 -2
  134. jolt/plugins/autotools.py +66 -0
  135. jolt/plugins/cache.py +133 -0
  136. jolt/plugins/cmake.py +74 -6
  137. jolt/plugins/conan.py +238 -0
  138. jolt/plugins/cxx.py +698 -0
  139. jolt/plugins/cxxinfo.py +7 -0
  140. jolt/plugins/dashboard.py +1 -1
  141. jolt/plugins/docker.py +91 -23
  142. jolt/plugins/email.py +5 -2
  143. jolt/plugins/email.xslt +144 -101
  144. jolt/plugins/environ.py +11 -0
  145. jolt/plugins/fetch.py +141 -0
  146. jolt/plugins/gdb.py +44 -21
  147. jolt/plugins/gerrit.py +1 -14
  148. jolt/plugins/git.py +316 -101
  149. jolt/plugins/googletest.py +522 -1
  150. jolt/plugins/http.py +36 -38
  151. jolt/plugins/libtool.py +63 -0
  152. jolt/plugins/linux.py +990 -0
  153. jolt/plugins/logstash.py +4 -4
  154. jolt/plugins/meson.py +61 -0
  155. jolt/plugins/ninja-compdb.py +107 -31
  156. jolt/plugins/ninja.py +929 -134
  157. jolt/plugins/paths.py +11 -1
  158. jolt/plugins/pkgconfig.py +219 -0
  159. jolt/plugins/podman.py +148 -91
  160. jolt/plugins/python.py +137 -0
  161. jolt/plugins/remote_execution/__init__.py +0 -0
  162. jolt/plugins/remote_execution/administration_pb2.py +46 -0
  163. jolt/plugins/remote_execution/administration_pb2_grpc.py +170 -0
  164. jolt/plugins/remote_execution/log_pb2.py +32 -0
  165. jolt/plugins/remote_execution/log_pb2_grpc.py +68 -0
  166. jolt/plugins/remote_execution/scheduler_pb2.py +41 -0
  167. jolt/plugins/remote_execution/scheduler_pb2_grpc.py +141 -0
  168. jolt/plugins/remote_execution/worker_pb2.py +38 -0
  169. jolt/plugins/remote_execution/worker_pb2_grpc.py +112 -0
  170. jolt/plugins/report.py +12 -2
  171. jolt/plugins/rust.py +25 -0
  172. jolt/plugins/scheduler.py +710 -0
  173. jolt/plugins/selfdeploy/setup.py +9 -4
  174. jolt/plugins/selfdeploy.py +138 -88
  175. jolt/plugins/strings.py +35 -22
  176. jolt/plugins/symlinks.py +26 -11
  177. jolt/plugins/telemetry.py +5 -2
  178. jolt/plugins/timeline.py +13 -3
  179. jolt/plugins/volume.py +46 -48
  180. jolt/scheduler.py +591 -191
  181. jolt/tasks.py +1783 -245
  182. jolt/templates/export.sh.template +12 -6
  183. jolt/templates/timeline.html.template +44 -47
  184. jolt/timer.py +22 -0
  185. jolt/tools.py +749 -302
  186. jolt/utils.py +245 -18
  187. jolt/version.py +1 -1
  188. jolt/version_utils.py +2 -2
  189. jolt/xmldom.py +12 -2
  190. {jolt-0.9.76.dist-info → jolt-0.9.429.dist-info}/METADATA +98 -38
  191. jolt-0.9.429.dist-info/RECORD +207 -0
  192. {jolt-0.9.76.dist-info → jolt-0.9.429.dist-info}/WHEEL +1 -1
  193. jolt/plugins/amqp.py +0 -834
  194. jolt/plugins/debian.py +0 -338
  195. jolt/plugins/ftp.py +0 -181
  196. jolt/plugins/ninja-cache.py +0 -64
  197. jolt/plugins/ninjacli.py +0 -271
  198. jolt/plugins/repo.py +0 -253
  199. jolt-0.9.76.dist-info/RECORD +0 -79
  200. {jolt-0.9.76.dist-info → jolt-0.9.429.dist-info}/entry_points.txt +0 -0
  201. {jolt-0.9.76.dist-info → jolt-0.9.429.dist-info}/top_level.txt +0 -0
jolt/plugins/git.py CHANGED
@@ -1,11 +1,13 @@
1
1
  import os
2
2
  import pygit2
3
3
  import re
4
+ from threading import RLock
5
+ import urllib.parse
4
6
 
5
- from jolt.tasks import BooleanParameter, Export, Parameter, TaskRegistry, WorkspaceResource
7
+ from jolt.tasks import BooleanParameter, Export, IntParameter, Parameter, TaskRegistry, WorkspaceResource
6
8
  from jolt.influence import FileInfluence, HashInfluenceRegistry
7
9
  from jolt.tools import Tools
8
- from jolt.loader import JoltLoader
10
+ from jolt.loader import JoltLoader, workspace_locked
9
11
  from jolt import config
10
12
  from jolt import filesystem as fs
11
13
  from jolt import log
@@ -19,54 +21,119 @@ from jolt.error import raise_task_error_if
19
21
  log.verbose("[Git] Loaded")
20
22
 
21
23
 
24
+ def locked(func):
25
+ def _f(self, *args, **kwargs):
26
+ with self._lock:
27
+ return func(self, *args, **kwargs)
28
+ return _f
29
+
30
+
22
31
  class GitRepository(object):
23
32
  def __init__(self, url, path, relpath, refspecs=None):
24
33
  self.path = path
25
34
  self.relpath = relpath
35
+ self.gitpath = None
26
36
  self.tools = Tools()
27
37
  self.url = url
28
38
  self.default_refspecs = [
29
39
  '+refs/heads/*:refs/remotes/origin/*',
30
- '+refs/tags/*:refs/remotes/origin/*',
40
+ '+refs/tags/*:refs/tags/*',
31
41
  ]
32
42
  self.refspecs = refspecs or []
33
43
  self._tree_hash = {}
34
44
  self._original_head = True
45
+ self._last_rev = None
35
46
  self._init_repo()
36
47
 
37
48
  def _init_repo(self):
38
- self.repository = pygit2.Repository(self.path) if os.path.exists(self._git_folder()) else None
49
+ gitpath = os.path.join(self.path, ".git")
50
+ if os.path.isdir(os.path.join(self.path, ".git")):
51
+ self.gitpath = gitpath
52
+ self.repository = pygit2.Repository(self.gitpath)
53
+ elif os.path.exists(gitpath):
54
+ with self.tools.cwd(self.path):
55
+ path = self.tools.run("git rev-parse --git-dir", output=False)
56
+ self.gitpath = os.path.join(self.path, path)
57
+ self.repository = pygit2.Repository(self.gitpath)
58
+ else:
59
+ self.repository = None
39
60
 
40
61
  @utils.cached.instance
41
62
  def _git_folder(self):
42
- return fs.path.join(self.path, ".git")
63
+ return self.gitpath
43
64
 
44
65
  @utils.cached.instance
45
66
  def _git_index(self):
46
- return fs.path.join(self.path, ".git", "index")
67
+ return fs.path.join(self.gitpath, "index")
47
68
 
48
69
  @utils.cached.instance
49
70
  def _git_jolt_index(self):
50
- return fs.path.join(self.path, ".git", "jolt-index")
71
+ return fs.path.join(self.gitpath, "jolt-index")
51
72
 
52
73
  def is_cloned(self):
53
- return fs.path.exists(self._git_folder())
74
+ return self.gitpath is not None
54
75
 
55
76
  def is_indexed(self):
56
- return fs.path.exists(self._git_index())
77
+ return self.is_cloned() and fs.path.exists(self._git_index())
57
78
 
58
- def clone(self):
79
+ def clone(self, submodules=False):
59
80
  log.info("Cloning into {0}", self.path)
81
+
82
+ # Get reference repository path
83
+ refroot = config.get("git", "reference", None)
84
+ refpath = None
85
+ if refroot:
86
+ # Append <host>/<path> to the reference root
87
+ url = urllib.parse.urlparse(str(self.url))
88
+ refpath = fs.path.join(refroot, url.hostname, url.path.lstrip("/"))
89
+ refpath = fs.path.abspath(refpath)
90
+
91
+ # If the directory exists, initialize the repository instead of cloning
60
92
  if fs.path.exists(self.path):
61
93
  with self.tools.cwd(self.path):
62
- self.tools.run("git init && git remote add origin {} && git fetch",
63
- self.url, output_on_error=True)
94
+ self.tools.run("git init", output_on_error=True)
95
+
96
+ # Set the reference repository if available
97
+ if refpath:
98
+ # Check if the reference repository is a git repository
99
+ objpath = os.path.join(refpath, ".git", "objects")
100
+ objpath_bare = os.path.join(refpath, "objects")
101
+ if os.path.isdir(objpath):
102
+ refpath = objpath
103
+ elif os.path.isdir(objpath_bare):
104
+ refpath = objpath_bare
105
+ else:
106
+ refpath = None
107
+
108
+ if refpath:
109
+ self.tools.mkdir(".git/objects/info")
110
+ self.tools.write_file(".git/objects/info/alternates", refpath)
111
+
112
+ utils.call_and_catch(self.tools.run, "git remote remove origin", output=False)
113
+ self.tools.run("git remote add origin {}", self.url, output_on_error=True)
114
+ self._fetch_origin(submodules=submodules)
115
+ self.tools.run("git checkout -f FETCH_HEAD", output_on_error=True)
64
116
  else:
65
- self.tools.run("git clone {0} {1}", self.url, self.path, output_on_error=True)
66
- raise_error_if(
67
- not fs.path.exists(self._git_folder()),
68
- "git: failed to clone repository '{0}'", self.relpath)
117
+ # Get configurable extra clone options
118
+ extra_clone_options = config.get("git", "clone_options", "")
119
+
120
+ if refpath and os.path.isdir(refpath):
121
+ self.tools.run("git clone --reference-if-able {0} {1} {2} {3}", refpath, extra_clone_options, self.url, self.path, output_on_error=True)
122
+ else:
123
+ self.tools.run("git clone {0} {1} {2}", extra_clone_options, self.url, self.path, output_on_error=True)
124
+
69
125
  self._init_repo()
126
+ raise_error_if(
127
+ self.repository is None,
128
+ "Failed to clone repository '{0}'", self.relpath)
129
+
130
+ @utils.retried.on_exception(JoltCommandError, pattern="Command failed: git fetch", count=6, backoff=[2, 5, 10, 15, 20, 30])
131
+ def _fetch_origin(self, submodules=False):
132
+ # Get configurable extra clone options
133
+ extra_fetch_options = config.get("git", "fetch_options", "")
134
+
135
+ with self.tools.cwd(self.path):
136
+ self.tools.run("git fetch {0} {1} origin", "", extra_fetch_options, output_on_error=True)
70
137
 
71
138
  @utils.cached.instance
72
139
  def diff_unchecked(self):
@@ -84,18 +151,18 @@ class GitRepository(object):
84
151
 
85
152
  def diff(self):
86
153
  diff = self.diff_unchecked()
87
- dlim = config.getsize("git", "maxdiffsize", "1M")
154
+ dlim = config.getsize("git", "maxdiffsize", "1 MiB")
88
155
  raise_error_if(
89
156
  len(diff) > dlim,
90
- "git patch for '{}' exceeds configured size limit of {} bytes - actual size {}"
91
- .format(self.path, dlim, len(diff)))
157
+ "Repository '{}' has uncommitted changes. Size of patch exceeds configured transfer limit ({} > {} bytes)."
158
+ .format(self.relpath, len(diff), dlim))
92
159
  return diff
93
160
 
94
161
  def patch(self, patch):
95
162
  if not patch:
96
163
  return
97
- with self.tools.cwd(self.path), self.tools.tmpdir("git") as t:
98
- patchfile = fs.path.join(t.path, "jolt.diff")
164
+ with self.tools.cwd(self.path), self.tools.tmpdir("git") as tmp:
165
+ patchfile = fs.path.join(tmp, "jolt.diff")
99
166
  with open(patchfile, "wb") as f:
100
167
  f.write(patch.encode())
101
168
  log.info("Applying patch to {0}", self.path)
@@ -122,17 +189,18 @@ class GitRepository(object):
122
189
  try:
123
190
  commit = self.repository.revparse_single(rev)
124
191
  except KeyError:
125
- self.fetch()
192
+ self.fetch(commit=rev)
126
193
  try:
127
194
  commit = self.repository.revparse_single(rev)
128
195
  except Exception:
129
- raise_error("invalid git reference: {}", rev)
196
+ raise_error("Invalid git reference: {}", rev)
130
197
  try:
131
198
  return str(commit.id)
132
199
  except Exception:
133
200
  return str(commit)
134
201
 
135
202
  @utils.cached.instance
203
+ @workspace_locked
136
204
  def write_tree(self):
137
205
  tools = Tools()
138
206
  with tools.cwd(self._git_folder()):
@@ -143,30 +211,30 @@ class GitRepository(object):
143
211
  output_on_error=True)
144
212
  return tree
145
213
 
146
- def tree_hash(self, sha=None, path="/"):
147
- # When sha is None, the caller want the tree hash of the repository's
214
+ def tree_hash(self, rev=None, path="/"):
215
+ # When rev is None, the caller want the tree hash of the repository's
148
216
  # current workspace state. If no checkout has been made, that would be the
149
217
  # tree that was written upon initialization of the repository as it
150
218
  # includes any uncommitted changes. If a checkout has been made since
151
219
  # the repo was initialized, make this an explicit request for the current
152
220
  # head - there can be no local changes.
153
- if sha is None:
221
+ if rev is None:
154
222
  if self.is_original_head():
155
223
  tree = self.repository.get(self.write_tree())
156
224
  else:
157
- sha = self.head()
225
+ rev = self.head()
158
226
 
159
227
  path = fs.path.normpath(path)
160
228
  full_path = fs.path.join(self.path, path) if path != "/" else self.path
161
229
 
162
230
  # Lookup tree hash value in cache
163
- value = self._tree_hash.get((full_path, sha))
231
+ value = self._tree_hash.get((full_path, rev))
164
232
  if value is not None:
165
233
  return value
166
234
 
167
- # Translate explicit sha to tree
168
- if sha is not None:
169
- commit = self.rev_parse(sha)
235
+ # Translate explicit rev to tree
236
+ if rev is not None:
237
+ commit = self.rev_parse(rev)
170
238
  obj = self.repository.get(commit)
171
239
  try:
172
240
  tree = obj.tree
@@ -178,7 +246,7 @@ class GitRepository(object):
178
246
  tree = tree[fs.as_posix(path)]
179
247
 
180
248
  # Update tree hash cache
181
- self._tree_hash[(full_path, sha)] = value = tree.id
249
+ self._tree_hash[(full_path, rev)] = value = tree.id
182
250
 
183
251
  return value
184
252
 
@@ -190,24 +258,49 @@ class GitRepository(object):
190
258
  with self.tools.cwd(self.path):
191
259
  return self.tools.run("git reset --hard", output_on_error=True)
192
260
 
193
- def fetch(self):
261
+ @utils.retried.on_exception(JoltCommandError, pattern="Command failed: git fetch", count=6, backoff=[2, 5, 10, 15, 20, 30])
262
+ def fetch(self, commit=None):
263
+ if commit and not self.is_valid_sha(commit):
264
+ commit = None
265
+
266
+ # Get configurable extra clone options
267
+ extra_fetch_options = config.get("git", "fetch_options", "")
268
+
194
269
  refspec = " ".join(self.default_refspecs + self.refspecs)
195
270
  with self.tools.cwd(self.path):
196
- log.info("Fetching {0} from {1}", refspec or 'commits', self.url)
197
- self.tools.run("git fetch {url} {refspec}",
198
- url=self.url,
199
- refspec=refspec or '',
200
- output_on_error=True)
201
-
202
- def checkout(self, rev):
203
- log.info("Checking out {0} in {1}", rev, self.path)
271
+ log.info("Fetching {0} from {1}", commit or refspec or 'commits', self.url)
272
+ self.tools.run(
273
+ "git fetch --force --prune {extra_fetch_options} {url} {what}",
274
+ extra_fetch_options=extra_fetch_options,
275
+ url=self.url,
276
+ what=commit or refspec or '',
277
+ output_on_error=True)
278
+
279
+ def checkout(self, rev, commit=None, submodules=False):
280
+ if rev == self._last_rev:
281
+ log.debug("Checkout skipped, already @ {}", rev)
282
+ return False
283
+ log.verbose("Checking out {0} in {1}", rev, self.path)
204
284
  with self.tools.cwd(self.path):
205
285
  try:
206
- return self.tools.run("git checkout -f {rev}", rev=rev, output=False)
286
+ self.tools.run("git checkout -f {rev}", rev=rev, output=False)
287
+ if submodules:
288
+ self._update_submodules()
207
289
  except Exception:
208
- self.fetch()
209
- return self.tools.run("git checkout -f {rev}", rev=rev, output_on_error=True)
290
+ self.fetch(commit=commit)
291
+ try:
292
+ self.tools.run("git checkout -f {rev}", rev=rev, output_on_error=True)
293
+ if submodules:
294
+ self._update_submodules()
295
+ except Exception:
296
+ raise_error("Commit does not exist in remote for '{}': {}", self.relpath, rev)
210
297
  self._original_head = False
298
+ self._last_rev = rev
299
+ return True
300
+
301
+ def _update_submodules(self):
302
+ with self.tools.cwd(self.path):
303
+ self.tools.run("git submodule update --init --recursive", output_on_error=True)
211
304
 
212
305
 
213
306
  _gits = {}
@@ -217,9 +310,9 @@ def new_git(url, path, relpath, refspecs=None):
217
310
  refspecs = utils.as_list(refspecs or [])
218
311
  try:
219
312
  git = _gits[path]
220
- raise_error_if(git.url != url, "multiple git repositories required at {}", relpath)
313
+ raise_error_if(git.url != url, "Multiple git repositories required at {}", relpath)
221
314
  raise_error_if(git.refspecs != refspecs,
222
- "conflicting refspecs detected for git repository at {}", relpath)
315
+ "Conflicting refspecs detected for git repository at {}", relpath)
223
316
  return git
224
317
  except Exception:
225
318
  git = _gits[path] = GitRepository(url, path, relpath, refspecs)
@@ -230,7 +323,7 @@ class GitInfluenceProvider(FileInfluence):
230
323
  name = "Git"
231
324
 
232
325
  def __init__(self, path):
233
- super(GitInfluenceProvider, self).__init__(path)
326
+ super().__init__(path)
234
327
  self.path = path.rstrip(fs.sep)
235
328
  self.name = GitInfluenceProvider.name
236
329
 
@@ -241,11 +334,11 @@ class GitInfluenceProvider(FileInfluence):
241
334
  def _find_dotgit(self, path):
242
335
  ppath = None
243
336
  while path != ppath:
244
- if fs.path.isdir(fs.path.join(path, ".git")):
337
+ if fs.path.exists(fs.path.join(path, ".git")):
245
338
  return path
246
339
  ppath = path
247
340
  path = fs.path.dirname(path)
248
- raise_error("no git repository found at '{}'", self.path)
341
+ raise_error("No git repository found at '{}'", self.path)
249
342
 
250
343
  @utils.cached.instance
251
344
  def get_influence(self, task):
@@ -260,8 +353,9 @@ class GitInfluenceProvider(FileInfluence):
260
353
  return "{0}/{1}: N/A".format(git_rel, relpath)
261
354
  try:
262
355
  git = new_git(None, git_abs, git_rel)
263
-
264
356
  return "{0}/{1}: {2}".format(git_rel, relpath, git.tree_hash(path=relpath or "/"))
357
+ except KeyError:
358
+ return "{0}/{1}: N/A".format(git_rel, relpath)
265
359
  except JoltCommandError as e:
266
360
  stderr = "\n".join(e.stderr)
267
361
  if "exists on disk, but not in" in stderr:
@@ -292,23 +386,101 @@ def influence(path, git_cls=GitInfluenceProvider):
292
386
  return _decorate
293
387
 
294
388
 
295
- class GitSrc(WorkspaceResource, FileInfluence):
296
- """ Clones a Git repo.
389
+ class ErrorDict(dict):
390
+ """ A dict that raises an error if the value of a key is None. """
391
+
392
+ def __init__(self, repo):
393
+ self.repo = repo
394
+
395
+ def __getitem__(self, key):
396
+ value = super().__getitem__(key)
397
+ raise_task_error_if(value is None, self.repo, "Git repository '{0}' referenced in influence collection before being cloned/checked out. Assign hash=true to the git requirement.", key)
398
+ return value
399
+
400
+
401
+ class Git(WorkspaceResource, FileInfluence):
402
+ """
403
+ Resource that clones and monitors a Git repo.
404
+
405
+ By default, the repo is cloned into a build directory named after
406
+ the resource. The 'path' parameter can be used to specify a different
407
+ location relative to the workspace root.
408
+
409
+ The path of the cloned repo is made available to consuming tasks
410
+ through their 'git' attribute. The 'git' attribute is a dictionary
411
+ where the key is the name of the git repository and the value is
412
+ the relative path to the repository from the consuming task's
413
+ workspace.
414
+
415
+ The resource influences the hash of consuming tasks, causing tasks
416
+ to be re-executed if the cloned repo is modified.
417
+
418
+ The plugin must be loaded before it can be used. This is done by
419
+ importing the module, or by adding the following line to the
420
+ configuration file:
421
+
422
+ .. code-block:: ini
423
+
424
+ [git]
425
+
426
+ Example:
427
+
428
+ .. code-block:: python
429
+
430
+ from jolt.plugins import git
431
+
432
+ class Example(Task):
433
+ requires = ["git:url=https://github.com/user/repo.git"]
434
+
435
+ def run(self, deps, tools):
436
+ self.info("The git repo is located at: {git[repo]}")
437
+ with tools.cwd(self.git["repo"]):
438
+ tools.run("make")
439
+
297
440
  """
441
+ name = "git"
298
442
 
299
- name = "git-src"
300
443
  url = Parameter(help="URL to the git repo to be cloned. Required.")
301
- sha = Parameter(required=False, help="Specific commit or tag to be checked out. Optional.")
444
+ """ URL to the git repo to be cloned. Required. """
445
+
446
+ rev = Parameter(required=False, help="Specific commit or tag to be checked out. Optional.")
447
+ """ Specific commit or tag to be checked out. Optional. """
448
+
449
+ hash = BooleanParameter(required=False, help="Let repo content influence the hash of consuming tasks.")
450
+ """ Let repo content influence the hash of consuming tasks. Default ``True``. Optional. """
451
+
302
452
  path = Parameter(required=False, help="Local path where the repository should be cloned.")
303
- defer = BooleanParameter(False, help="Defer cloning until a consumer task must be built.")
453
+ """ Alternative path where the repository should be cloned. Relative to ``joltdir``. Optional. """
454
+
455
+ submodules = BooleanParameter(default=False, help="Initialize and update git submodules after cloning.")
456
+ """ Initialize and update git submodules after cloning. Default ``False``. Optional. """
457
+
458
+ clean = BooleanParameter(default=False, help="Clean repository after it has been used")
459
+ """ Removes untracked files from the repository. """
460
+
304
461
  _revision = Export(value=lambda t: t._export_revision())
462
+ """ To worker exported value of the revision to be checked out. """
463
+
305
464
  _diff = Export(value=lambda t: t.git.diff(), encoded=True)
465
+ """ To worker exported value of the diff of the repo. """
306
466
 
307
467
  def __init__(self, *args, **kwargs):
308
- super(GitSrc, self).__init__(*args, **kwargs)
468
+ super().__init__(*args, **kwargs)
469
+ self._lock = RLock()
309
470
  self.joltdir = JoltLoader.get().joltdir
310
- self.relpath = str(self.path) or self._get_name()
311
- self.abspath = fs.path.join(self.joltdir, self.relpath)
471
+
472
+ # Set the path to the repo
473
+ if self.path.is_unset():
474
+ self.abspath = self.tools.builddir(utils.canonical(self.short_qualified_name), incremental="always", unique=False)
475
+ self.relpath = fs.path.relpath(self.abspath, self.tools.wsroot)
476
+ else:
477
+ self.abspath = fs.path.join(self.joltdir, str(self.path) or self._get_name())
478
+ self.relpath = fs.path.relpath(self.abspath, self.tools.wsroot)
479
+
480
+ self.abspath = fs.path.normpath(self.abspath)
481
+ self.relpath = fs.path.normpath(self.relpath)
482
+
483
+ # Create the git repository
312
484
  self.refspecs = kwargs.get("refspecs", [])
313
485
  self.git = new_git(self.url, self.abspath, self.relpath, self.refspecs)
314
486
 
@@ -319,83 +491,126 @@ class GitSrc(WorkspaceResource, FileInfluence):
319
491
  return name
320
492
 
321
493
  def _export_revision(self):
322
- return self.sha.value or self.git.head()
494
+ return self.rev.value or self.git.head()
323
495
 
324
496
  def _get_revision(self):
325
497
  if self._revision.is_imported:
326
498
  return self._revision.value
327
- if not self.sha.is_unset():
328
- return self.sha.get_value()
499
+ if not self.rev.is_unset():
500
+ return self.rev.get_value()
329
501
  return None
330
502
 
331
- def acquire(self, **kwargs):
503
+ def _assign_git(self, task, none=False):
504
+ if not hasattr(task, "git"):
505
+ task.git = ErrorDict(self)
506
+ if none:
507
+ # None means the git repo is not cloned or checked out
508
+ # and should not be included in the git dictionary
509
+ # of the consuming task yet. If the consuming task
510
+ # requires the git repo for its influence collection,
511
+ # the dict will raise an error. The solution is to
512
+ # assign hash=true to the git requirement which
513
+ # will cause the git repo to be cloned and checked out
514
+ # before the influence collection is performed.
515
+ task.git[self._get_name()] = None
516
+ else:
517
+ # Assign the git repo to the consuming task.
518
+ # The git repo is cloned and checked out before
519
+ # any influence collection is performed.
520
+ task.git[self._get_name()] = fs.path.relpath(self.abspath, task.joltdir)
521
+
522
+ def acquire(self, artifact, deps, tools, owner):
332
523
  self._acquire_ws()
524
+ self._assign_git(owner)
525
+ artifact.worktree = fs.path.relpath(self.abspath, owner.joltdir)
526
+
527
+ def release(self, artifact, deps, tools, owner):
528
+ if self.clean:
529
+ self.git.clean()
530
+ self.git.reset()
531
+
532
+ def prepare_ws_for(self, task):
533
+ """ Prepare the workspace for the task.
534
+
535
+ :param task: The task to prepare the workspace for.
536
+ """
537
+ if not self._must_influence():
538
+ # The content of the git repo is not required to influence the hash of the
539
+ # consumer task. The repo is therefore not cloned or checked out
540
+ # until the consumer is executed. Raise an error if the git repo
541
+ # is required for the influence collection of the consumer task.
542
+ self._assign_git(task, none=True)
543
+ return
544
+ # The content of the git repo is required to influence the hash of the consumer task.
545
+ self._assign_git(task)
333
546
 
334
- def acquire_ws(self):
335
- if self.defer is None or self.defer.is_false:
547
+ def acquire_ws(self, force=False):
548
+ """ Clone and/or checkout the git repo if required """
549
+ if force or self._must_influence() or self._revision.is_imported:
336
550
  self._acquire_ws()
337
551
 
552
+ @locked
338
553
  def _acquire_ws(self):
554
+ commit = None
339
555
  if not self.git.is_cloned():
340
- self.git.clone()
556
+ self.git.clone(submodules=bool(self.submodules))
341
557
  if not self._revision.is_imported:
342
558
  self.git.diff_unchecked()
559
+ else:
560
+ commit = self._revision.value
343
561
  rev = self._get_revision()
344
562
  if rev is not None:
345
563
  raise_task_error_if(
346
- not self._revision.is_imported and not self.sha.is_unset() and self.git.diff(), self,
347
- "explicit sha requested but git repo '{0}' has local changes", self.git.relpath)
564
+ not self._revision.is_imported and not self.rev.is_unset() and self.git.diff(), self,
565
+ "Explicit revision requested but git repo '{0}' has local changes, refusing checkout", self.git.relpath)
348
566
  # Should be safe to do this now
349
567
  rev = self.git.rev_parse(rev)
350
568
  if not self.git.is_head(rev) or self._revision.is_imported:
351
- self.git.checkout(rev)
352
- self.git.clean()
353
- self.git.patch(self._diff.value)
354
-
355
- def get_influence(self, task):
356
- return None
357
-
358
- def is_influenced_by(self, task, path):
359
- return fs.is_relative_to(path, self.abspath) and self.sha.is_set()
569
+ if self.git.checkout(rev, commit=commit, submodules=bool(self.submodules)):
570
+ self.git.clean()
571
+ self.git.patch(self._diff.value)
360
572
 
573
+ def _must_influence(self):
574
+ """ Check if the git repo must influence the hash of the consumer task."""
361
575
 
362
- TaskRegistry.get().add_task_class(GitSrc)
576
+ # If the hash parameter is set, honor it
577
+ if self.hash.is_set():
578
+ return self.hash
363
579
 
580
+ # If the revision parameter is not set, the git repo must influence the hash
581
+ if self.rev.is_unset():
582
+ return True
364
583
 
365
- class Git(GitSrc):
366
- """ Clones a Git repo.
584
+ # If the revision parameter is set, no influence is needed since the
585
+ # revision is fixed and repository content will not change.
586
+ return False
367
587
 
368
- Also influences the hash of consuming tasks, causing tasks to
369
- be re-executed if the cloned repo is modified.
370
-
371
- """
372
- name = "git"
373
- url = Parameter(help="URL to the git repo to be cloned. Required.")
374
- sha = Parameter(required=False, help="Specific commit or tag to be checked out. Optional.")
375
- path = Parameter(required=False, help="Local path where the repository should be cloned.")
376
- defer = None
377
- _revision = Export(value=lambda t: t._export_revision())
378
- _diff = Export(value=lambda t: t.git.diff(), encoded=True)
588
+ def is_influenced_by(self, task, path):
589
+ influencing = self._must_influence() or self.rev.is_set()
590
+ return influencing and fs.is_relative_to(path, self.abspath)
379
591
 
380
- def __init__(self, *args, **kwargs):
381
- super(Git, self).__init__(*args, **kwargs)
382
- self.influence.append(self)
592
+ def _influence(self):
593
+ influence = super()._influence()
594
+ return influence + [self] if self._must_influence() else influence
383
595
 
596
+ @locked
384
597
  @utils.cached.instance
385
598
  def get_influence(self, task):
386
599
  if not self.git.is_cloned():
387
- self.git.clone()
600
+ self.git.clone(submodules=bool(self.submodules))
388
601
  if not self._revision.is_imported:
389
602
  self.git.diff_unchecked()
390
603
  rev = self._get_revision()
391
- if rev is not None:
392
- return self.git.tree_hash(rev)
393
- return "{0}: {1}".format(
394
- self.git.relpath,
395
- self.git.tree_hash())
396
604
 
397
- def is_influenced_by(self, task, path):
398
- return path.startswith(self.abspath + fs.sep)
605
+ try:
606
+ if rev is not None:
607
+ th = self.git.tree_hash(rev)
608
+ else:
609
+ th = self.git.tree_hash()
610
+ except KeyError:
611
+ th = "N/A"
612
+
613
+ return "{0}: {1}".format(self.git.relpath, th)
399
614
 
400
615
 
401
616
  TaskRegistry.get().add_task_class(Git)