-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
/
Copy path__init__.py
112 lines (90 loc) · 3.8 KB
/
__init__.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
"""
Module for the file tree diff feature (FTD).
This feature is used to compare the files of two versions of a project.
The process is as follows:
- A build is triggered for a version.
- A task is triggered after the build has succeeded
to generate a manifest of the files of the version.
Currently, we only consider the latest version and pull request previews.
- The manifest contains the hash of the main content of each file.
Only HTML files are considered for now.
- The manifest is stored in the diff media storage.
- Then our application can compare the manifest to get a list of added,
deleted, and modified files between two versions.
"""
import json
from dataclasses import asdict
from readthedocs.builds.models import Version
from readthedocs.filetreediff.dataclasses import FileTreeDiff, FileTreeDiffManifest
from readthedocs.projects.constants import MEDIA_TYPE_DIFF
from readthedocs.storage import build_media_storage
MANIFEST_FILE_NAME = "manifest.json"
def get_diff(version_a: Version, version_b: Version) -> FileTreeDiff | None:
"""
Get the file tree diff between two versions.
If any of the versions don't have a manifest, return None.
If the latest build of any of the versions is different from the manifest build,
the diff is marked as outdated. The client is responsible for deciding
how to handle this case.
Set operations are used to calculate the added, deleted, and modified files.
To get the modified files, we compare the main content hash of each common file.
If there are no changes between the versions, all lists will be empty.
"""
outdated = False
manifests: list[FileTreeDiffManifest] = []
for version in (version_a, version_b):
manifest = get_manifest(version)
if not manifest:
return None
latest_build = version.latest_successful_build
if not latest_build:
return None
if latest_build.id != manifest.build.id:
outdated = True
manifests.append(manifest)
# pylint: disable=unbalanced-tuple-unpacking
version_a_manifest, version_b_manifest = manifests
files_a = set(version_a_manifest.files.keys())
files_b = set(version_b_manifest.files.keys())
files_added = list(files_a - files_b)
files_deleted = list(files_b - files_a)
files_modified = []
for file_path in files_a & files_b:
file_a = version_a_manifest.files[file_path]
file_b = version_b_manifest.files[file_path]
if file_a.main_content_hash != file_b.main_content_hash:
files_modified.append(file_path)
return FileTreeDiff(
added=files_added,
deleted=files_deleted,
modified=files_modified,
outdated=outdated,
)
def get_manifest(version: Version) -> FileTreeDiffManifest | None:
"""
Get the file manifest for a version.
If the manifest file does not exist, return None.
"""
storage_path = version.project.get_storage_path(
type_=MEDIA_TYPE_DIFF,
version_slug=version.slug,
include_file=False,
version_type=version.type,
)
manifest_path = build_media_storage.join(storage_path, MANIFEST_FILE_NAME)
try:
with build_media_storage.open(manifest_path) as manifest_file:
manifest = json.load(manifest_file)
except FileNotFoundError:
return None
return FileTreeDiffManifest.from_dict(manifest)
def write_manifest(version: Version, manifest: FileTreeDiffManifest):
storage_path = version.project.get_storage_path(
type_=MEDIA_TYPE_DIFF,
version_slug=version.slug,
include_file=False,
version_type=version.type,
)
manifest_path = build_media_storage.join(storage_path, MANIFEST_FILE_NAME)
with build_media_storage.open(manifest_path, "w") as f:
json.dump(asdict(manifest), f)