xref: /aosp_15_r20/tools/aadevtools/dev/change_report.py (revision b32fbb6340ee6fe4a25d7b39d84085c084465677)
1*b32fbb63SXin Li#!/usr/bin/python3
2*b32fbb63SXin Li#
3*b32fbb63SXin Li# Copyright (C) 2021 The Android Open Source Project
4*b32fbb63SXin Li#
5*b32fbb63SXin Li# Licensed under the Apache License, Version 2.0 (the "License");
6*b32fbb63SXin Li# you may not use this file except in compliance with the License.
7*b32fbb63SXin Li# You may obtain a copy of the License at
8*b32fbb63SXin Li#
9*b32fbb63SXin Li#      http://www.apache.org/licenses/LICENSE-2.0
10*b32fbb63SXin Li#
11*b32fbb63SXin Li# Unless required by applicable law or agreed to in writing, software
12*b32fbb63SXin Li# distributed under the License is distributed on an "AS IS" BASIS,
13*b32fbb63SXin Li# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14*b32fbb63SXin Li# See the License for the specific language governing permissions and
15*b32fbb63SXin Li# limitations under the License.
16*b32fbb63SXin Li"""Utilities for comparing two version of a codebase."""
17*b32fbb63SXin Li
18*b32fbb63SXin Liimport argparse
19*b32fbb63SXin Liimport difflib
20*b32fbb63SXin Liimport filecmp
21*b32fbb63SXin Liimport os
22*b32fbb63SXin Liimport pathlib
23*b32fbb63SXin Liimport re
24*b32fbb63SXin Li
25*b32fbb63SXin Li
26*b32fbb63SXin Liclass FileStat:
27*b32fbb63SXin Li  """File statistics class for a file."""
28*b32fbb63SXin Li
29*b32fbb63SXin Li  NON_TEXT = 0
30*b32fbb63SXin Li  TEXT = 1
31*b32fbb63SXin Li
32*b32fbb63SXin Li  def __init__(self, file_path):
33*b32fbb63SXin Li    """Initializes with a file path string."""
34*b32fbb63SXin Li    if file_path:
35*b32fbb63SXin Li      self.file_name = str(file_path)
36*b32fbb63SXin Li      self.size = file_path.stat().st_size
37*b32fbb63SXin Li    else:
38*b32fbb63SXin Li      self.file_name = ''
39*b32fbb63SXin Li      self.size = 0
40*b32fbb63SXin Li
41*b32fbb63SXin Li    self.line_cnt = 0
42*b32fbb63SXin Li    self.group_cnt = 0
43*b32fbb63SXin Li    self.add_line_cnt = 0
44*b32fbb63SXin Li    self.remove_line_cnt = 0
45*b32fbb63SXin Li    self.replace_line_cnt = 0
46*b32fbb63SXin Li
47*b32fbb63SXin Li  @staticmethod
48*b32fbb63SXin Li  def get_csv_header(prefix=None):
49*b32fbb63SXin Li    """Returns CSV header string."""
50*b32fbb63SXin Li    cols = ['file', 'size', 'line', 'group', 'add', 'remove', 'replace']
51*b32fbb63SXin Li    if prefix:
52*b32fbb63SXin Li      return ','.join('{0}_{1}'.format(prefix, c) for c in cols)
53*b32fbb63SXin Li    else:
54*b32fbb63SXin Li      return ','.join(c for c in cols)
55*b32fbb63SXin Li
56*b32fbb63SXin Li  def get_csv_str(self, strip_dir_len=0):
57*b32fbb63SXin Li    """Returns the file statistic CSV string."""
58*b32fbb63SXin Li    name = self.file_name[strip_dir_len:]
59*b32fbb63SXin Li    csv = [
60*b32fbb63SXin Li        FileStat.no_comma(name), self.size, self.line_cnt, self.group_cnt,
61*b32fbb63SXin Li        self.add_line_cnt, self.remove_line_cnt, self.replace_line_cnt
62*b32fbb63SXin Li    ]
63*b32fbb63SXin Li    return ','.join(str(i) for i in csv)
64*b32fbb63SXin Li
65*b32fbb63SXin Li  @staticmethod
66*b32fbb63SXin Li  def no_comma(astr):
67*b32fbb63SXin Li    """Replaces , with _."""
68*b32fbb63SXin Li    return astr.replace(',', '_')
69*b32fbb63SXin Li
70*b32fbb63SXin Li
71*b32fbb63SXin Liclass DiffStat:
72*b32fbb63SXin Li  """Diff statistic class for 2 versions of a file."""
73*b32fbb63SXin Li
74*b32fbb63SXin Li  SAME = 0
75*b32fbb63SXin Li  NEW = 1
76*b32fbb63SXin Li  REMOVED = 2
77*b32fbb63SXin Li  MODIFIED = 3
78*b32fbb63SXin Li  INCOMPARABLE = 4
79*b32fbb63SXin Li
80*b32fbb63SXin Li  def __init__(self, common_name, old_file_stat, new_file_stat, state):
81*b32fbb63SXin Li    """Initializes with the common names & etc."""
82*b32fbb63SXin Li    self.old_file_stat = old_file_stat
83*b32fbb63SXin Li    self.new_file_stat = new_file_stat
84*b32fbb63SXin Li    self.name = common_name
85*b32fbb63SXin Li    self.ext = os.path.splitext(self.name)[1].lstrip('.')
86*b32fbb63SXin Li    self.state = state
87*b32fbb63SXin Li    self.file_type = FileStat.NON_TEXT
88*b32fbb63SXin Li
89*b32fbb63SXin Li  def add_diff_stat(self, diff_lines):
90*b32fbb63SXin Li    """Adds the statistic by the diff lines."""
91*b32fbb63SXin Li    # These align with https://github.com/python/cpython/blob/3.9/Lib/difflib.py
92*b32fbb63SXin Li    old_pattern = re.compile(r'\*{3} (.*)')
93*b32fbb63SXin Li    new_pattern = re.compile(r'-{3} (.*)')
94*b32fbb63SXin Li    group_separator = '***************'
95*b32fbb63SXin Li    old_group_header = re.compile(r'\*{3} (\d*),(\d*) \*{4}')
96*b32fbb63SXin Li    new_group_header = re.compile(r'-{3} (\d*),(\d*) -{4}')
97*b32fbb63SXin Li
98*b32fbb63SXin Li    # section 0 is old verion & 1 is new verion
99*b32fbb63SXin Li    section = -1
100*b32fbb63SXin Li    diff_stats = [self.old_file_stat, self.new_file_stat]
101*b32fbb63SXin Li    in_group = False
102*b32fbb63SXin Li
103*b32fbb63SXin Li    h1m = old_pattern.match(diff_lines[0])
104*b32fbb63SXin Li    if not h1m:
105*b32fbb63SXin Li      print('ERROR: wrong diff header line 1: %s' % diff_lines[0])
106*b32fbb63SXin Li      return
107*b32fbb63SXin Li
108*b32fbb63SXin Li    h2m = new_pattern.match(diff_lines[1])
109*b32fbb63SXin Li    if not h2m:
110*b32fbb63SXin Li      print('ERROR: wrong diff header line 2: %s' % diff_lines[1])
111*b32fbb63SXin Li      return
112*b32fbb63SXin Li
113*b32fbb63SXin Li    for line in diff_lines[2:]:
114*b32fbb63SXin Li      if in_group:
115*b32fbb63SXin Li        if line.startswith('  '):
116*b32fbb63SXin Li          # equal
117*b32fbb63SXin Li          continue
118*b32fbb63SXin Li        elif line.startswith('! '):
119*b32fbb63SXin Li          # replace
120*b32fbb63SXin Li          diff_stats[section].replace_line_cnt += 1
121*b32fbb63SXin Li          continue
122*b32fbb63SXin Li        elif line.startswith('+ '):
123*b32fbb63SXin Li          # add
124*b32fbb63SXin Li          diff_stats[section].add_line_cnt += 1
125*b32fbb63SXin Li          continue
126*b32fbb63SXin Li        elif line.startswith('- '):
127*b32fbb63SXin Li          # removed
128*b32fbb63SXin Li          diff_stats[section].remove_line_cnt += 1
129*b32fbb63SXin Li          continue
130*b32fbb63SXin Li
131*b32fbb63SXin Li      oghm = old_group_header.match(line)
132*b32fbb63SXin Li      if oghm:
133*b32fbb63SXin Li        section = 0
134*b32fbb63SXin Li        diff_stats[section].group_cnt += 1
135*b32fbb63SXin Li        continue
136*b32fbb63SXin Li
137*b32fbb63SXin Li      nghm = new_group_header.match(line)
138*b32fbb63SXin Li      if nghm:
139*b32fbb63SXin Li        section = 1
140*b32fbb63SXin Li        diff_stats[section].group_cnt += 1
141*b32fbb63SXin Li        continue
142*b32fbb63SXin Li
143*b32fbb63SXin Li      if line.startswith(group_separator):
144*b32fbb63SXin Li        in_group = True
145*b32fbb63SXin Li        continue
146*b32fbb63SXin Li
147*b32fbb63SXin Li
148*b32fbb63SXin Liclass ChangeReport:
149*b32fbb63SXin Li  """Change report class for the diff statistics on 2 versions of a codebase.
150*b32fbb63SXin Li
151*b32fbb63SXin Li  Attributes:
152*b32fbb63SXin Li    old_dir: The old codebase dir path string.
153*b32fbb63SXin Li    new_dir: The new codebase dir path string.
154*b32fbb63SXin Li    dircmp: The dircmp object
155*b32fbb63SXin Li    group_cnt: How many diff groups.
156*b32fbb63SXin Li    add_line_cnt: How many lines are added.
157*b32fbb63SXin Li    remove_line_cnt: How many lines are removed.
158*b32fbb63SXin Li    replace_line_cnt: Hoe many lines are changed.
159*b32fbb63SXin Li  """
160*b32fbb63SXin Li
161*b32fbb63SXin Li  def __init__(self, old_dir, new_dir, ignores=None, state_filter=None):
162*b32fbb63SXin Li    """Initializes with old & new dir path strings."""
163*b32fbb63SXin Li    self.old_dir = os.path.abspath(old_dir)
164*b32fbb63SXin Li    self._old_dir_prefix_len = len(self.old_dir) + 1
165*b32fbb63SXin Li    self.new_dir = os.path.abspath(new_dir)
166*b32fbb63SXin Li    self._new_dir_prefix_len = len(self.new_dir) + 1
167*b32fbb63SXin Li    if ignores:
168*b32fbb63SXin Li      self._ignores = ignores.split(',')
169*b32fbb63SXin Li      self._ignores.extend(filecmp.DEFAULT_IGNORES)
170*b32fbb63SXin Li    else:
171*b32fbb63SXin Li      self._ignores = filecmp.DEFAULT_IGNORES
172*b32fbb63SXin Li
173*b32fbb63SXin Li    if state_filter:
174*b32fbb63SXin Li      self._state_filter = list(map(int, state_filter.split(',')))
175*b32fbb63SXin Li    else:
176*b32fbb63SXin Li      self._state_filter = [0, 1, 2, 3, 4]
177*b32fbb63SXin Li
178*b32fbb63SXin Li    self._do_same = DiffStat.SAME in self._state_filter
179*b32fbb63SXin Li    self._do_new = DiffStat.NEW in self._state_filter
180*b32fbb63SXin Li    self._do_removed = DiffStat.REMOVED in self._state_filter
181*b32fbb63SXin Li    self._do_moeified = DiffStat.MODIFIED in self._state_filter
182*b32fbb63SXin Li    self._do_incomparable = DiffStat.INCOMPARABLE in self._state_filter
183*b32fbb63SXin Li
184*b32fbb63SXin Li    self.dircmp = filecmp.dircmp(
185*b32fbb63SXin Li        self.old_dir, self.new_dir, ignore=self._ignores)
186*b32fbb63SXin Li    self._diff_stats = []
187*b32fbb63SXin Li    self._diff_stat_lines = []
188*b32fbb63SXin Li    self._diff_lines = []
189*b32fbb63SXin Li    self._processed_cnt = 0
190*b32fbb63SXin Li    self._common_dir_len = ChangeReport.get_common_path_len(
191*b32fbb63SXin Li        self.old_dir, self.new_dir)
192*b32fbb63SXin Li
193*b32fbb63SXin Li  @staticmethod
194*b32fbb63SXin Li  def get_common_path_len(dir1, dir2):
195*b32fbb63SXin Li    """Gets the length of the common path of old & new folders."""
196*b32fbb63SXin Li    sep = os.path.sep
197*b32fbb63SXin Li    last_sep_pos = 0
198*b32fbb63SXin Li    for i in range(len(dir1)):
199*b32fbb63SXin Li      if dir1[i] == sep:
200*b32fbb63SXin Li        last_sep_pos = i
201*b32fbb63SXin Li      if dir1[i] != dir2[i]:
202*b32fbb63SXin Li        break
203*b32fbb63SXin Li    return last_sep_pos + 1
204*b32fbb63SXin Li
205*b32fbb63SXin Li  @staticmethod
206*b32fbb63SXin Li  def get_diff_stat_header():
207*b32fbb63SXin Li    """Gets the diff statistic CSV header."""
208*b32fbb63SXin Li    return 'file,ext,text,state,{0},{1}\n'.format(
209*b32fbb63SXin Li        FileStat.get_csv_header('new'), FileStat.get_csv_header('old'))
210*b32fbb63SXin Li
211*b32fbb63SXin Li  def get_diff_stat_lines(self):
212*b32fbb63SXin Li    """Gets the diff statistic CSV lines."""
213*b32fbb63SXin Li    if self._processed_cnt < 1:
214*b32fbb63SXin Li      self._process_dircmp(self.dircmp)
215*b32fbb63SXin Li      self._processed_cnt += 1
216*b32fbb63SXin Li
217*b32fbb63SXin Li      self._diff_stat_lines = []
218*b32fbb63SXin Li      for diff_stat in self._diff_stats:
219*b32fbb63SXin Li        self._diff_stat_lines.append('{0},{1},{2},{3},{4},{5}\n'.format(
220*b32fbb63SXin Li            FileStat.no_comma(diff_stat.name), diff_stat.ext,
221*b32fbb63SXin Li            diff_stat.file_type, diff_stat.state,
222*b32fbb63SXin Li            diff_stat.new_file_stat.get_csv_str(self._common_dir_len),
223*b32fbb63SXin Li            diff_stat.old_file_stat.get_csv_str(self._common_dir_len)))
224*b32fbb63SXin Li
225*b32fbb63SXin Li    return self._diff_stat_lines
226*b32fbb63SXin Li
227*b32fbb63SXin Li  def get_diff_lines(self):
228*b32fbb63SXin Li    """Gets the diff output lines."""
229*b32fbb63SXin Li    if self._processed_cnt < 1:
230*b32fbb63SXin Li      self._process_dircmp(self.dircmp)
231*b32fbb63SXin Li      self._processed_cnt += 1
232*b32fbb63SXin Li    return self._diff_lines
233*b32fbb63SXin Li
234*b32fbb63SXin Li  def _process_dircmp(self, dircmp):
235*b32fbb63SXin Li    """Compare all files in a dircmp object for diff statstics & output."""
236*b32fbb63SXin Li    if self._do_moeified:
237*b32fbb63SXin Li      self._process_diff_files(dircmp)
238*b32fbb63SXin Li
239*b32fbb63SXin Li    for subdir_dircmp in dircmp.subdirs.values():
240*b32fbb63SXin Li      rp = pathlib.Path(subdir_dircmp.right)
241*b32fbb63SXin Li      lp = pathlib.Path(subdir_dircmp.left)
242*b32fbb63SXin Li      if rp.is_symlink() or lp.is_symlink():
243*b32fbb63SXin Li        print('SKIP: symlink: {0} or {1}'.format(subdir_dircmp.right,
244*b32fbb63SXin Li                                                 subdir_dircmp.left))
245*b32fbb63SXin Li        continue
246*b32fbb63SXin Li      self._process_dircmp(subdir_dircmp)
247*b32fbb63SXin Li
248*b32fbb63SXin Li    if self._do_new:
249*b32fbb63SXin Li      self._process_others(dircmp.right_only, dircmp.right,
250*b32fbb63SXin Li                           self._new_dir_prefix_len, DiffStat.NEW)
251*b32fbb63SXin Li    if self._do_same:
252*b32fbb63SXin Li      self._process_others(dircmp.same_files, dircmp.right,
253*b32fbb63SXin Li                           self._new_dir_prefix_len, DiffStat.SAME)
254*b32fbb63SXin Li    if self._do_incomparable:
255*b32fbb63SXin Li      self._process_others(dircmp.funny_files, dircmp.right,
256*b32fbb63SXin Li                           self._new_dir_prefix_len, DiffStat.INCOMPARABLE)
257*b32fbb63SXin Li    if self._do_removed:
258*b32fbb63SXin Li      self._process_others(dircmp.left_only, dircmp.left,
259*b32fbb63SXin Li                           self._old_dir_prefix_len, DiffStat.REMOVED)
260*b32fbb63SXin Li
261*b32fbb63SXin Li  def _process_others(self, files, adir, prefix_len, state):
262*b32fbb63SXin Li    """Processes files are not modified."""
263*b32fbb63SXin Li    empty_stat = FileStat(None)
264*b32fbb63SXin Li    for file in files:
265*b32fbb63SXin Li      file_path = pathlib.Path(adir, file)
266*b32fbb63SXin Li      if file_path.is_symlink():
267*b32fbb63SXin Li        print('SKIP: symlink: {0}, {1}'.format(state, file_path))
268*b32fbb63SXin Li        continue
269*b32fbb63SXin Li      elif file_path.is_dir():
270*b32fbb63SXin Li        flist = self._get_filtered_files(file_path)
271*b32fbb63SXin Li        self._process_others(flist, adir, prefix_len, state)
272*b32fbb63SXin Li      else:
273*b32fbb63SXin Li        file_stat = FileStat(file_path)
274*b32fbb63SXin Li        common_name = str(file_path)[prefix_len:]
275*b32fbb63SXin Li        if state == DiffStat.REMOVED:
276*b32fbb63SXin Li          diff_stat = DiffStat(common_name, file_stat, empty_stat, state)
277*b32fbb63SXin Li        else:
278*b32fbb63SXin Li          diff_stat = DiffStat(common_name, empty_stat, file_stat, state)
279*b32fbb63SXin Li        try:
280*b32fbb63SXin Li          with open(file_path, encoding='utf-8') as f:
281*b32fbb63SXin Li            lines = f.readlines()
282*b32fbb63SXin Li          file_stat.line_cnt = len(lines)
283*b32fbb63SXin Li          file_type = FileStat.TEXT
284*b32fbb63SXin Li        except UnicodeDecodeError:
285*b32fbb63SXin Li          file_type = FileStat.NON_TEXT
286*b32fbb63SXin Li
287*b32fbb63SXin Li        diff_stat.file_type = file_type
288*b32fbb63SXin Li        self._diff_stats.append(diff_stat)
289*b32fbb63SXin Li
290*b32fbb63SXin Li  def _process_diff_files(self, dircmp):
291*b32fbb63SXin Li    """Processes files are modified."""
292*b32fbb63SXin Li    for file in dircmp.diff_files:
293*b32fbb63SXin Li      old_file_path = pathlib.Path(dircmp.left, file)
294*b32fbb63SXin Li      new_file_path = pathlib.Path(dircmp.right, file)
295*b32fbb63SXin Li      self._diff_files(old_file_path, new_file_path)
296*b32fbb63SXin Li
297*b32fbb63SXin Li  def _diff_files(self, old_file_path, new_file_path):
298*b32fbb63SXin Li    """Diff old & new files."""
299*b32fbb63SXin Li    old_file_stat = FileStat(old_file_path)
300*b32fbb63SXin Li    new_file_stat = FileStat(new_file_path)
301*b32fbb63SXin Li    common_name = str(new_file_path)[self._new_dir_prefix_len:]
302*b32fbb63SXin Li    diff_stat = DiffStat(common_name, old_file_stat, new_file_stat,
303*b32fbb63SXin Li                         DiffStat.MODIFIED)
304*b32fbb63SXin Li
305*b32fbb63SXin Li    try:
306*b32fbb63SXin Li      with open(old_file_path, encoding='utf-8') as f1:
307*b32fbb63SXin Li        old_lines = f1.readlines()
308*b32fbb63SXin Li      old_file_stat.line_cnt = len(old_lines)
309*b32fbb63SXin Li      with open(new_file_path, encoding='utf-8') as f2:
310*b32fbb63SXin Li        new_lines = f2.readlines()
311*b32fbb63SXin Li      new_file_stat.line_cnt = len(new_lines)
312*b32fbb63SXin Li      diff_lines = list(
313*b32fbb63SXin Li          difflib.context_diff(old_lines, new_lines, old_file_path.name,
314*b32fbb63SXin Li                               new_file_path.name))
315*b32fbb63SXin Li      file_type = FileStat.TEXT
316*b32fbb63SXin Li      if diff_lines:
317*b32fbb63SXin Li        self._diff_lines.extend(diff_lines)
318*b32fbb63SXin Li        diff_stat.add_diff_stat(diff_lines)
319*b32fbb63SXin Li      else:
320*b32fbb63SXin Li        print('WARNING: no diff lines on {0} {1}'.format(
321*b32fbb63SXin Li            old_file_path, new_file_path))
322*b32fbb63SXin Li
323*b32fbb63SXin Li    except UnicodeDecodeError:
324*b32fbb63SXin Li      file_type = FileStat.NON_TEXT
325*b32fbb63SXin Li
326*b32fbb63SXin Li    diff_stat.file_type = file_type
327*b32fbb63SXin Li    self._diff_stats.append(diff_stat)
328*b32fbb63SXin Li
329*b32fbb63SXin Li  def _get_filtered_files(self, dir_path):
330*b32fbb63SXin Li    """Returns a filtered file list."""
331*b32fbb63SXin Li    flist = []
332*b32fbb63SXin Li    for f in dir_path.glob('*'):
333*b32fbb63SXin Li      if f.name not in self._ignores:
334*b32fbb63SXin Li        if f.is_symlink():
335*b32fbb63SXin Li          print('SKIP: symlink: %s' % f)
336*b32fbb63SXin Li          continue
337*b32fbb63SXin Li        else:
338*b32fbb63SXin Li          flist.append(f)
339*b32fbb63SXin Li    return flist
340*b32fbb63SXin Li
341*b32fbb63SXin Li
342*b32fbb63SXin Lidef write_file(file, lines, header=None):
343*b32fbb63SXin Li  """Write lines into a file."""
344*b32fbb63SXin Li
345*b32fbb63SXin Li  with open(file, 'w') as f:
346*b32fbb63SXin Li    if header:
347*b32fbb63SXin Li      f.write(header)
348*b32fbb63SXin Li
349*b32fbb63SXin Li    f.writelines(lines)
350*b32fbb63SXin Li  print('OUTPUT: {0}, {1} lines'.format(file, len(lines)))
351*b32fbb63SXin Li
352*b32fbb63SXin Li
353*b32fbb63SXin Lidef main():
354*b32fbb63SXin Li  parser = argparse.ArgumentParser(
355*b32fbb63SXin Li      'Generate a diff stat cvs file for 2 versions of a codebase')
356*b32fbb63SXin Li  parser.add_argument('--old_dir', help='the old version codebase dir')
357*b32fbb63SXin Li  parser.add_argument('--new_dir', help='the new version codebase dir')
358*b32fbb63SXin Li  parser.add_argument(
359*b32fbb63SXin Li      '--csv_file', required=False, help='the diff stat cvs file if to create')
360*b32fbb63SXin Li  parser.add_argument(
361*b32fbb63SXin Li      '--diff_output_file',
362*b32fbb63SXin Li      required=False,
363*b32fbb63SXin Li      help='the diff output file if to create')
364*b32fbb63SXin Li  parser.add_argument(
365*b32fbb63SXin Li      '--ignores',
366*b32fbb63SXin Li      required=False,
367*b32fbb63SXin Li      default='.repo,.git,.github,.idea,__MACOSX,.prebuilt_info',
368*b32fbb63SXin Li      help='names to ignore')
369*b32fbb63SXin Li  parser.add_argument(
370*b32fbb63SXin Li      '--state_filter',
371*b32fbb63SXin Li      required=False,
372*b32fbb63SXin Li      default='1,2,3',
373*b32fbb63SXin Li      help='csv diff states to process, 0:SAME, 1:NEW, 2:REMOVED, 3:MODIFIED, '
374*b32fbb63SXin Li      '4:INCOMPARABLE')
375*b32fbb63SXin Li
376*b32fbb63SXin Li  args = parser.parse_args()
377*b32fbb63SXin Li
378*b32fbb63SXin Li  if not os.path.isdir(args.old_dir):
379*b32fbb63SXin Li    print('ERROR: %s does not exist.' % args.old_dir)
380*b32fbb63SXin Li    exit()
381*b32fbb63SXin Li
382*b32fbb63SXin Li  if not os.path.isdir(args.new_dir):
383*b32fbb63SXin Li    print('ERROR: %s does not exist.' % args.new_dir)
384*b32fbb63SXin Li    exit()
385*b32fbb63SXin Li
386*b32fbb63SXin Li  change_report = ChangeReport(args.old_dir, args.new_dir, args.ignores,
387*b32fbb63SXin Li                               args.state_filter)
388*b32fbb63SXin Li  if args.csv_file:
389*b32fbb63SXin Li    write_file(
390*b32fbb63SXin Li        args.csv_file,
391*b32fbb63SXin Li        change_report.get_diff_stat_lines(),
392*b32fbb63SXin Li        header=ChangeReport.get_diff_stat_header())
393*b32fbb63SXin Li
394*b32fbb63SXin Li  if args.diff_output_file:
395*b32fbb63SXin Li    write_file(args.diff_output_file, change_report.get_diff_lines())
396*b32fbb63SXin Li
397*b32fbb63SXin Li
398*b32fbb63SXin Liif __name__ == '__main__':
399*b32fbb63SXin Li  main()
400