Skip to content

Commit f2c75df

Browse files
committed
[Feature]Manage optional sections
1 parent 7dc57ed commit f2c75df

7 files changed

Lines changed: 196 additions & 20 deletions

File tree

gitlab_codeowners_linter/autofix.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,13 @@ def fix(codeowners_data, violations, file_path):
3232
# and DOCUMENTATION are combined, using the case of the first section
3333
i = 0
3434
while i < len(codeowners_data)-1:
35-
if codeowners_data[i].codeowner_section.lower() == codeowners_data[i+1].codeowner_section.lower():
35+
current_section = codeowners_data[i].codeowner_section.lower()
36+
if current_section.startswith('^'):
37+
current_section = current_section[1:]
38+
next_section = codeowners_data[i+1].codeowner_section.lower()
39+
if next_section.startswith('^'):
40+
next_section = next_section[1:]
41+
if current_section == next_section:
3642
codeowners_data[i].comments = codeowners_data[i].comments + \
3743
codeowners_data[i+1].comments
3844
codeowners_data[i].entries = codeowners_data[i].entries + \
@@ -140,7 +146,7 @@ def _update_codeowners_file(codeowners_data, file_path):
140146
for comment_line in section.comments:
141147
f.write(f'{comment_line}\n')
142148
if section.codeowner_section != DEFAULT_SECTION:
143-
f.write(f'[{section.codeowner_section}]')
149+
f.write(f'{section.codeowner_section}')
144150
if section.entries:
145151
f.write('\n')
146152
for entry in section.entries:

gitlab_codeowners_linter/checks.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ def _is_codeowners_empty(codeowners_data):
8585
def _get_duplicated_sections(codeowners_data):
8686
all_sections_name = list(
8787
section.codeowner_section for section in codeowners_data)
88+
all_sections_name = [x.split('^')[1] if x.startswith(
89+
'^') else x for x in all_sections_name]
8890
seen = set()
8991
return [x for x in all_sections_name if x.lower() in seen or seen.add(x.lower())]
9092

gitlab_codeowners_linter/parser.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def _is_consecutive_blank_line_in_section(codeowners_content):
4444

4545

4646
def parse_codeowners(file_path):
47-
section_regex = re.compile(r'\[(.*?)\]')
47+
section_regex = re.compile(r'(\^)?\[(.*?)\]')
4848

4949
codeowners_content = [CodeownerSection(DEFAULT_SECTION, [], [])]
5050
comments_block = []
@@ -69,20 +69,16 @@ def parse_codeowners(file_path):
6969
comments_block = []
7070
continue
7171
if section_regex.search(line):
72-
# TODO: manage gitlab optional sections, the ones starting with ^
7372
# TODO: at the moment for any section with a following comment like
7473
# [Section]#this is a comment
7574
# the comment is ignored without any message to the user.
7675
# A solution could be to create a function that scans all the parsed lines for
7776
# trailing comments, both on gitlab sections names and on entries, and appends them to
7877
# the proper comment space, CodeownerSection.comments or CodeownerEntry.comments
79-
8078
# Here we have a new section
81-
8279
codeowners_content.append(
8380
CodeownerSection(
84-
section_regex.search(line).group(
85-
1), comments_block, [],
81+
line.split(']', 1)[0]+']', comments_block, [],
8682
),
8783
)
8884
comments_block = []

gitlab_codeowners_linter/sorting.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
import re
4+
35

46
def sort_paths(entry1, entry2):
57
line1 = entry1.path.lower()
@@ -23,8 +25,19 @@ def sort_paths(entry1, entry2):
2325

2426

2527
def sort_section_names(section1, section2):
26-
section_name1 = section1.codeowner_section.lower()
27-
section_name2 = section2.codeowner_section.lower()
28+
section_name1 = re.search(re.compile(
29+
r'\[([^]]*)\]'), section1.codeowner_section).group(1).lower()
30+
section_name2 = re.search(re.compile(
31+
r'\[([^]]*)\]'), section2.codeowner_section).group(1).lower()
32+
is_section_1_optional = section1.codeowner_section.startswith('^')
33+
is_section_2_optional = section2.codeowner_section.startswith('^')
34+
2835
if section_name1 == section_name2:
36+
if is_section_1_optional and is_section_2_optional:
37+
return 0
38+
if is_section_1_optional:
39+
return -1
40+
if is_section_2_optional:
41+
return 1
2942
return 0
3043
return -1 if (section_name1 < section_name2) else 1

tests/codeowners_linter_tests.py

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from gitlab_codeowners_linter.parser import CodeownerEntry
1616
from gitlab_codeowners_linter.parser import CodeownerSection
1717
from gitlab_codeowners_linter.sorting import sort_paths
18+
from gitlab_codeowners_linter.sorting import sort_section_names
1819

