Skip to content

Commit 2859ec8

Browse files
authored
File Tree Diff: allow users to ignore files (#11977)
Add a field to allow users to tell us what files should be ignored when comparing versions. This is going to be used by the front-end to don't expose these files in the list of added/modified/deleted files. [Peek 2025-02-05 14-26.webm](https://github.com/user-attachments/assets/f6e45326-fead-4f40-8b76-36a82b1d6cb0) Closes #11694
1 parent 543f389 commit 2859ec8

File tree

6 files changed

+180
-44
lines changed

6 files changed

+180
-44
lines changed

readthedocs/projects/forms.py

+35
Original file line numberDiff line numberDiff line change
@@ -652,11 +652,42 @@ def __init__(self, *args, **kwargs):
652652
self.fields.pop("external_builds_privacy_level")
653653

654654

655+
class OnePerLineList(forms.Field):
656+
widget = forms.Textarea(
657+
attrs={
658+
"placeholder": "\n".join(
659+
[
660+
"whatsnew.html",
661+
"archive/*",
662+
"tags/*",
663+
"guides/getting-started.html",
664+
"changelog.html",
665+
"release/*",
666+
]
667+
),
668+
},
669+
)
670+
671+
def to_python(self, value):
672+
"""Convert a text area into a list of items (one per line)."""
673+
if not value:
674+
return []
675+
# Sanitize lines removing trailing spaces and skipping empty lines
676+
return [line.strip() for line in value.splitlines() if line.strip()]
677+
678+
def prepare_value(self, value):
679+
"""Convert a list of items into a text area (one per line)."""
680+
if not value:
681+
return ""
682+
return "\n".join(value)
683+
684+
655685
class AddonsConfigForm(forms.ModelForm):
656686

657687
"""Form to opt-in into new addons."""
658688

659689
project = forms.CharField(widget=forms.HiddenInput(), required=False)
690+
filetreediff_ignored_files = OnePerLineList(required=False)
660691

661692
class Meta:
662693
model = AddonsConfig
@@ -666,6 +697,8 @@ class Meta:
666697
"options_root_selector",
667698
"analytics_enabled",
668699
"doc_diff_enabled",
700+
"filetreediff_enabled",
701+
"filetreediff_ignored_files",
669702
"flyout_enabled",
670703
"flyout_sorting",
671704
"flyout_sorting_latest_stable_at_beginning",
@@ -682,6 +715,8 @@ class Meta:
682715
labels = {
683716
"enabled": _("Enable Addons"),
684717
"doc_diff_enabled": _("Visual diff enabled"),
718+
"filetreediff_enabled": _("Enabled"),
719+
"filetreediff_ignored_files": _("Ignored files"),
685720
"notifications_show_on_external": _(
686721
"Show a notification on builds from pull requests"
687722
),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Generated by Django 4.2.18 on 2025-02-05 11:33
2+
3+
from django.db import migrations, models
4+
from django_safemigrate import Safe
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
safe = Safe.before_deploy
10+
11+
dependencies = [
12+
('projects', '0145_alter_importedfile_id'),
13+
]
14+
15+
operations = [
16+
migrations.AddField(
17+
model_name='addonsconfig',
18+
name='filetreediff_ignored_files',
19+
field=models.JSONField(blank=True, help_text='List of ignored files. One per line.', null=True),
20+
),
21+
migrations.AddField(
22+
model_name='historicaladdonsconfig',
23+
name='filetreediff_ignored_files',
24+
field=models.JSONField(blank=True, help_text='List of ignored files. One per line.', null=True),
25+
),
26+
]

readthedocs/projects/models.py

+5
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,11 @@ class AddonsConfig(TimeStampedModel):
195195

196196
# File Tree Diff
197197
filetreediff_enabled = models.BooleanField(default=False, null=True, blank=True)
198+
filetreediff_ignored_files = models.JSONField(
199+
help_text=_("List of ignored files. One per line."),
200+
null=True,
201+
blank=True,
202+
)
198203

199204
# Flyout
200205
flyout_enabled = models.BooleanField(

readthedocs/proxito/tests/test_hosting.py

+74-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414

1515
from readthedocs.builds.constants import BUILD_STATE_FINISHED, EXTERNAL, LATEST
1616
from readthedocs.builds.models import Build, Version
17-
from readthedocs.filetreediff.dataclasses import FileTreeDiffFile, FileTreeDiffManifest
17+
from readthedocs.filetreediff.dataclasses import (
18+
FileTreeDiff,
19+
FileTreeDiffFile,
20+
FileTreeDiffManifest,
21+
)
1822
from readthedocs.projects.constants import (
1923
ADDONS_FLYOUT_SORTING_ALPHABETICALLY,
2024
ADDONS_FLYOUT_SORTING_CALVER,
@@ -885,6 +889,75 @@ def test_number_of_queries_url_translations(self):
885889
)
886890
assert r.status_code == 200
887891

892+
@override_settings(
893+
RTD_FILETREEDIFF_ALL=True,
894+
)
895+
@mock.patch("readthedocs.proxito.views.hosting.get_diff")
896+
def test_file_tree_diff_ignored_files(self, get_diff):
897+
ignored_files = [
898+
"ignored.html",
899+
"archives/*",
900+
]
901+
902+
self.project.addons.filetreediff_enabled = True
903+
self.project.addons.filetreediff_ignored_files = ignored_files
904+
self.project.addons.save()
905+
906+
get_diff.return_value = FileTreeDiff(
907+
added=["tags/newtag.html"],
908+
modified=["ignored.html", "archives/2025.html", "changelog/2025.2.html"],
909+
deleted=["deleted.html"],
910+
)
911+
912+
r = self.client.get(
913+
reverse("proxito_readthedocs_docs_addons"),
914+
{
915+
"url": "https://project.dev.readthedocs.io/en/latest/",
916+
"client-version": "0.6.0",
917+
"api-version": "1.0.0",
918+
},
919+
secure=True,
920+
headers={
921+
"host": "project.dev.readthedocs.io",
922+
},
923+
)
924+
925+
expected = {
926+
"enabled": True,
927+
"outdated": False,
928+
"diff": {
929+
"added": [
930+
{
931+
"filename": "tags/newtag.html",
932+
"urls": {
933+
"current": "https://project.dev.readthedocs.io/en/latest/tags/newtag.html",
934+
"base": "https://project.dev.readthedocs.io/en/latest/tags/newtag.html",
935+
},
936+
},
937+
],
938+
"deleted": [
939+
{
940+
"filename": "deleted.html",
941+
"urls": {
942+
"current": "https://project.dev.readthedocs.io/en/latest/deleted.html",
943+
"base": "https://project.dev.readthedocs.io/en/latest/deleted.html",
944+
},
945+
},
946+
],
947+
"modified": [
948+
{
949+
"filename": "changelog/2025.2.html",
950+
"urls": {
951+
"current": "https://project.dev.readthedocs.io/en/latest/changelog/2025.2.html",
952+
"base": "https://project.dev.readthedocs.io/en/latest/changelog/2025.2.html",
953+
},
954+
},
955+
],
956+
},
957+
}
958+
assert r.status_code == 200
959+
assert r.json()["addons"]["filetreediff"] == expected
960+
888961
@mock.patch("readthedocs.filetreediff.get_manifest")
889962
def test_file_tree_diff(self, get_manifest):
890963
self.project.addons.filetreediff_enabled = True

readthedocs/proxito/views/hosting.py

+25-43
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Views for hosting features."""
2+
import fnmatch
23
from functools import lru_cache
34

45
import packaging
@@ -657,47 +658,21 @@ def _get_filetreediff_response(self, *, request, project, version, resolver):
657658
if not diff:
658659
return None
659660

660-
return {
661-
"enabled": True,
662-
"outdated": diff.outdated,
663-
"diff": {
664-
"added": [
665-
{
666-
"filename": filename,
667-
"urls": {
668-
"current": resolver.resolve_version(
669-
project=project,
670-
filename=filename,
671-
version=version,
672-
),
673-
"base": resolver.resolve_version(
674-
project=project,
675-
filename=filename,
676-
version=base_version,
677-
),
678-
},
679-
}
680-
for filename in diff.added
681-
],
682-
"deleted": [
683-
{
684-
"filename": filename,
685-
"urls": {
686-
"current": resolver.resolve_version(
687-
project=project,
688-
filename=filename,
689-
version=version,
690-
),
691-
"base": resolver.resolve_version(
692-
project=project,
693-
filename=filename,
694-
version=base_version,
695-
),
696-
},
697-
}
698-
for filename in diff.deleted
699-
],
700-
"modified": [
661+
def _filter_diff_files(files):
662+
# Filter out all the files that match the ignored patterns
663+
ignore_patterns = project.addons.filetreediff_ignored_files or []
664+
files = [
665+
filename
666+
for filename in files
667+
if not any(
668+
fnmatch.fnmatch(filename, ignore_pattern)
669+
for ignore_pattern in ignore_patterns
670+
)
671+
]
672+
673+
result = []
674+
for filename in files:
675+
result.append(
701676
{
702677
"filename": filename,
703678
"urls": {
@@ -713,8 +688,15 @@ def _get_filetreediff_response(self, *, request, project, version, resolver):
713688
),
714689
},
715690
}
716-
for filename in diff.modified
717-
],
691+
)
692+
return result
693+
694+
return {
695+
"outdated": diff.outdated,
696+
"diff": {
697+
"added": _filter_diff_files(diff.added),
698+
"deleted": _filter_diff_files(diff.deleted),
699+
"modified": _filter_diff_files(diff.modified),
718700
},
719701
}
720702

readthedocs/rtd_tests/tests/test_project_forms.py

+15
Original file line numberDiff line numberDiff line change
@@ -1176,14 +1176,20 @@ def setUp(self):
11761176
def test_addonsconfig_form(self):
11771177
data = {
11781178
"enabled": True,
1179+
"options_root_selector": "main",
11791180
"analytics_enabled": False,
11801181
"doc_diff_enabled": False,
1182+
"filetreediff_enabled": True,
1183+
# Empty lines, lines with trailing spaces or lines full of spaces are ignored
1184+
"filetreediff_ignored_files": "user/index.html\n \n\n\n changelog.html \n",
11811185
"flyout_enabled": True,
11821186
"flyout_sorting": ADDONS_FLYOUT_SORTING_CALVER,
11831187
"flyout_sorting_latest_stable_at_beginning": True,
11841188
"flyout_sorting_custom_pattern": None,
1189+
"flyout_position": "bottom-left",
11851190
"hotkeys_enabled": False,
11861191
"search_enabled": False,
1192+
"linkpreviews_enabled": True,
11871193
"notifications_enabled": True,
11881194
"notifications_show_on_latest": True,
11891195
"notifications_show_on_non_stable": True,
@@ -1194,8 +1200,14 @@ def test_addonsconfig_form(self):
11941200
form.save()
11951201

11961202
self.assertEqual(self.project.addons.enabled, True)
1203+
self.assertEqual(self.project.addons.options_root_selector, "main")
11971204
self.assertEqual(self.project.addons.analytics_enabled, False)
11981205
self.assertEqual(self.project.addons.doc_diff_enabled, False)
1206+
self.assertEqual(self.project.addons.filetreediff_enabled, True)
1207+
self.assertEqual(
1208+
self.project.addons.filetreediff_ignored_files,
1209+
["user/index.html", "changelog.html"],
1210+
)
11991211
self.assertEqual(self.project.addons.notifications_enabled, True)
12001212
self.assertEqual(self.project.addons.notifications_show_on_latest, True)
12011213
self.assertEqual(self.project.addons.notifications_show_on_non_stable, True)
@@ -1210,8 +1222,11 @@ def test_addonsconfig_form(self):
12101222
True,
12111223
)
12121224
self.assertEqual(self.project.addons.flyout_sorting_custom_pattern, None)
1225+
self.assertEqual(self.project.addons.flyout_position, "bottom-left")
12131226
self.assertEqual(self.project.addons.hotkeys_enabled, False)
12141227
self.assertEqual(self.project.addons.search_enabled, False)
1228+
self.assertEqual(self.project.addons.linkpreviews_enabled, True)
1229+
self.assertEqual(self.project.addons.notifications_enabled, True)
12151230
self.assertEqual(self.project.addons.notifications_show_on_latest, True)
12161231
self.assertEqual(self.project.addons.notifications_show_on_non_stable, True)
12171232
self.assertEqual(self.project.addons.notifications_show_on_external, True)

0 commit comments

Comments
 (0)