rekordbox-edit 0.4.0.dev21__tar.gz → 0.4.0.dev22__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.
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/CHANGELOG.md +6 -1
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/PKG-INFO +1 -1
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/pyproject.toml +1 -1
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/commands/edit.py +29 -9
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/tests/commands/test_edit.py +181 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/uv.lock +1 -1
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.agent-style/RULES.md +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.agent-style/claude-code.md +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.github/actions/commitizen-bump/action.yml +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.github/actions/commitizen-bump/commitizen-bump.sh +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.github/actions/install/action.yml +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.github/actions/lint/action.yml +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.github/actions/test/action.yml +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.github/workflows/cd.yml +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.github/workflows/ci.yml +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.github/workflows/publish.yml +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.github/workflows/release.yml +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.gitignore +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.pre-commit-config.yaml +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/AGENTS.md +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/CLAUDE.md +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/CONTRIBUTING.md +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/LICENSE +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/Makefile +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/README.md +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/codecov.yml +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/__init__.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/_click.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/cli.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/commands/__init__.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/commands/convert.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/commands/search.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/logger.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/query.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/utils.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/renovate.json5 +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/ruff.toml +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/tests/__init__.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/tests/commands/__init__.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/tests/commands/test_convert.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/tests/commands/test_search.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/tests/conftest.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/tests/test_logger.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/tests/test_query.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
## v0.4.0.
|
|
1
|
+
## v0.4.0.dev22 (2026-05-17)
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
- feat(edit): add --match for literal find/replace within field value
|
|
5
|
+
- Introduces a _compute_new_value helper and --match option so that
|
|
6
|
+
rbe edit can replace a substring of the current field value instead of
|
|
7
|
+
performing a wholesale replacement; None current values and non-matching
|
|
8
|
+
patterns are treated as no-ops.
|
|
4
9
|
- chore: add max-complexity lint rule
|
|
5
10
|
- docs: update AGENTS.md
|
|
6
11
|
- test: add coverage for --interactive + --yes skipping all confirms
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rekordbox-edit
|
|
3
|
-
Version: 0.4.0.
|
|
3
|
+
Version: 0.4.0.dev22
|
|
4
4
|
Summary: Tools for managing and modifying a RekordBox library en-masse
|
|
5
5
|
Project-URL: Homepage, https://github.com/jviall/rekordbox-edit
|
|
6
6
|
Project-URL: Repository, https://github.com/jviall/rekordbox-edit
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rekordbox-edit"
|
|
7
|
-
version = "0.4.0.
|
|
7
|
+
version = "0.4.0.dev22"
|
|
8
8
|
description = "Tools for managing and modifying a RekordBox library en-masse"
|
|
9
9
|
authors = [{ name = "James Viall", email= "jamesviall@pm.me"}]
|
|
10
10
|
license = "MIT"
|
|
@@ -26,6 +26,18 @@ FIELD_COLUMNS = {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def _compute_new_value(
|
|
30
|
+
current: str | int | None,
|
|
31
|
+
match_pattern: str | None,
|
|
32
|
+
replace_value: str | int,
|
|
33
|
+
) -> str | int | None:
|
|
34
|
+
"""Derive the new field value."""
|
|
35
|
+
if current is None:
|
|
36
|
+
return None
|
|
37
|
+
if match_pattern is not None:
|
|
38
|
+
return str(current).replace(match_pattern, str(replace_value))
|
|
39
|
+
return replace_value
|
|
40
|
+
|
|
29
41
|
|
|
30
42
|
@click.command(
|
|
31
43
|
epilog=f"Debug logs for each run can be found at:\n{get_debug_file_path().parent}"
|
|
@@ -54,6 +66,13 @@ FIELD_COLUMNS = {
|
|
|
54
66
|
required=True,
|
|
55
67
|
help="The new value to write to the field",
|
|
56
68
|
)
|
|
69
|
+
@click.option(
|
|
70
|
+
"--match",
|
|
71
|
+
"match_pattern",
|
|
72
|
+
default=None,
|
|
73
|
+
metavar="PATTERN",
|
|
74
|
+
help="Find this literal string within the field value and replace only that portion",
|
|
75
|
+
)
|
|
57
76
|
@track_ids_argument
|
|
58
77
|
@click.argument(
|
|
59
78
|
"field",
|
|
@@ -62,10 +81,11 @@ FIELD_COLUMNS = {
|
|
|
62
81
|
def edit_command(
|
|
63
82
|
field: str,
|
|
64
83
|
replace_value: str,
|
|
84
|
+
match_pattern: str | None,
|
|
65
85
|
dry_run: bool,
|
|
66
86
|
yes: bool,
|
|
67
87
|
interactive: bool,
|
|
68
|
-
track_ids:
|
|
88
|
+
track_ids: List[str] | None,
|
|
69
89
|
track_id: List[str] | None,
|
|
70
90
|
playlist: List[str] | None,
|
|
71
91
|
exact_playlist: List[str] | None,
|
|
@@ -99,9 +119,7 @@ def edit_command(
|
|
|
99
119
|
)
|
|
100
120
|
|
|
101
121
|
if piped_stdin and not (dry_run or yes):
|
|
102
|
-
raise click.UsageError(
|
|
103
|
-
"Piping track IDs into edit requires --dry-run or --yes"
|
|
104
|
-
)
|
|
122
|
+
raise click.UsageError("Piping track IDs into edit requires --dry-run or --yes")
|
|
105
123
|
|
|
106
124
|
db = Rekordbox6Database()
|
|
107
125
|
if not db.session:
|
|
@@ -127,11 +145,13 @@ def edit_command(
|
|
|
127
145
|
tracks = result.scalars().all()
|
|
128
146
|
|
|
129
147
|
col_name = FIELD_COLUMNS[field]
|
|
130
|
-
edits = [
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
148
|
+
edits = []
|
|
149
|
+
for track in tracks:
|
|
150
|
+
current = getattr(track, col_name)
|
|
151
|
+
new_value = _compute_new_value(current, match_pattern, replace_value)
|
|
152
|
+
if new_value is None or new_value == current:
|
|
153
|
+
continue
|
|
154
|
+
edits.append((track, new_value))
|
|
135
155
|
|
|
136
156
|
if not edits:
|
|
137
157
|
logger.info("No changes to make.")
|
|
@@ -246,3 +246,184 @@ class TestEditCommandPhase1:
|
|
|
246
246
|
|
|
247
247
|
assert result.exit_code == 0
|
|
248
248
|
assert "99999" in result.output
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class TestEditCommandPhase3:
|
|
252
|
+
"""Phase 3: --match flag for literal find/replace within field value."""
|
|
253
|
+
|
|
254
|
+
@patch("rekordbox_edit.commands.edit.confirm")
|
|
255
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
256
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
257
|
+
def test_match_replaces_substring(
|
|
258
|
+
self, mock_db_class, mock_gfc, mock_confirm, make_djmd_content_item
|
|
259
|
+
):
|
|
260
|
+
"""--match substitutes the pattern within the current value."""
|
|
261
|
+
track = make_djmd_content_item(Title="Dark Matter (feat. Jane)")
|
|
262
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
263
|
+
mock_db_class.return_value = mock_db
|
|
264
|
+
mock_gfc.return_value = mock_result
|
|
265
|
+
mock_confirm.return_value = True
|
|
266
|
+
|
|
267
|
+
from click.testing import CliRunner
|
|
268
|
+
result = CliRunner().invoke(
|
|
269
|
+
edit_command,
|
|
270
|
+
["Title", "--match", "feat.", "--replace", "ft."],
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
assert result.exit_code == 0
|
|
274
|
+
assert track.Title == "Dark Matter (ft. Jane)"
|
|
275
|
+
mock_db.session.commit.assert_called_once()
|
|
276
|
+
|
|
277
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
278
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
279
|
+
def test_match_no_match_is_noop(
|
|
280
|
+
self, mock_db_class, mock_gfc, make_djmd_content_item
|
|
281
|
+
):
|
|
282
|
+
"""When --match pattern is not found in current value, track is a no-op."""
|
|
283
|
+
track = make_djmd_content_item(Title="Dark Matter")
|
|
284
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
285
|
+
mock_db_class.return_value = mock_db
|
|
286
|
+
mock_gfc.return_value = mock_result
|
|
287
|
+
|
|
288
|
+
from click.testing import CliRunner
|
|
289
|
+
result = CliRunner().invoke(
|
|
290
|
+
edit_command,
|
|
291
|
+
["Title", "--match", "feat.", "--replace", "ft.", "--yes"],
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
assert result.exit_code == 0
|
|
295
|
+
mock_db.session.commit.assert_not_called()
|
|
296
|
+
|
|
297
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
298
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
299
|
+
def test_match_with_none_current_value_is_noop(
|
|
300
|
+
self, mock_db_class, mock_gfc, make_djmd_content_item
|
|
301
|
+
):
|
|
302
|
+
"""--match on a track where the field is None is silently skipped."""
|
|
303
|
+
track = make_djmd_content_item(Title=None)
|
|
304
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
305
|
+
mock_db_class.return_value = mock_db
|
|
306
|
+
mock_gfc.return_value = mock_result
|
|
307
|
+
|
|
308
|
+
from click.testing import CliRunner
|
|
309
|
+
result = CliRunner().invoke(
|
|
310
|
+
edit_command,
|
|
311
|
+
["Title", "--match", "feat.", "--replace", "ft.", "--yes"],
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
assert result.exit_code == 0
|
|
315
|
+
mock_db.session.commit.assert_not_called()
|
|
316
|
+
|
|
317
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
318
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
319
|
+
def test_match_treats_pattern_as_literal(
|
|
320
|
+
self, mock_db_class, mock_gfc, make_djmd_content_item
|
|
321
|
+
):
|
|
322
|
+
"""--match treats the pattern as a literal string, not a regex."""
|
|
323
|
+
# "1.0" as a regex would also match "1X0"; as a literal it should not
|
|
324
|
+
track = make_djmd_content_item(Title="Version 1X0")
|
|
325
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
326
|
+
mock_db_class.return_value = mock_db
|
|
327
|
+
mock_gfc.return_value = mock_result
|
|
328
|
+
|
|
329
|
+
from click.testing import CliRunner
|
|
330
|
+
result = CliRunner().invoke(
|
|
331
|
+
edit_command,
|
|
332
|
+
["Title", "--match", "1.0", "--replace", "2.0", "--yes"],
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
assert result.exit_code == 0
|
|
336
|
+
mock_db.session.commit.assert_not_called()
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class TestEditCommandUnicode:
|
|
340
|
+
"""Unicode and multibyte character handling in edit command fields."""
|
|
341
|
+
|
|
342
|
+
@patch("rekordbox_edit.commands.edit.confirm")
|
|
343
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
344
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
345
|
+
def test_wholesale_replace_with_unicode_value(
|
|
346
|
+
self, mock_db_class, mock_gfc, mock_confirm, make_djmd_content_item
|
|
347
|
+
):
|
|
348
|
+
"""--replace accepts and writes multibyte unicode titles correctly."""
|
|
349
|
+
track = make_djmd_content_item(Title="Original Title")
|
|
350
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
351
|
+
mock_db_class.return_value = mock_db
|
|
352
|
+
mock_gfc.return_value = mock_result
|
|
353
|
+
mock_confirm.return_value = True
|
|
354
|
+
|
|
355
|
+
from click.testing import CliRunner
|
|
356
|
+
result = CliRunner().invoke(
|
|
357
|
+
edit_command,
|
|
358
|
+
["Title", "--replace", "日本語タイトル"],
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
assert result.exit_code == 0
|
|
362
|
+
assert track.Title == "日本語タイトル"
|
|
363
|
+
mock_db.session.commit.assert_called_once()
|
|
364
|
+
|
|
365
|
+
@patch("rekordbox_edit.commands.edit.confirm")
|
|
366
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
367
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
368
|
+
def test_wholesale_replace_of_unicode_current_value(
|
|
369
|
+
self, mock_db_class, mock_gfc, mock_confirm, make_djmd_content_item
|
|
370
|
+
):
|
|
371
|
+
"""A track with a unicode title can be replaced wholesale."""
|
|
372
|
+
track = make_djmd_content_item(Title="Ü-Bahn Nights (feat. Ångström)")
|
|
373
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
374
|
+
mock_db_class.return_value = mock_db
|
|
375
|
+
mock_gfc.return_value = mock_result
|
|
376
|
+
mock_confirm.return_value = True
|
|
377
|
+
|
|
378
|
+
from click.testing import CliRunner
|
|
379
|
+
result = CliRunner().invoke(
|
|
380
|
+
edit_command,
|
|
381
|
+
["Title", "--replace", "U-Bahn Nights (feat. Angstrom)"],
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
assert result.exit_code == 0
|
|
385
|
+
assert track.Title == "U-Bahn Nights (feat. Angstrom)"
|
|
386
|
+
mock_db.session.commit.assert_called_once()
|
|
387
|
+
|
|
388
|
+
@patch("rekordbox_edit.commands.edit.confirm")
|
|
389
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
390
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
391
|
+
def test_match_replace_within_unicode_title(
|
|
392
|
+
self, mock_db_class, mock_gfc, mock_confirm, make_djmd_content_item
|
|
393
|
+
):
|
|
394
|
+
"""--match correctly finds and replaces a substring within a unicode title."""
|
|
395
|
+
track = make_djmd_content_item(Title="夜 feat. 山田太郎")
|
|
396
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
397
|
+
mock_db_class.return_value = mock_db
|
|
398
|
+
mock_gfc.return_value = mock_result
|
|
399
|
+
mock_confirm.return_value = True
|
|
400
|
+
|
|
401
|
+
from click.testing import CliRunner
|
|
402
|
+
result = CliRunner().invoke(
|
|
403
|
+
edit_command,
|
|
404
|
+
["Title", "--match", "feat.", "--replace", "ft."],
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
assert result.exit_code == 0
|
|
408
|
+
assert track.Title == "夜 ft. 山田太郎"
|
|
409
|
+
mock_db.session.commit.assert_called_once()
|
|
410
|
+
|
|
411
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
412
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
413
|
+
def test_noop_when_unicode_values_are_equal(
|
|
414
|
+
self, mock_db_class, mock_gfc, make_djmd_content_item
|
|
415
|
+
):
|
|
416
|
+
"""No edit is made when the current unicode value already equals --replace."""
|
|
417
|
+
track = make_djmd_content_item(Title="Ø (Disambiguation)")
|
|
418
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
419
|
+
mock_db_class.return_value = mock_db
|
|
420
|
+
mock_gfc.return_value = mock_result
|
|
421
|
+
|
|
422
|
+
from click.testing import CliRunner
|
|
423
|
+
result = CliRunner().invoke(
|
|
424
|
+
edit_command,
|
|
425
|
+
["Title", "--replace", "Ø (Disambiguation)", "--yes"],
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
assert result.exit_code == 0
|
|
429
|
+
mock_db.session.commit.assert_not_called()
|
|
File without changes
|
|
File without changes
|
{rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.github/actions/commitizen-bump/action.yml
RENAMED
|
File without changes
|
|
File without changes
|
{rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/.github/actions/install/action.yml
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/commands/__init__.py
RENAMED
|
File without changes
|
{rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/commands/convert.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|