1920

2021
class Test_Functions(unittest.TestCase):
@@ -73,7 +74,70 @@ class TestCase:
7374
case.expected_no_autofix,
7475
no_autofix,))
7576

76-
def test_sort_function(self):
77+
def test_sort_sections_function(self):
78+
@dataclass
79+
class TestCase:
80+
name: str
81+
input: list[str]
82+
expected: list[str]
83+
84+
testcases = [
85+
TestCase(
86+
name='unsorted',
87+
input=[
88+
'[BUILD]',
89+
'[SECURITY]',
90+
'[SYSTEM]',
91+
'^[And_a_last_section]',
92+
'^[SYSTEM]',
93+
],
94+
expected=[
95+
'^[And_a_last_section]',
96+
'[BUILD]',
97+
'[SECURITY]',
98+
'^[SYSTEM]',
99+
'[SYSTEM]',
100+
],
101+
),
102+
TestCase(name='empty_slice', input=[], expected=[]),
103+
TestCase(
104+
name='already_sorted',
105+
input=[
106+
'^[And_a_last_section]',
107+
'[BUILD]',
108+
'[SECURITY]',
109+
'^[SYSTEM]',
110+
'[SYSTEM]',
111+
],
112+
expected=[
113+
'^[And_a_last_section]',
114+
'[BUILD]',
115+
'[SECURITY]',
116+
'^[SYSTEM]',
117+
'[SYSTEM]',
118+
],
119+
),
120+
]
121+
sort_section_key = cmp_to_key(sort_section_names)
122+
123+
for case in testcases:
124+
data = []
125+
actual = data
126+
for section in case.input:
127+
data.append(CodeownerSection(section, [], []))
128+
actual = sorted(data, key=sort_section_key)
129+
actual_names = [x.codeowner_section for x in actual]
130+
self.assertListEqual(
131+
case.expected,
132+
actual_names,
133+
'failed test {} expected {}, actual {}'.format(
134+
case.name,
135+
case.expected,
136+
actual_names,
137+
),
138+
)
139+
140+
def test_sort_path_function(self):
77141
@dataclass
78142
class TestCase:
79143
name: str
@@ -189,10 +253,10 @@ class TestCase:
189253
),
190254
expected_check=[
191255
'Sections are not sorted',
192-
'The sections SECURITY, security are duplicates',
193-
'The paths in sections __default_codeowner_section__, Security, SYSTEM, SECURITY are not sorted',
256+
'The sections [SECURITY], [security] are duplicates',
257+
'The paths in sections __default_codeowner_section__, [Security], [SYSTEM], [SECURITY] are not sorted',
194258
'The sections __default_codeowner_section__ have duplicate paths',
195-
'The sections __default_codeowner_section__, Security, SYSTEM, SECURITY, security have non-existing paths'],
259+
'The sections __default_codeowner_section__, [Security], [SYSTEM], [SECURITY], [security] have non-existing paths'],
196260
expected_fix=os.path.join(
197261
os.path.dirname(os.path.abspath(__file__)),
198262
'resources/existing_paths_autofix.txt',
@@ -268,8 +332,8 @@ class TestCase:
268332
),
269333
expected_check=[
270334
'Sections are not sorted',
271-
'There are blank lines in the sections __default_codeowner_section__, BUILD, SECURITY',
272-
'The paths in sections __default_codeowner_section__, BUILD, SYSTEM, TEST_SECTION are not sorted',
335+
'There are blank lines in the sections __default_codeowner_section__, [BUILD], [SECURITY]',
336+
'The paths in sections __default_codeowner_section__, [BUILD], [SYSTEM], [TEST_SECTION] are not sorted',
273337
'The sections __default_codeowner_section__ have duplicate paths',
274338
],
275339
expected_fix=os.path.join(
@@ -285,16 +349,32 @@ class TestCase:
285349
),
286350
expected_check=[
287351
'Sections are not sorted',
288-
'The sections SECTION_NAME, section_name, Section_Name are duplicates',
289-
'There are blank lines in the sections Section_name, BUILD, SECURITY',
290-
'The paths in sections Section_name, BUILD, SYSTEM, SECTION_NAME, TEST_SECTION are not sorted',
291-
'The sections Section_name, SECTION_NAME have duplicate paths',
352+
'The sections [SECTION_NAME], [section_name], [Section_Name] are duplicates',
353+
'There are blank lines in the sections [Section_name], [BUILD], [SECURITY]',
354+
'The paths in sections [Section_name], [BUILD], [SYSTEM], [SECTION_NAME], [TEST_SECTION] are not sorted',
355+
'The sections [Section_name], [SECTION_NAME] have duplicate paths',
292356
],
293357
expected_fix=os.path.join(
294358
os.path.dirname(os.path.abspath(__file__)),
295359
'resources/no_default_section_autofix.txt',
296360
),
297361
),
362+
TestCase(
363+
name='optional_sections_not_formatted',
364+
input=os.path.join(
365+
os.path.dirname(os.path.abspath(__file__)),
366+
'resources/optional_sections_input.txt',
367+
),
368+
expected_check=[
369+
'Sections are not sorted',
370+
'The sections [System], [SYSTEM], [SYSTEM] are duplicates',
371+
372+
],
373+
expected_fix=os.path.join(
374+
os.path.dirname(os.path.abspath(__file__)),
375+
'resources/optional_sections_autofix.txt',
376+
),
377+
),
298378
TestCase(
299379
name='empty_file',
300380
input=os.path.join(
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
### CODEOWNERS ###
2+
#
3+
# This is a test case with issues and optional sections
4+
#
5+
6+
# this is a comment for * test@email.com test1@email.com test2@email.com
7+
* test@email.com test1@email.com test2@email.com
8+
*.md test@email.com
9+
WORKSPACE test@email.com test1@email.com test2@email.com test3@email.com
10+
/.pylintrc test@email.com
11+
# this is a comment for /ui test@email.com
12+
/ui test@email.com
13+
/ui/components/ test@email.com test1@email.com test2@email.com test3@email.com
14+
/ui/lighting test@email.com test1@email.com test2@email.com test3@email.com
15+
16+
^[And_a_last_section]
17+
/pipeline/ test@email.com test1@email.com test2@email.com test3@email.com
18+
/release/release_script.sh test@email.com
19+
20+
# Gitlab Groups
21+
[BUILD]
22+
.gitlab/.gitlab-ci.yml test@email.com
23+
.gitlab/ci/ test@email.com test1@email.com test2@email.com test3@email.com
24+
25+
[SECURITY]
26+
/ops/terraform/path1 @test/teams/admin @test/teams/security/admin
27+
/ops/terraform/path2/ @test/teams/security/admin
28+
29+
# This is a comment for [SYSTEM]
30+
^[system]
31+
/docker/pipeline/ test@email.com test1@email.com test2@email.com test3@email.com
32+
/go/src/repo/pipeline/ test@email.com test1@email.com test2@email.com test3@email.com
33+
/ops/terraform/pipeline/ test@email.com
34+
/pipeline/ test1@email.com test2@email.com test3@email.com test@email.com
35+
/release/release_script.sh test@email.com
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
### CODEOWNERS ###
2+
#
3+
# This is a test case with issues and optional sections
4+
#
5+
6+
# this is a comment for * test@email.com test1@email.com test2@email.com
7+
* test@email.com test1@email.com test2@email.com
8+
*.md test@email.com
9+
WORKSPACE test@email.com test1@email.com test2@email.com test3@email.com
10+
/.pylintrc test@email.com
11+
# this is a comment for /ui test@email.com
12+
/ui test@email.com
13+
/ui/components/ test@email.com test1@email.com test2@email.com test3@email.com
14+
/ui/lighting test@email.com test1@email.com test2@email.com test3@email.com
15+
16+
# Gitlab Groups
17+
[BUILD]
18+
.gitlab/.gitlab-ci.yml test@email.com
19+
.gitlab/ci/ test@email.com test1@email.com test2@email.com test3@email.com
20+
21+
^[system]
22+
/pipeline/ test@email.com test1@email.com test2@email.com test3@email.com
23+
24+
[SECURITY]
25+
/ops/terraform/path1 @test/teams/admin @test/teams/security/admin
26+
/ops/terraform/path2/ @test/teams/security/admin
27+
28+
^[System]
29+
/pipeline/ test@email.com test1@email.com test2@email.com test3@email.com
30+
/release/release_script.sh test@email.com
31+
32+
# This is a comment for [SYSTEM]
33+
[SYSTEM]
34+
/docker/pipeline/ test@email.com test1@email.com test2@email.com test3@email.com
35+
/go/src/repo/pipeline/ test@email.com test1@email.com test2@email.com test3@email.com
36+
/ops/terraform/pipeline/ test@email.com
37+
38+
^[And_a_last_section]
39+
/pipeline/ test@email.com test1@email.com test2@email.com test3@email.com
40+
/release/release_script.sh test@email.com
41+
42+
^[SYSTEM]
43+
/pipeline/ test@email.com test1@email.com test2@email.com test3@email.com
44+
/release/release_script.sh test@email.com

0 commit comments

Comments
 (0)