Coverage for ghtc/parser.py: 94%

53 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-10-08 10:59 +0000

1from typing import Optional, List, Dict 

2import re 

3from ghtc.models import ( 

4 ConventionalCommitType, 

5 ConventionalCommitFooter, 

6 ConventionalCommitMessage, 

7) 

8 

9 

10TYPE_MAPPINGS: Dict[str, ConventionalCommitType] = { 

11 "feat": ConventionalCommitType.FEAT, 

12 "fix": ConventionalCommitType.FIX, 

13 "build": ConventionalCommitType.BUILD, 

14 "chore": ConventionalCommitType.CHORE, 

15 "ci": ConventionalCommitType.CI, 

16 "docs": ConventionalCommitType.DOCS, 

17 "doc": ConventionalCommitType.DOCS, 

18 "style": ConventionalCommitType.STYLE, 

19 "refactor": ConventionalCommitType.REFACTOR, 

20 "perf": ConventionalCommitType.PERF, 

21 "perfs": ConventionalCommitType.PERF, 

22 "test": ConventionalCommitType.TEST, 

23 "tests": ConventionalCommitType.TEST, 

24} 

25TITLE_REGEX = r"^([a-zA-Z0-9_-]+)(!{0,1})(\([a-zA-Z0-9_-]*\)){0,1}(!{0,1}): (.*)$" 

26TITLE_COMPILED_REGEX = re.compile(TITLE_REGEX) 

27FOOTER_REGEX1 = r"^([a-zA-Z0-9_-]+): (.*)$" 

28FOOTER_COMPILED_REGEX1 = re.compile(FOOTER_REGEX1) 

29FOOTER_REGEX2 = r"^([a-zA-Z0-9_-]+) #(.*)$" 

30FOOTER_COMPILED_REGEX2 = re.compile(FOOTER_REGEX2) 

31BREAKING_CHANGE_FOOTER_REGEX = r"^BREAKING[- ]CHANGE: (.*)$" 

32BREAKING_CHANGE_FOOTER_COMPILED_REGEX = re.compile(BREAKING_CHANGE_FOOTER_REGEX) 

33 

34 

35def type_string_to_commit_type(type_str: str) -> ConventionalCommitType: 

36 if type_str not in TYPE_MAPPINGS: 

37 return ConventionalCommitType.OTHER 

38 return TYPE_MAPPINGS[type_str] 

39 

40 

41def parse(commit_message: str) -> Optional[ConventionalCommitMessage]: 

42 if not commit_message: 

43 return None 

44 lines = commit_message.splitlines() 

45 first_line = lines[0] 

46 match = TITLE_COMPILED_REGEX.match(first_line) 

47 if match is None: 

48 return None 

49 type_str = match[1].lower() 

50 breaking = False 

51 if match[2] or match[4]: 

52 breaking = True 

53 scope = None 

54 if match[3]: 

55 scope = match[3].lower()[1:-1] 

56 description = match[5] 

57 body = None 

58 footers: List[ConventionalCommitFooter] = [] 

59 if len(lines) > 1 and lines[1] == "": 

60 for line in lines[1:]: 

61 if not line: 

62 continue 

63 tmp1 = FOOTER_COMPILED_REGEX1.match(line) 

64 tmp2 = FOOTER_COMPILED_REGEX2.match(line) 

65 tmp3 = BREAKING_CHANGE_FOOTER_COMPILED_REGEX.match(line) 

66 if len(footers) == 0 and tmp1 is None and tmp2 is None and tmp3 is None: 

67 if body is None: 

68 body = f"{line}" 

69 else: 

70 body += f"\n{line}" 

71 else: 

72 if tmp3 is not None: 

73 breaking = True 

74 footers.append(ConventionalCommitFooter("BREAKING CHANGE", tmp3[1])) 

75 elif tmp1 is not None: 

76 footers.append(ConventionalCommitFooter(tmp1[1], tmp1[2])) 

77 elif tmp2 is not None: 

78 footers.append(ConventionalCommitFooter(tmp2[1], tmp2[2])) 

79 return ConventionalCommitMessage( 

80 type=type_string_to_commit_type(type_str), 

81 scope=scope, 

82 body=body, 

83 footers=footers, 

84 description=description, 

85 breaking=breaking, 

86 )