fluidattacks_core_git 12.0.0__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.
@@ -0,0 +1,382 @@
1
+ import asyncio
2
+ import logging
3
+ import re
4
+ from datetime import UTC, datetime
5
+ from pathlib import Path
6
+ from subprocess import ( # nosec
7
+ SubprocessError,
8
+ )
9
+
10
+ from git.exc import GitError
11
+ from git.repo import Repo
12
+
13
+ from .classes import (
14
+ CommitInfo,
15
+ InvalidParameter,
16
+ RebaseResult,
17
+ )
18
+ from .clone import (
19
+ call_codecommit_clone,
20
+ https_clone,
21
+ ssh_clone,
22
+ )
23
+ from .download_repo import (
24
+ download_repo_from_s3,
25
+ remove_symlinks_in_directory,
26
+ reset_repo,
27
+ )
28
+ from .https_utils import (
29
+ https_ls_remote,
30
+ )
31
+ from .remote import (
32
+ ls_remote,
33
+ )
34
+ from .show_file import (
35
+ show_file_at_ref,
36
+ show_file_at_ref_https,
37
+ show_file_at_ref_ssh,
38
+ )
39
+ from .ssh_utils import (
40
+ SSHHostValidationError,
41
+ ssh_ls_remote,
42
+ )
43
+ from .upload_repo import (
44
+ MultipartCallbacks,
45
+ UploadPartResult,
46
+ upload_repo_to_s3,
47
+ )
48
+ from .utils import run_git
49
+
50
+ LOGGER = logging.getLogger(__name__)
51
+
52
+ __all__ = [
53
+ "CommitInfo",
54
+ "InvalidParameter",
55
+ "MultipartCallbacks",
56
+ "RebaseResult",
57
+ "SSHHostValidationError",
58
+ "UploadPartResult",
59
+ "clone",
60
+ "disable_quotepath",
61
+ "download_repo_from_s3",
62
+ "get_head_commit",
63
+ "get_last_commit_info_new",
64
+ "get_line_author",
65
+ "get_modified_filenames",
66
+ "https_clone",
67
+ "https_ls_remote",
68
+ "is_commit_in_branch",
69
+ "ls_remote",
70
+ "rebase",
71
+ "remove_symlinks_in_directory",
72
+ "reset_repo",
73
+ "show_file_at_ref",
74
+ "show_file_at_ref_https",
75
+ "show_file_at_ref_ssh",
76
+ "ssh_clone",
77
+ "ssh_ls_remote",
78
+ "upload_repo_to_s3",
79
+ ]
80
+
81
+
82
+ async def disable_quotepath(git_path: str) -> None:
83
+ try:
84
+ _, stderr, returncode = await run_git(
85
+ f"--git-dir={git_path}",
86
+ "config",
87
+ "core.quotepath",
88
+ "off",
89
+ )
90
+ except asyncio.exceptions.TimeoutError:
91
+ LOGGER.exception(
92
+ "Timed out disabling git quotepath",
93
+ extra={"extra": {"git_path": git_path}},
94
+ )
95
+ return
96
+
97
+ if returncode != 0:
98
+ LOGGER.error(
99
+ "Failed to disable git quotepath",
100
+ extra={"extra": {"git_path": git_path, "stderr": stderr.decode()}},
101
+ )
102
+
103
+
104
+ async def get_last_commit_info_new(
105
+ repo_path: str,
106
+ filename: str,
107
+ ) -> CommitInfo | None:
108
+ proc = await asyncio.create_subprocess_exec(
109
+ "git",
110
+ "log",
111
+ "--max-count",
112
+ "1",
113
+ "--format=%H%n%ce%n%cI",
114
+ "--",
115
+ filename,
116
+ stderr=asyncio.subprocess.PIPE,
117
+ stdout=asyncio.subprocess.PIPE,
118
+ cwd=repo_path,
119
+ )
120
+ stdout, stderr = await proc.communicate()
121
+ git_log = stdout.decode().splitlines()
122
+
123
+ if stderr or proc.returncode != 0 or not git_log:
124
+ return None
125
+
126
+ return CommitInfo(
127
+ hash=git_log[0],
128
+ author=git_log[1],
129
+ modified_date=datetime.fromisoformat(git_log[2]),
130
+ )
131
+
132
+
133
+ async def get_line_author(
134
+ repo_path: str,
135
+ filename: str,
136
+ line: int,
137
+ rev: str = "HEAD",
138
+ ) -> CommitInfo | None:
139
+ try:
140
+ proc = await asyncio.create_subprocess_exec(
141
+ "git",
142
+ "blame",
143
+ "-L",
144
+ f"{line!s},+1",
145
+ "-l",
146
+ "-p",
147
+ "-M",
148
+ "-C",
149
+ "-C",
150
+ rev,
151
+ "--",
152
+ filename,
153
+ stderr=asyncio.subprocess.PIPE,
154
+ stdout=asyncio.subprocess.PIPE,
155
+ cwd=repo_path,
156
+ )
157
+ stdout, stderr = await proc.communicate()
158
+ cmd_output = stdout.decode("utf-8", "ignore")
159
+ except (
160
+ OSError,
161
+ SubprocessError,
162
+ UnicodeDecodeError,
163
+ ):
164
+ LOGGER.exception(
165
+ "An error occurred while getting the line author",
166
+ extra={
167
+ "extra": {
168
+ "repo_path": repo_path,
169
+ "filename": filename,
170
+ "line": str(line),
171
+ },
172
+ },
173
+ )
174
+
175
+ return None
176
+
177
+ if stderr or proc.returncode != 0 or not cmd_output:
178
+ return None
179
+
180
+ commit_hash = cmd_output.splitlines()[0].split(" ")[0]
181
+ mail_search = re.search(r"author-mail <(.*?)>", cmd_output)
182
+ author_email = mail_search.group(1) if mail_search else ""
183
+ time_search = re.search(r"committer-time (\d*)", cmd_output)
184
+ committer_time = time_search.group(1) if time_search else "0"
185
+ commit_date = datetime.fromtimestamp(float(committer_time), UTC)
186
+
187
+ return CommitInfo(
188
+ hash=commit_hash,
189
+ author=author_email,
190
+ modified_date=commit_date,
191
+ )
192
+
193
+
194
+ async def get_modified_filenames(repo_path: str, commit_sha: str) -> list[str]:
195
+ proc = await asyncio.create_subprocess_exec(
196
+ "git",
197
+ "diff",
198
+ "--name-only",
199
+ f"{commit_sha}..HEAD",
200
+ stderr=asyncio.subprocess.PIPE,
201
+ stdout=asyncio.subprocess.PIPE,
202
+ cwd=repo_path,
203
+ )
204
+ stdout, stderr = await proc.communicate()
205
+ if stderr or proc.returncode != 0:
206
+ return []
207
+
208
+ return stdout.decode().splitlines()
209
+
210
+
211
+ async def is_commit_in_branch(
212
+ repo_path: str,
213
+ branch: str,
214
+ commit_sha: str,
215
+ ) -> bool:
216
+ proc = await asyncio.create_subprocess_exec(
217
+ "git",
218
+ "branch",
219
+ "--contains",
220
+ f"{commit_sha}",
221
+ stderr=asyncio.subprocess.PIPE,
222
+ stdout=asyncio.subprocess.PIPE,
223
+ cwd=repo_path,
224
+ )
225
+ stdout, stderr = await proc.communicate()
226
+ if stderr or proc.returncode != 0:
227
+ return False
228
+
229
+ return branch in stdout.decode()
230
+
231
+
232
+ def rebase( # noqa: PLR0913
233
+ repo: Repo,
234
+ *,
235
+ path: str,
236
+ line: int,
237
+ rev_a: str,
238
+ rev_b: str,
239
+ ignore_errors: bool = True,
240
+ ) -> RebaseResult | None:
241
+ try:
242
+ result: list[str] = (
243
+ repo.git(c="core.quotepath=off")
244
+ .blame(
245
+ f"{rev_a}..{rev_b}",
246
+ "--",
247
+ path,
248
+ L=f"{line},+1",
249
+ l=True,
250
+ p=True,
251
+ show_number=True,
252
+ reverse=True,
253
+ show_name=True,
254
+ M=True,
255
+ C=True,
256
+ )
257
+ .splitlines()
258
+ )
259
+ except GitError:
260
+ if ignore_errors:
261
+ LOGGER.exception("A git error occurred while rebasing")
262
+ return None
263
+
264
+ raise
265
+
266
+ new_rev = result[0].split(" ")[0]
267
+ new_line = int(result[0].split(" ")[1])
268
+ new_path = next(
269
+ (row.split(" ", maxsplit=1)[1] for row in result if row.startswith("filename ")),
270
+ path,
271
+ )
272
+ # With core.quotepath=off, git only wraps the path in double quotes when it
273
+ # C-style-escapes "unusual" characters (", \, control chars). Bare Unicode
274
+ # paths (CJK, Cyrillic, ...) come through as proper Python strings and must
275
+ # skip the unicode-escape pipeline, which would fail .encode("latin-1") on
276
+ # codepoints above U+00FF.
277
+ if new_path.startswith('"'):
278
+ try:
279
+ decoded_bytes = new_path.encode("latin-1").decode("unicode-escape").encode("latin-1")
280
+ except (UnicodeDecodeError, UnicodeEncodeError):
281
+ if ignore_errors:
282
+ LOGGER.exception(
283
+ "Error decoding the new path",
284
+ extra={
285
+ "extra": {
286
+ "path": path,
287
+ "new_path": new_path,
288
+ },
289
+ },
290
+ )
291
+ return None
292
+
293
+ raise
294
+
295
+ try:
296
+ new_path = decoded_bytes.decode("utf-8").strip('"')
297
+ except UnicodeDecodeError:
298
+ # Filesystem locales may emit non-UTF-8 (e.g. Latin-1) path bytes
299
+ new_path = decoded_bytes.decode("latin-1").strip('"')
300
+
301
+ return RebaseResult(path=new_path, line=new_line, rev=new_rev)
302
+
303
+
304
+ def get_head_commit(path_to_repo: Path, branch: str) -> str | None:
305
+ try:
306
+ return (
307
+ Repo(path_to_repo.resolve(), search_parent_directories=True).heads[branch].object.hexsha
308
+ )
309
+ except (GitError, AttributeError, IndexError):
310
+ return None
311
+
312
+
313
+ async def clone( # noqa: PLR0913
314
+ repo_url: str,
315
+ repo_branch: str,
316
+ *,
317
+ temp_dir: str,
318
+ credential_key: str | None = None,
319
+ user: str | None = None,
320
+ password: str | None = None,
321
+ token: str | None = None,
322
+ provider: str | None = None,
323
+ is_pat: bool = False,
324
+ arn: str | None = None,
325
+ org_external_id: str | None = None,
326
+ follow_redirects: bool = False,
327
+ mirror: bool = False,
328
+ ) -> tuple[str | None, str | None]:
329
+ if credential_key:
330
+ return await ssh_clone(
331
+ branch=repo_branch,
332
+ credential_key=credential_key,
333
+ repo_url=repo_url,
334
+ temp_dir=temp_dir,
335
+ mirror=mirror,
336
+ )
337
+ if user is not None and password is not None:
338
+ return await https_clone(
339
+ branch=repo_branch,
340
+ password=password,
341
+ repo_url=repo_url,
342
+ temp_dir=temp_dir,
343
+ token=None,
344
+ user=user,
345
+ follow_redirects=follow_redirects,
346
+ mirror=mirror,
347
+ )
348
+ if token is not None:
349
+ return await https_clone(
350
+ branch=repo_branch,
351
+ password=None,
352
+ repo_url=repo_url,
353
+ temp_dir=temp_dir,
354
+ token=token,
355
+ user=None,
356
+ provider=provider,
357
+ is_pat=is_pat,
358
+ follow_redirects=follow_redirects,
359
+ mirror=mirror,
360
+ )
361
+ if arn is not None and org_external_id is not None:
362
+ return await call_codecommit_clone(
363
+ branch=repo_branch,
364
+ repo_url=repo_url,
365
+ temp_dir=temp_dir,
366
+ arn=arn,
367
+ org_external_id=org_external_id,
368
+ follow_redirects=follow_redirects,
369
+ mirror=mirror,
370
+ )
371
+
372
+ if repo_url.startswith("http"):
373
+ # it can be a public repository
374
+ return await https_clone(
375
+ branch=repo_branch,
376
+ repo_url=repo_url,
377
+ temp_dir=temp_dir,
378
+ follow_redirects=follow_redirects,
379
+ mirror=mirror,
380
+ )
381
+
382
+ raise InvalidParameter
@@ -0,0 +1,29 @@
1
+ from datetime import (
2
+ datetime,
3
+ )
4
+ from typing import (
5
+ NamedTuple,
6
+ )
7
+
8
+
9
+ class CommitInfo(NamedTuple):
10
+ hash: str
11
+ author: str
12
+ modified_date: datetime
13
+
14
+
15
+ class RebaseResult(NamedTuple):
16
+ path: str
17
+ line: int
18
+ rev: str
19
+
20
+
21
+ class InvalidParameter(Exception): # noqa: N818
22
+ """Exception to control empty required parameters."""
23
+
24
+ def __init__(self, field: str = "") -> None:
25
+ if field:
26
+ msg = f"Exception - Field {field} is invalid"
27
+ else:
28
+ msg = "Exception - Error value is not valid"
29
+ super().__init__(msg)
@@ -0,0 +1,243 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import os
5
+ import uuid
6
+
7
+ import boto3
8
+ from botocore.exceptions import ClientError
9
+
10
+ from .codecommit_utils import (
11
+ extract_region,
12
+ )
13
+ from .ssh_utils import (
14
+ parse_ssh_url,
15
+ ssh_git_env_context,
16
+ )
17
+ from .utils import (
18
+ format_url,
19
+ get_https_git_config_args,
20
+ )
21
+
22
+ LOGGER = logging.getLogger(__name__)
23
+ MSG = "Repo cloning failed"
24
+
25
+
26
+ async def ssh_clone(
27
+ *,
28
+ branch: str,
29
+ credential_key: str,
30
+ repo_url: str,
31
+ temp_dir: str,
32
+ mirror: bool = False,
33
+ ) -> tuple[str | None, str | None]:
34
+ parsed_repo_url = parse_ssh_url(repo_url)
35
+ folder_to_clone_root = f"{temp_dir}/{uuid.uuid4()}"
36
+ with ssh_git_env_context(temp_dir, credential_key) as env:
37
+ try:
38
+ proc = await asyncio.create_subprocess_exec(
39
+ "git",
40
+ "-c",
41
+ "core.symlinks=false",
42
+ "clone",
43
+ *(
44
+ ["--mirror"]
45
+ if mirror
46
+ else [
47
+ "--branch",
48
+ branch,
49
+ "--single-branch",
50
+ ]
51
+ ),
52
+ "--",
53
+ parsed_repo_url,
54
+ folder_to_clone_root,
55
+ stderr=asyncio.subprocess.PIPE,
56
+ stdout=asyncio.subprocess.PIPE,
57
+ env=env,
58
+ cwd=temp_dir,
59
+ )
60
+ _, stderr = await proc.communicate()
61
+ except OSError as ex:
62
+ LOGGER.exception(MSG, extra={"extra": {"branch": branch, "repo": repo_url}})
63
+ return None, str(ex)
64
+
65
+ if mirror and proc.returncode == 0:
66
+ with open(f"{folder_to_clone_root}/.info.json", "w") as f: # noqa: ASYNC230,PTH123
67
+ json.dump({"fluid_branch": branch, "repo": repo_url}, f)
68
+ if proc.returncode == 0:
69
+ return (folder_to_clone_root, None)
70
+
71
+ LOGGER.error(MSG, extra={"extra": {"message": stderr.decode()}})
72
+
73
+ return (None, stderr.decode("utf-8"))
74
+
75
+
76
+ async def https_clone( # noqa: PLR0913
77
+ *,
78
+ branch: str,
79
+ repo_url: str,
80
+ temp_dir: str,
81
+ password: str | None = None,
82
+ token: str | None = None,
83
+ user: str | None = None,
84
+ provider: str | None = None,
85
+ is_pat: bool = False,
86
+ follow_redirects: bool = False,
87
+ mirror: bool = False,
88
+ ) -> tuple[str | None, str | None]:
89
+ url = format_url(
90
+ repo_url=repo_url,
91
+ user=user,
92
+ password=password,
93
+ token=token,
94
+ provider=provider,
95
+ is_pat=is_pat,
96
+ )
97
+ config_args = get_https_git_config_args(
98
+ follow_redirects=follow_redirects,
99
+ is_pat=is_pat,
100
+ token=token,
101
+ )
102
+ folder_to_clone_root = f"{temp_dir}/{uuid.uuid4()}"
103
+ try:
104
+ proc = await asyncio.create_subprocess_exec(
105
+ "git",
106
+ *config_args,
107
+ "clone",
108
+ *(
109
+ ["--mirror"]
110
+ if mirror
111
+ else [
112
+ "--branch",
113
+ branch,
114
+ "--single-branch",
115
+ ]
116
+ ),
117
+ "--",
118
+ url,
119
+ folder_to_clone_root,
120
+ stderr=asyncio.subprocess.PIPE,
121
+ stdout=asyncio.subprocess.PIPE,
122
+ cwd=temp_dir,
123
+ )
124
+ _, stderr = await proc.communicate()
125
+ except OSError as ex:
126
+ LOGGER.exception(MSG, extra={"extra": {"branch": branch, "repo": repo_url}})
127
+
128
+ return None, str(ex)
129
+
130
+ if mirror and proc.returncode == 0:
131
+ with open(f"{folder_to_clone_root}/.info.json", "w") as f: # noqa: ASYNC230,PTH123
132
+ json.dump({"fluid_branch": branch, "repo": repo_url}, f)
133
+
134
+ if proc.returncode == 0:
135
+ return (folder_to_clone_root, None)
136
+
137
+ LOGGER.error(MSG, extra={"extra": {"message": stderr.decode()}})
138
+
139
+ return (None, stderr.decode("utf-8"))
140
+
141
+
142
+ async def codecommit_clone( # noqa: PLR0913
143
+ *,
144
+ env: dict[str, str],
145
+ branch: str,
146
+ repo_url: str,
147
+ temp_dir: str,
148
+ mirror: bool = False,
149
+ follow_redirects: bool = False,
150
+ ) -> tuple[str | None, str | None]:
151
+ folder_to_clone_root = f"{temp_dir}/{uuid.uuid4()}"
152
+ try:
153
+ proc = await asyncio.create_subprocess_exec(
154
+ "git",
155
+ "-c",
156
+ "core.symlinks=false",
157
+ "-c",
158
+ "http.sslVerify=false",
159
+ "-c",
160
+ f"http.followRedirects={follow_redirects}",
161
+ "clone",
162
+ *(
163
+ ["--mirror"]
164
+ if mirror
165
+ else [
166
+ "--branch",
167
+ branch,
168
+ "--single-branch",
169
+ ]
170
+ ),
171
+ "--",
172
+ repo_url,
173
+ folder_to_clone_root,
174
+ cwd=temp_dir,
175
+ env={**os.environ.copy(), **env},
176
+ stderr=asyncio.subprocess.PIPE,
177
+ stdout=asyncio.subprocess.PIPE,
178
+ )
179
+ _, stderr = await proc.communicate()
180
+ except OSError as ex:
181
+ LOGGER.exception(MSG, extra={"extra": {"branch": branch, "repo": repo_url}})
182
+
183
+ return None, str(ex)
184
+
185
+ if mirror and proc.returncode == 0:
186
+ with open(f"{folder_to_clone_root}/.info.json", "w") as f: # noqa: ASYNC230, PTH123
187
+ json.dump({"fluid_branch": branch, "repo": repo_url}, f)
188
+
189
+ if proc.returncode == 0:
190
+ return (folder_to_clone_root, None)
191
+
192
+ LOGGER.error(MSG, extra={"extra": {"message": stderr.decode()}})
193
+
194
+ return (None, stderr.decode("utf-8"))
195
+
196
+
197
+ async def call_codecommit_clone( # noqa: PLR0913
198
+ *,
199
+ branch: str,
200
+ repo_url: str,
201
+ temp_dir: str,
202
+ arn: str,
203
+ org_external_id: str,
204
+ follow_redirects: bool = False,
205
+ mirror: bool = False,
206
+ ) -> tuple[str | None, str | None]:
207
+ try:
208
+ sts_client = boto3.client("sts")
209
+ assumed_role = sts_client.assume_role(
210
+ RoleArn=arn,
211
+ RoleSessionName=f"session-{uuid.uuid4()}",
212
+ ExternalId=org_external_id,
213
+ )
214
+ credentials = assumed_role["Credentials"]
215
+
216
+ return await codecommit_clone(
217
+ env={
218
+ "AWS_ACCESS_KEY_ID": credentials["AccessKeyId"],
219
+ "AWS_SECRET_ACCESS_KEY": credentials["SecretAccessKey"],
220
+ "AWS_SESSION_TOKEN": credentials["SessionToken"],
221
+ "AWS_DEFAULT_REGION": extract_region(repo_url),
222
+ },
223
+ branch=branch,
224
+ repo_url=repo_url,
225
+ temp_dir=temp_dir,
226
+ follow_redirects=follow_redirects,
227
+ mirror=mirror,
228
+ )
229
+
230
+ except ClientError as exc:
231
+ LOGGER.exception(
232
+ MSG,
233
+ extra={
234
+ "extra": {
235
+ "repo_url": repo_url,
236
+ "arn": arn,
237
+ "org_external_id": org_external_id,
238
+ "exc": exc,
239
+ },
240
+ },
241
+ )
242
+
243
+ return None, str(exc)