UEPyScripts 1.4.1__tar.gz → 1.4.3__tar.gz
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.
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/PKG-INFO +1 -1
- uepyscripts-1.4.3/src/uepyscripts/tools/ugs/git_utils.py +110 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/tools/ugs/push.py +84 -16
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/tools/ugs/ugs_types.py +10 -0
- uepyscripts-1.4.1/src/uepyscripts/tools/ugs/git_utils.py +0 -49
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/.gitignore +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/LICENSE +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/README.md +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/pyproject.toml +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/__init__.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/context/__init__.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/internal/__init__.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/internal/config.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/internal/engine.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/internal/engine_resolver.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/internal/project.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/py.typed +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/run/__init__.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/run/buildgraph.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/run/uat.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/run/ubt.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/tools/__init__.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/tools/ci/buildgraph.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/tools/ci/cleanup.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/tools/helpers.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/tools/subprocess.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/tools/ue/__init__.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/tools/ue/check_engine_installation.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/tools/ue/close_editor.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/tools/ue/compile_editor.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/tools/ue/engine_installation/engine_destination.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/tools/ue/engine_installation/engine_installer.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/tools/ue/engine_installation/engine_source.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/tools/ue/generate_solution.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/tools/ue/run_editor.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/tools/ugs/cleanup.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/tools/ugs/pull.py +0 -0
- {uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/tools/winreg.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: UEPyScripts
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.3
|
|
4
4
|
Summary: Collection of Python tools and helpers designed to automate common Unreal Engine project tasks (builds, packaging, editor control, CI tasks, etc.).
|
|
5
5
|
Project-URL: Changelog, https://github.com/TheEmidee/UEPyScripts/releases
|
|
6
6
|
Project-URL: Homepage, https://github.com/TheEmidee/UEPyScripts
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from uepyscripts import logger
|
|
6
|
+
|
|
7
|
+
ANCESTRY_LOOKBACK = 500
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_current_sha(cwd: Path) -> str:
|
|
11
|
+
sha = os.environ.get("GIT_COMMIT") # Jenkins sets this automatically
|
|
12
|
+
if sha:
|
|
13
|
+
return sha
|
|
14
|
+
return subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=cwd, text=True).strip()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_current_branch(cwd: Path) -> str:
|
|
18
|
+
branch = os.environ.get("GIT_BRANCH") # Jenkins' Git plugin sets this, sometimes as "origin/<branch>"
|
|
19
|
+
if branch:
|
|
20
|
+
return branch.split("/", 1)[1] if "/" in branch else branch
|
|
21
|
+
|
|
22
|
+
branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=cwd, text=True).strip()
|
|
23
|
+
if branch != "HEAD":
|
|
24
|
+
return branch
|
|
25
|
+
|
|
26
|
+
# Detached HEAD (e.g. a CI checkout of an explicit ref rather than a
|
|
27
|
+
# tracked branch, with no GIT_BRANCH env var set either) — fall back to
|
|
28
|
+
# whichever local or remote-tracking ref currently points at this commit.
|
|
29
|
+
refs = subprocess.check_output(
|
|
30
|
+
["git", "for-each-ref", "--points-at=HEAD", "--format=%(refname:short)", "refs/heads/", "refs/remotes/"],
|
|
31
|
+
cwd=cwd,
|
|
32
|
+
text=True,
|
|
33
|
+
).splitlines()
|
|
34
|
+
for ref in refs:
|
|
35
|
+
return ref.split("/", 1)[1] if "/" in ref else ref
|
|
36
|
+
|
|
37
|
+
logger.warning("Could not resolve current branch: HEAD is detached and no ref points at it")
|
|
38
|
+
return branch
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def is_shallow_repository(cwd: Path) -> bool:
|
|
42
|
+
out = subprocess.check_output(["git", "rev-parse", "--is-shallow-repository"], cwd=cwd, text=True).strip()
|
|
43
|
+
return out == "true"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def deepen_shallow_history(cwd: Path, limit: int, remote: str = "origin") -> bool:
|
|
47
|
+
"""Pulls down the commits missing from a shallow checkout, and only the
|
|
48
|
+
commits: `--filter=tree:0` keeps trees and blobs on the server, so this
|
|
49
|
+
stays a metadata-sized fetch even on a repository the size of an engine
|
|
50
|
+
tree. Returns False (after logging) when the remote refuses, so callers
|
|
51
|
+
can carry on with whatever history is available locally."""
|
|
52
|
+
try:
|
|
53
|
+
subprocess.run(
|
|
54
|
+
["git", "fetch", "--no-tags", "--filter=tree:0", f"--deepen={limit}", remote],
|
|
55
|
+
cwd=cwd,
|
|
56
|
+
check=True,
|
|
57
|
+
capture_output=True,
|
|
58
|
+
text=True,
|
|
59
|
+
)
|
|
60
|
+
return True
|
|
61
|
+
except subprocess.CalledProcessError as e:
|
|
62
|
+
reason = e.stderr.strip() or e
|
|
63
|
+
logger.warning(f"Could not deepen shallow history from '{remote}' ({reason}); ancestry walking is limited to the local history")
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_local_ancestry(cwd: Path, limit: int = ANCESTRY_LOOKBACK) -> list[str]:
|
|
68
|
+
ancestry = _log_ancestry(cwd, limit)
|
|
69
|
+
|
|
70
|
+
# CI checkouts are typically shallow clones (Jenkins' cloneOption(depth: 1)),
|
|
71
|
+
# where HEAD is a grafted boundary commit: `git log HEAD` stops at HEAD
|
|
72
|
+
# itself and every already-published parent looks unreachable, which would
|
|
73
|
+
# downgrade an incremental publish into a full fresh sync. Deepen first so
|
|
74
|
+
# the walk sees the real ancestry.
|
|
75
|
+
if len(ancestry) < limit and is_shallow_repository(cwd):
|
|
76
|
+
logger.info(f"Shallow checkout detected ({len(ancestry)} commit(s) available); deepening history to {limit} commits")
|
|
77
|
+
if deepen_shallow_history(cwd, limit):
|
|
78
|
+
ancestry = _log_ancestry(cwd, limit)
|
|
79
|
+
|
|
80
|
+
return ancestry
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _log_ancestry(cwd: Path, limit: int) -> list[str]:
|
|
84
|
+
out = subprocess.check_output(["git", "log", f"-{limit}", "--format=%H", "HEAD"], cwd=cwd, text=True)
|
|
85
|
+
return out.splitlines()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_remote_reachable_shas(cwd: Path, remote: str = "origin") -> set[str]:
|
|
89
|
+
"""All commit SHAs reachable from any current branch tip on `remote`, after
|
|
90
|
+
pruning local remote-tracking refs for branches deleted on the remote."""
|
|
91
|
+
subprocess.run(["git", "fetch", "--prune", remote], cwd=cwd, check=True, capture_output=True, text=True)
|
|
92
|
+
out = subprocess.check_output(["git", "rev-list", f"--remotes={remote}"], cwd=cwd, text=True)
|
|
93
|
+
return set(out.splitlines())
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def resolve_nearest_published_ancestor(index: list[str], ancestry: list[str]) -> tuple[str | None, bool]:
|
|
97
|
+
"""Finds the nearest ancestor SHA (including HEAD itself) present in the
|
|
98
|
+
index. Returns None if nothing has ever been published reachable from HEAD
|
|
99
|
+
within the walked ancestry. The bool is True when the match was found by
|
|
100
|
+
walking the ancestry itself (an exact match), False when falling back to
|
|
101
|
+
the most recent published version because nothing in the ancestry matched."""
|
|
102
|
+
index_set = set(index)
|
|
103
|
+
for sha in ancestry:
|
|
104
|
+
if sha in index_set:
|
|
105
|
+
return sha, True
|
|
106
|
+
|
|
107
|
+
if index:
|
|
108
|
+
return index[-1], False
|
|
109
|
+
|
|
110
|
+
return None, True
|
|
@@ -35,10 +35,9 @@ from uepyscripts.tools.ugs.git_utils import (
|
|
|
35
35
|
get_local_ancestry,
|
|
36
36
|
resolve_nearest_published_ancestor,
|
|
37
37
|
)
|
|
38
|
-
from uepyscripts.tools.ugs.ugs_types import Manifest, VersionManifest
|
|
38
|
+
from uepyscripts.tools.ugs.ugs_types import HashCacheEntry, HashCacheManifest, Manifest, VersionManifest
|
|
39
39
|
|
|
40
40
|
# ---- CONFIG ----------------------------------------------------------
|
|
41
|
-
LOCAL_HASH_CACHE = ".hash-cache.json"
|
|
42
41
|
|
|
43
42
|
# Extensions that go through the delta/checkpoint sync pipeline (no PDBs — see symstore below)
|
|
44
43
|
SYNC_EXT = {".dll", ".exe", ".lib", ".modules", ".target"}
|
|
@@ -72,18 +71,18 @@ class HashCacheInfos:
|
|
|
72
71
|
|
|
73
72
|
|
|
74
73
|
class HashCache:
|
|
75
|
-
def __init__(self) -> None:
|
|
74
|
+
def __init__(self, commit_sha: str) -> None:
|
|
75
|
+
self.commit_sha = commit_sha
|
|
76
76
|
self.cache: dict[str, HashCacheInfos] = dict()
|
|
77
|
-
self.local_hash_cache = Path(LOCAL_HASH_CACHE)
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
self.cache = {path: HashCacheInfos(**entry) for path, entry in raw.items()}
|
|
78
|
+
def seed_from(self, files: dict[str, HashCacheEntry]) -> None:
|
|
79
|
+
self.cache = {path: HashCacheInfos(**entry) for path, entry in files.items()}
|
|
82
80
|
|
|
83
|
-
def
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
def to_manifest(self) -> HashCacheManifest:
|
|
82
|
+
return {
|
|
83
|
+
"commit_sha": self.commit_sha,
|
|
84
|
+
"files": {path: cast(HashCacheEntry, asdict(entry)) for path, entry in self.cache.items()},
|
|
85
|
+
}
|
|
87
86
|
|
|
88
87
|
def get(self, path: str) -> HashCacheInfos | None:
|
|
89
88
|
return self.cache.get(path)
|
|
@@ -102,10 +101,35 @@ class Context:
|
|
|
102
101
|
)
|
|
103
102
|
self.symbol_store_path: str = args.symbol_store_path
|
|
104
103
|
self.tmp_root: Path = Path(tempfile.mkdtemp(prefix="publish-"))
|
|
105
|
-
self.hash_cache = HashCache()
|
|
106
104
|
self.root_path = root_path
|
|
107
105
|
self.current_sha = get_current_sha(root_path)
|
|
108
106
|
self.current_branch = get_current_branch(root_path)
|
|
107
|
+
self.ancestry: list[str] = get_local_ancestry(root_path)
|
|
108
|
+
self.hash_cache, self.hash_cache_index = self._load_hash_cache()
|
|
109
|
+
|
|
110
|
+
def _load_hash_cache(self) -> tuple[HashCache, list[str]]:
|
|
111
|
+
"""Resolves the nearest commit with an uploaded hash cache and seeds a
|
|
112
|
+
fresh HashCache from it. Unlike the manifest baseline used for diffing,
|
|
113
|
+
an inexact match is fine here: a fingerprint mismatch just falls back to
|
|
114
|
+
a full hash for that file, no worse than a cold-cache case, so
|
|
115
|
+
there's no correctness risk in seeding from a non-ancestor."""
|
|
116
|
+
bucket_name = self.args.s3_bucket_name
|
|
117
|
+
hash_cache_index = cast(list[str], self.s3_client.download_json(bucket_name, "hash_caches/index.json", default=[]))
|
|
118
|
+
baseline_sha, _ = resolve_nearest_published_ancestor(hash_cache_index, self.ancestry)
|
|
119
|
+
|
|
120
|
+
hash_cache = HashCache(self.current_sha)
|
|
121
|
+
if baseline_sha is None:
|
|
122
|
+
logger.info("No hash cache baseline found — starting with a cold hash cache")
|
|
123
|
+
return hash_cache, hash_cache_index
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
manifest = cast(HashCacheManifest, self.s3_client.download_json(bucket_name, f"hash_caches/{baseline_sha}.json"))
|
|
127
|
+
hash_cache.seed_from(manifest["files"])
|
|
128
|
+
logger.info(f"Seeded hash cache from {baseline_sha} ({len(manifest['files'])} entries)")
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.warning(f"Failed to load hash cache from hash_caches/{baseline_sha}.json ({e}); starting with a cold hash cache")
|
|
131
|
+
|
|
132
|
+
return hash_cache, hash_cache_index
|
|
109
133
|
|
|
110
134
|
def finalize(self) -> None:
|
|
111
135
|
if not self.args.keep_temp_directory:
|
|
@@ -327,6 +351,24 @@ def prune_old_versions(context: Context, cfg: FilesConfiguration, index: list[st
|
|
|
327
351
|
return index[cutoff_idx:], checkpoints_to_keep
|
|
328
352
|
|
|
329
353
|
|
|
354
|
+
def prune_old_hash_caches(context: Context, hash_cache_index: list[str], keep: int) -> list[str]:
|
|
355
|
+
"""Simple count-based retention for the hash-cache side-channel: unlike
|
|
356
|
+
index.json (only appended on actual content changes), hash_cache_index
|
|
357
|
+
grows once per run, so it needs its own, faster-turnover retention
|
|
358
|
+
rather than piggybacking on prune_old_versions's checkpoint-aligned
|
|
359
|
+
logic."""
|
|
360
|
+
if len(hash_cache_index) <= keep:
|
|
361
|
+
return hash_cache_index
|
|
362
|
+
|
|
363
|
+
to_delete = hash_cache_index[:-keep]
|
|
364
|
+
to_keep = hash_cache_index[-keep:]
|
|
365
|
+
|
|
366
|
+
logger.info(f"Pruning {len(to_delete)} old hash cache(s)")
|
|
367
|
+
context.s3_client.delete_keys(context.args.s3_bucket_name, [f"hash_caches/{sha}.json" for sha in to_delete])
|
|
368
|
+
|
|
369
|
+
return to_keep
|
|
370
|
+
|
|
371
|
+
|
|
330
372
|
def publish_to_symbol_store(context: Context, paths: list[str], cfg: FilesConfiguration) -> None:
|
|
331
373
|
"""Indexes PDB/DLL/EXE files into the symbol store via symstore.exe."""
|
|
332
374
|
logger.info("Publish to symbol store")
|
|
@@ -397,6 +439,12 @@ def parse_arguments() -> argparse.Namespace:
|
|
|
397
439
|
default=2,
|
|
398
440
|
help="Number of checkpoints to retain; older ones (and their deltas) get pruned (default: 2)",
|
|
399
441
|
)
|
|
442
|
+
parser.add_argument(
|
|
443
|
+
"--keep_hash_caches",
|
|
444
|
+
type=int,
|
|
445
|
+
default=100,
|
|
446
|
+
help="Number of hash cache entries to retain in hash_caches/index.json; older ones get pruned (default: 100)",
|
|
447
|
+
)
|
|
400
448
|
parser.add_argument(
|
|
401
449
|
"--checkpoint-branch",
|
|
402
450
|
type=str,
|
|
@@ -460,8 +508,7 @@ def main() -> None:
|
|
|
460
508
|
logger.info(f"Finished scan. Found {len(new_state)} files.")
|
|
461
509
|
|
|
462
510
|
logger.info("Resolve diff baseline from git ancestry")
|
|
463
|
-
|
|
464
|
-
baseline_sha, is_exact = resolve_nearest_published_ancestor(index, ancestry)
|
|
511
|
+
baseline_sha, is_exact = resolve_nearest_published_ancestor(index, context.ancestry)
|
|
465
512
|
|
|
466
513
|
old_state: dict[str, str]
|
|
467
514
|
if baseline_sha is None:
|
|
@@ -573,8 +620,29 @@ def main() -> None:
|
|
|
573
620
|
|
|
574
621
|
logger.info("=== Finished processing binary files ===")
|
|
575
622
|
|
|
576
|
-
|
|
577
|
-
|
|
623
|
+
if not context.args.skip_upload:
|
|
624
|
+
logger.info("Upload hash cache")
|
|
625
|
+
context.s3_client.upload_bytes(
|
|
626
|
+
context.args.s3_bucket_name,
|
|
627
|
+
f"hash_caches/{context.current_sha}.json",
|
|
628
|
+
json.dumps(context.hash_cache.to_manifest()).encode(),
|
|
629
|
+
content_type="application/json",
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
if context.current_sha not in context.hash_cache_index:
|
|
633
|
+
context.hash_cache_index.append(context.current_sha)
|
|
634
|
+
|
|
635
|
+
context.hash_cache_index = prune_old_hash_caches(context, context.hash_cache_index, context.args.keep_hash_caches)
|
|
636
|
+
|
|
637
|
+
logger.info("Upload hash_caches/index.json")
|
|
638
|
+
context.s3_client.upload_bytes(
|
|
639
|
+
context.args.s3_bucket_name,
|
|
640
|
+
"hash_caches/index.json",
|
|
641
|
+
json.dumps(context.hash_cache_index).encode(),
|
|
642
|
+
content_type="application/json",
|
|
643
|
+
)
|
|
644
|
+
else:
|
|
645
|
+
logger.info("Skipping hash cache upload (--skip-upload)")
|
|
578
646
|
finally:
|
|
579
647
|
context.finalize()
|
|
580
648
|
|
|
@@ -7,3 +7,13 @@ class VersionManifest(TypedDict):
|
|
|
7
7
|
files: Manifest # full state at this version: {relative_path: sha256}
|
|
8
8
|
changed: list[str] # files added or modified since the previous version
|
|
9
9
|
removed: list[str] # files removed since the previous version
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HashCacheEntry(TypedDict):
|
|
13
|
+
fingerprint: str # "mtime_ns:size"
|
|
14
|
+
hash: str # blake3 hex digest
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HashCacheManifest(TypedDict):
|
|
18
|
+
commit_sha: str # commit this cache was captured at
|
|
19
|
+
files: dict[str, HashCacheEntry]
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import subprocess
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
ANCESTRY_LOOKBACK = 2000
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def get_current_sha(cwd: Path) -> str:
|
|
9
|
-
sha = os.environ.get("GIT_COMMIT") # Jenkins sets this automatically
|
|
10
|
-
if sha:
|
|
11
|
-
return sha
|
|
12
|
-
return subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=cwd, text=True).strip()
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def get_current_branch(cwd: Path) -> str:
|
|
16
|
-
branch = os.environ.get("GIT_BRANCH") # Jenkins' Git plugin sets this, sometimes as "origin/<branch>"
|
|
17
|
-
if branch:
|
|
18
|
-
return branch.split("/", 1)[1] if "/" in branch else branch
|
|
19
|
-
return subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=cwd, text=True).strip()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def get_local_ancestry(cwd: Path, limit: int = ANCESTRY_LOOKBACK) -> list[str]:
|
|
23
|
-
out = subprocess.check_output(["git", "log", f"-{limit}", "--format=%H", "HEAD"], cwd=cwd, text=True)
|
|
24
|
-
return out.splitlines()
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def get_remote_reachable_shas(cwd: Path, remote: str = "origin") -> set[str]:
|
|
28
|
-
"""All commit SHAs reachable from any current branch tip on `remote`, after
|
|
29
|
-
pruning local remote-tracking refs for branches deleted on the remote."""
|
|
30
|
-
subprocess.run(["git", "fetch", "--prune", remote], cwd=cwd, check=True, capture_output=True, text=True)
|
|
31
|
-
out = subprocess.check_output(["git", "rev-list", f"--remotes={remote}"], cwd=cwd, text=True)
|
|
32
|
-
return set(out.splitlines())
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def resolve_nearest_published_ancestor(index: list[str], ancestry: list[str]) -> tuple[str | None, bool]:
|
|
36
|
-
"""Finds the nearest ancestor SHA (including HEAD itself) present in the
|
|
37
|
-
index. Returns None if nothing has ever been published reachable from HEAD
|
|
38
|
-
within the walked ancestry. The bool is True when the match was found by
|
|
39
|
-
walking the ancestry itself (an exact match), False when falling back to
|
|
40
|
-
the most recent published version because nothing in the ancestry matched."""
|
|
41
|
-
index_set = set(index)
|
|
42
|
-
for sha in ancestry:
|
|
43
|
-
if sha in index_set:
|
|
44
|
-
return sha, True
|
|
45
|
-
|
|
46
|
-
if index:
|
|
47
|
-
return index[-1], False
|
|
48
|
-
|
|
49
|
-
return None, True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{uepyscripts-1.4.1 → uepyscripts-1.4.3}/src/uepyscripts/tools/ue/check_engine_installation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|