Skip to content

Commit

Permalink
File Tree Diff: allow users to ignore files (#11977)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
humitos authored Feb 10, 2025
1 parent 543f389 commit 2859ec8
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 44 deletions.
35 changes: 35 additions & 0 deletions readthedocs/projects/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -652,11 +652,42 @@ def __init__(self, *args, **kwargs):
self.fields.pop("external_builds_privacy_level")


class OnePerLineList(forms.Field):
widget = forms.Textarea(
attrs={
"placeholder": "\n".join(
[
"whatsnew.html",
"archive/*",
"tags/*",
"guides/getting-started.html",
"changelog.html",
"release/*",
]
),
},
)

def to_python(self, value):
"""Convert a text area into a list of items (one per line)."""
if not value:
return []
# Sanitize lines removing trailing spaces and skipping empty lines
return [line.strip() for line in value.splitlines() if line.strip()]

def prepare_value(self, value):
"""Convert a list of items into a text area (one per line)."""
if not value:
return ""
return "\n".join(value)


class AddonsConfigForm(forms.ModelForm):

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

project = forms.CharField(widget=forms.HiddenInput(), required=False)
filetreediff_ignored_files = OnePerLineList(required=False)

class Meta:
model = AddonsConfig
Expand All @@ -666,6 +697,8 @@ class Meta:
"options_root_selector",
"analytics_enabled",
"doc_diff_enabled",
"filetreediff_enabled",
"filetreediff_ignored_files",
"flyout_enabled",
"flyout_sorting",
"flyout_sorting_latest_stable_at_beginning",
Expand All @@ -682,6 +715,8 @@ class Meta:
labels = {
"enabled": _("Enable Addons"),
"doc_diff_enabled": _("Visual diff enabled"),
"filetreediff_enabled": _("Enabled"),
"filetreediff_ignored_files": _("Ignored files"),
"notifications_show_on_external": _(
"Show a notification on builds from pull requests"
),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 4.2.18 on 2025-02-05 11:33

from django.db import migrations, models
from django_safemigrate import Safe


class Migration(migrations.Migration):

safe = Safe.before_deploy

dependencies = [
('projects', '0145_alter_importedfile_id'),
]

operations = [
migrations.AddField(
model_name='addonsconfig',
name='filetreediff_ignored_files',
field=models.JSONField(blank=True, help_text='List of ignored files. One per line.', null=True),
),
migrations.AddField(
model_name='historicaladdonsconfig',
name='filetreediff_ignored_files',
field=models.JSONField(blank=True, help_text='List of ignored files. One per line.', null=True),
),
]
5 changes: 5 additions & 0 deletions readthedocs/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,11 @@ class AddonsConfig(TimeStampedModel):

# File Tree Diff
filetreediff_enabled = models.BooleanField(default=False, null=True, blank=True)
filetreediff_ignored_files = models.JSONField(
help_text=_("List of ignored files. One per line."),
null=True,
blank=True,
)

# Flyout
flyout_enabled = models.BooleanField(
Expand Down
75 changes: 74 additions & 1 deletion readthedocs/proxito/tests/test_hosting.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@

from readthedocs.builds.constants import BUILD_STATE_FINISHED, EXTERNAL, LATEST
from readthedocs.builds.models import Build, Version
from readthedocs.filetreediff.dataclasses import FileTreeDiffFile, FileTreeDiffManifest
from readthedocs.filetreediff.dataclasses import (
FileTreeDiff,
FileTreeDiffFile,
FileTreeDiffManifest,
)
from readthedocs.projects.constants import (
ADDONS_FLYOUT_SORTING_ALPHABETICALLY,
ADDONS_FLYOUT_SORTING_CALVER,
Expand Down Expand Up @@ -885,6 +889,75 @@ def test_number_of_queries_url_translations(self):
)
assert r.status_code == 200

@override_settings(
RTD_FILETREEDIFF_ALL=True,
)
@mock.patch("readthedocs.proxito.views.hosting.get_diff")
def test_file_tree_diff_ignored_files(self, get_diff):
ignored_files = [
"ignored.html",
"archives/*",
]

self.project.addons.filetreediff_enabled = True
self.project.addons.filetreediff_ignored_files = ignored_files
self.project.addons.save()

get_diff.return_value = FileTreeDiff(
added=["tags/newtag.html"],
modified=["ignored.html", "archives/2025.html", "changelog/2025.2.html"],
deleted=["deleted.html"],
)

r = self.client.get(
reverse("proxito_readthedocs_docs_addons"),
{
"url": "https://project.dev.readthedocs.io/en/latest/",
"client-version": "0.6.0",
"api-version": "1.0.0",
},
secure=True,
headers={
"host": "project.dev.readthedocs.io",
},
)

expected = {
"enabled": True,
"outdated": False,
"diff": {
"added": [
{
"filename": "tags/newtag.html",
"urls": {
"current": "https://project.dev.readthedocs.io/en/latest/tags/newtag.html",
"base": "https://project.dev.readthedocs.io/en/latest/tags/newtag.html",
},
},
],
"deleted": [
{
"filename": "deleted.html",
"urls": {
"current": "https://project.dev.readthedocs.io/en/latest/deleted.html",
"base": "https://project.dev.readthedocs.io/en/latest/deleted.html",
},
},
],
"modified": [
{
"filename": "changelog/2025.2.html",
"urls": {
"current": "https://project.dev.readthedocs.io/en/latest/changelog/2025.2.html",
"base": "https://project.dev.readthedocs.io/en/latest/changelog/2025.2.html",
},
},
],
},
}
assert r.status_code == 200
assert r.json()["addons"]["filetreediff"] == expected

@mock.patch("readthedocs.filetreediff.get_manifest")
def test_file_tree_diff(self, get_manifest):
self.project.addons.filetreediff_enabled = True
Expand Down
68 changes: 25 additions & 43 deletions readthedocs/proxito/views/hosting.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Views for hosting features."""
import fnmatch
from functools import lru_cache

import packaging
Expand Down Expand Up @@ -657,47 +658,21 @@ def _get_filetreediff_response(self, *, request, project, version, resolver):
if not diff:
return None

return {
"enabled": True,
"outdated": diff.outdated,
"diff": {
"added": [
{
"filename": filename,
"urls": {
"current": resolver.resolve_version(
project=project,
filename=filename,
version=version,
),
"base": resolver.resolve_version(
project=project,
filename=filename,
version=base_version,
),
},
}
for filename in diff.added
],
"deleted": [
{
"filename": filename,
"urls": {
"current": resolver.resolve_version(
project=project,
filename=filename,
version=version,
),
"base": resolver.resolve_version(
project=project,
filename=filename,
version=base_version,
),
},
}
for filename in diff.deleted
],
"modified": [
def _filter_diff_files(files):
# Filter out all the files that match the ignored patterns
ignore_patterns = project.addons.filetreediff_ignored_files or []
files = [
filename
for filename in files
if not any(
fnmatch.fnmatch(filename, ignore_pattern)
for ignore_pattern in ignore_patterns
)
]

result = []
for filename in files:
result.append(
{
"filename": filename,
"urls": {
Expand All @@ -713,8 +688,15 @@ def _get_filetreediff_response(self, *, request, project, version, resolver):
),
},
}
for filename in diff.modified
],
)
return result

return {
"outdated": diff.outdated,
"diff": {
"added": _filter_diff_files(diff.added),
"deleted": _filter_diff_files(diff.deleted),
"modified": _filter_diff_files(diff.modified),
},
}

Expand Down
15 changes: 15 additions & 0 deletions readthedocs/rtd_tests/tests/test_project_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -1176,14 +1176,20 @@ def setUp(self):
def test_addonsconfig_form(self):
data = {
"enabled": True,
"options_root_selector": "main",
"analytics_enabled": False,
"doc_diff_enabled": False,
"filetreediff_enabled": True,
# Empty lines, lines with trailing spaces or lines full of spaces are ignored
"filetreediff_ignored_files": "user/index.html\n \n\n\n changelog.html \n",
"flyout_enabled": True,
"flyout_sorting": ADDONS_FLYOUT_SORTING_CALVER,
"flyout_sorting_latest_stable_at_beginning": True,
"flyout_sorting_custom_pattern": None,
"flyout_position": "bottom-left",
"hotkeys_enabled": False,
"search_enabled": False,
"linkpreviews_enabled": True,
"notifications_enabled": True,
"notifications_show_on_latest": True,
"notifications_show_on_non_stable": True,
Expand All @@ -1194,8 +1200,14 @@ def test_addonsconfig_form(self):
form.save()

self.assertEqual(self.project.addons.enabled, True)
self.assertEqual(self.project.addons.options_root_selector, "main")
self.assertEqual(self.project.addons.analytics_enabled, False)
self.assertEqual(self.project.addons.doc_diff_enabled, False)
self.assertEqual(self.project.addons.filetreediff_enabled, True)
self.assertEqual(
self.project.addons.filetreediff_ignored_files,
["user/index.html", "changelog.html"],
)
self.assertEqual(self.project.addons.notifications_enabled, True)
self.assertEqual(self.project.addons.notifications_show_on_latest, True)
self.assertEqual(self.project.addons.notifications_show_on_non_stable, True)
Expand All @@ -1210,8 +1222,11 @@ def test_addonsconfig_form(self):
True,
)
self.assertEqual(self.project.addons.flyout_sorting_custom_pattern, None)
self.assertEqual(self.project.addons.flyout_position, "bottom-left")
self.assertEqual(self.project.addons.hotkeys_enabled, False)
self.assertEqual(self.project.addons.search_enabled, False)
self.assertEqual(self.project.addons.linkpreviews_enabled, True)
self.assertEqual(self.project.addons.notifications_enabled, True)
self.assertEqual(self.project.addons.notifications_show_on_latest, True)
self.assertEqual(self.project.addons.notifications_show_on_non_stable, True)
self.assertEqual(self.project.addons.notifications_show_on_external, True)
Expand Down

0 comments on commit 2859ec8

Please sign in to comment.