Get diff from last stable build, not just previous build
[akaros.git] / tools / jenkins / utils / changes.py
1 #!/usr/bin/python
2 """Parses a 'git diff --stat' file to extract what files have been changed as
3 shown in the diff. Extracts the directory paths of those files, and decides
4 which components of AKAROS should be compiled and tested, accordingly.
5 """
6 import json
7 import os
8 import re
9 import sys
10
11 REGEX_EXTRACT_PATH_FROM_GIT_DIFF_LINE = r'^(?:\s)*([^\s]*)(?:\s)*\|(?:.*)\n?$'
12 # Path to file with git diff
13 DIFF_FILE = sys.argv[1]
14 # Path to file with JSON definition of akaros components
15 CONFIG_COMP_COMPONENTS_FILE = sys.argv[2]
16
17 # Arguments to fill variable paths with. Useful, for example, to define a path
18 # that will vary whether we are compiling an architecture or another.
19 # TODO(alfongj): Get these from Env var or sys.argv
20
21
22 """The following is the definition of the given components of AKAROS (filled in
23 from CONFIG_COMP_COMPONENTS_FILE (which should be 
24 config/compilation_components.json or something).
25
26 Each 'component' consists of a name (which will be a unique identifier printed
27 out to console) as key, plus a list of PATHS as content. If any of these paths
28 is modified, then we will consider that the full component is affected, and
29 therefore may need to be compiled and tested again.
30
31 Paths should be written from the root repo folder, beginning with './' and not
32 '/', and ending in '/'.
33
34 If a path should include any subpaths, then it must be appended with a + symbol.
35 e.g. ./path/with/subpaths/+
36
37 If a path has variable arguments, they should be represented like {{this}}.
38 These arguments will be filled in with the contents of PATH_ARGS.
39 e.g. ./path/with/{{VARIABLE}}/{{ARGUMENTS}}/. 
40 """
41 akaros_components = {}
42
43 affected_components = {}
44
45 def get_variable_path_args() :
46         """Returns dict of arguments to use in the load_component_config function
47         to generate dynamic paths. Currently it is only being used to change a 
48         subdirectory in one of the paths depending on the architecture being 
49         tested.
50         """
51         PATH_ARGS = {
52                 "I686": {
53                         "arch": "x86"
54                 },
55                 "X86_64": {
56                         "arch": "x86"
57                 },
58                 "RISCV": {
59                         "arch": "riscv"
60                 }
61         }
62         compilation_arch = os.getenv('COMPILATION_ARCH', 'I686')
63         return PATH_ARGS[compilation_arch]
64
65 def load_component_config() :
66         """Loads ../config/compilation_components.json object, which contains a
67         list of all the different AKAROS compilation components along with the paths
68         to look for for compiling them.
69         """
70         conf_file_contents = ""
71         # Read config file.
72         with open(CONFIG_COMP_COMPONENTS_FILE, 'r') as conf_file :
73                 conf_file_contents = conf_file.read().replace('\n', '')
74
75         # Replace variable arguments.
76         var_path_args = get_variable_path_args()
77         for arg in var_path_args :
78                 wrapped_arg = "{{" + arg + "}}"
79                 conf_file_contents = conf_file_contents.replace(wrapped_arg, 
80                                                                 var_path_args[arg])
81
82         # Parse JSON into python object.
83         global akaros_components
84         akaros_components = json.loads(conf_file_contents)['compilation_components']
85
86 def extract_dir(diff_line) :
87         """Given a line from a "git diff --stat" output, it tries to extract a 
88         directory from it. 
89
90         If a blank or non-change line is passed, it ignores it and returns nothing.
91
92         If a 'change' line (e.g. ' path/to/file.ext  |  33++ ') is passed, it strips
93         the path (not including the file name) and prepends a './' to it and returns
94         it.
95         """
96         match = re.match(REGEX_EXTRACT_PATH_FROM_GIT_DIFF_LINE, diff_line) 
97         if (match) :
98                 full_path = './' + match.group(1)
99                 folder_list = full_path.split('/')[0:-1]
100                 folder_path = '/'.join(folder_list) + '/'
101                 return folder_path
102
103 def includes_subpaths(path) :
104         """Checks if a given path includes subpaths or not. It includes them if it
105         ends in a '+' symbol.
106         """
107         return path[-1] == '+'
108
109 def check_components_affected(path_of_changed_file) :
110         """Checks if a given directory should set the state of one of the components
111         to affected.
112         """
113         global affected_components
114         for component in akaros_components :
115                 affected = component in affected_components
116                 paths = akaros_components[component]['PATHS']
117                 if (not affected) :
118                         for path in paths :
119                                 if (includes_subpaths(path)) :
120                                         # Checks if a given string contains the given path.
121                                         # e.g., If the path is 'path/to':
122                                                 # The regex will match for: 'path/to/file.txt' or 
123                                                         # 'path/to/and/subpath/to/file.txt'
124                                                 # But not for: 'path/file.txt' nor for 'path/tofile.txt' or
125                                                         # 'path/tobananas/file.txt'
126                                         regex = re.compile('^\%s(?:.*/)*$' % path[:-1])
127                                 else :
128                                         # Checks if a given string contains the given path with no 
129                                         # subpaths.
130                                         # e.g., If the path is 'path/to':
131                                                 # The regex will match for: 'path/to/file.txt'
132                                                 # But not for: 'path/file.txt' nor for 'path/tofile.txt' or
133                                                         # 'path/tobananas/file.txt' or 
134                                                         # 'path/to/and/subpath/to/file.txt'
135                                         regex = re.compile('^\%s[^/]*$' % path)
136
137                                 if (re.match(regex, path_of_changed_file)) :
138                                         affected_components[component] = True
139                                         break
140
141 def main() :
142         load_component_config()
143         diff_file = open(DIFF_FILE)
144         for line in diff_file :
145                 cur_dir = extract_dir(line)
146                 if (cur_dir) :
147                         check_components_affected(cur_dir)
148
149         print ' '.join(affected_components)
150
151 main()