fix git support for v1.5.3 (or higher) by setting "--work-tree"
[translate_toolkit.git] / tools / pydiff.py
blob5122736c104575604e8ef689b7f50362c710bf9c
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # Copyright 2005, 2006 Zuza Software Foundation
5 #
6 # This file is part of translate.
8 # translate is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version.
13 # translate is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with translate; if not, write to the Free Software
20 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22 """diff tool like GNU diff, but lets you have special options that are useful in dealing with PO files"""
24 import difflib
25 import optparse
26 import time
27 import os
28 import sys
29 import fnmatch
31 lineterm = "\n"
33 def main():
34 """main program for pydiff"""
35 usage = "usage: %prog [options] fromfile tofile"
36 parser = optparse.OptionParser(usage)
37 # GNU diff like options
38 parser.add_option("-i", "--ignore-case", default=False, action="store_true",
39 help='Ignore case differences in file contents.')
40 parser.add_option("-U", "--unified", type="int", metavar="NUM", default=3, dest="unified_lines",
41 help='Output NUM (default 3) lines of unified context')
42 parser.add_option("-r", "--recursive", default=False, action="store_true",
43 help='Recursively compare any subdirectories found.')
44 parser.add_option("-N", "--new-file", default=False, action="store_true",
45 help='Treat absent files as empty.')
46 parser.add_option("", "--unidirectional-new-file", default=False, action="store_true",
47 help='Treat absent first files as empty.')
48 parser.add_option("-s", "--report-identical-files", default=False, action="store_true",
49 help='Report when two files are the same.')
50 parser.add_option("-x", "--exclude", default=["CVS", "*.po~"], action="append", metavar="PAT",
51 help='Exclude files that match PAT.')
52 # our own options
53 parser.add_option("", "--fromcontains", type="string", default=None, metavar="TEXT",
54 help='Only show changes where fromfile contains TEXT')
55 parser.add_option("", "--tocontains", type="string", default=None, metavar="TEXT",
56 help='Only show changes where tofile contains TEXT')
57 parser.add_option("", "--contains", type="string", default=None, metavar="TEXT",
58 help='Only show changes where fromfile or tofile contains TEXT')
59 parser.add_option("-I", "--ignore-case-contains", default=False, action="store_true",
60 help='Ignore case differences when matching any of the changes')
61 parser.add_option("", "--accelerator", dest="accelchars", default="",
62 metavar="ACCELERATORS", help="ignores the given accelerator characters when matching")
63 (options, args) = parser.parse_args()
65 if len(args) != 2:
66 parser.error("fromfile and tofile required")
67 fromfile, tofile = args
68 if fromfile == "-" and tofile == "-":
69 parser.error("Only one of fromfile and tofile can be read from stdin")
71 if os.path.isdir(fromfile):
72 if os.path.isdir(tofile):
73 differ = DirDiffer(fromfile, tofile, options)
74 else:
75 parser.error("File %s is a directory while file %s is a regular file" % (fromfile, tofile))
76 else:
77 if os.path.isdir(tofile):
78 parser.error("File %s is a regular file while file %s is a directory" % (fromfile, tofile))
79 else:
80 differ = FileDiffer(fromfile, tofile, options)
81 differ.writediff(sys.stdout)
83 class DirDiffer:
84 """generates diffs between directories"""
85 def __init__(self, fromdir, todir, options):
86 """constructs a comparison between the two dirs using the given options"""
87 self.fromdir = fromdir
88 self.todir = todir
89 self.options = options
91 def isexcluded(self, difffile):
92 """checks if the given filename has been excluded from the diff"""
93 for exclude_pat in self.options.exclude:
94 if fnmatch.fnmatch(difffile, exclude_pat):
95 return True
96 return False
98 def writediff(self, outfile):
99 """writes the actual diff to the given file"""
100 fromfiles = os.listdir(self.fromdir)
101 tofiles = os.listdir(self.todir)
102 difffiles = dict.fromkeys(fromfiles + tofiles).keys()
103 difffiles.sort()
104 for difffile in difffiles:
105 if self.isexcluded(difffile):
106 continue
107 from_ok = (difffile in fromfiles or self.options.new_file or self.options.unidirectional_new_file)
108 to_ok = (difffile in tofiles or self.options.new_file)
109 if from_ok and to_ok:
110 fromfile = os.path.join(self.fromdir, difffile)
111 tofile = os.path.join(self.todir, difffile)
112 if os.path.isdir(fromfile):
113 if os.path.isdir(tofile):
114 if self.options.recursive:
115 differ = DirDiffer(fromfile, tofile, self.options)
116 differ.writediff(outfile)
117 else:
118 outfile.write("Common subdirectories: %s and %s\n" % (fromfile, tofile))
119 else:
120 outfile.write("File %s is a directory while file %s is a regular file\n" % (fromfile, tofile))
121 else:
122 if os.path.isdir(tofile):
123 parser.error("File %s is a regular file while file %s is a directory\n" % (fromfile, tofile))
124 else:
125 filediffer = FileDiffer(fromfile, tofile, self.options)
126 filediffer.writediff(outfile)
127 elif from_ok:
128 outfile.write("Only in %s: %s\n" % (self.fromdir, difffile))
129 elif to_ok:
130 outfile.write("Only in %s: %s\n" % (self.todir, difffile))
132 class FileDiffer:
133 """generates diffs between files"""
134 def __init__(self, fromfile, tofile, options):
135 """constructs a comparison between the two files using the given options"""
136 self.fromfile = fromfile
137 self.tofile = tofile
138 self.options = options
140 def writediff(self, outfile):
141 """writes the actual diff to the given file"""
142 validfiles = True
143 if os.path.exists(self.fromfile):
144 self.from_lines = open(self.fromfile, 'U').readlines()
145 fromfiledate = os.stat(self.fromfile).st_mtime
146 elif self.fromfile == "-":
147 self.from_lines = sys.stdin.readlines()
148 fromfiledate = time.time()
149 elif self.options.new_file or self.options.unidirectional_new_file:
150 self.from_lines = []
151 fromfiledate = 0
152 else:
153 outfile.write("%s: No such file or directory\n" % self.fromfile)
154 validfiles = False
155 if os.path.exists(self.tofile):
156 self.to_lines = open(self.tofile, 'U').readlines()
157 tofiledate = os.stat(self.tofile).st_mtime
158 elif self.tofile == "-":
159 self.to_lines = sys.stdin.readlines()
160 tofiledate = time.time()
161 elif self.options.new_file:
162 self.to_lines = []
163 tofiledate = 0
164 else:
165 outfile.write("%s: No such file or directory\n" % self.tofile)
166 validfiles = False
167 if not validfiles:
168 return
169 fromfiledate = time.ctime(fromfiledate)
170 tofiledate = time.ctime(tofiledate)
171 compare_from_lines = self.from_lines
172 compare_to_lines = self.to_lines
173 if self.options.ignore_case:
174 compare_from_lines = [line.lower() for line in compare_from_lines]
175 compare_to_lines = [line.lower() for line in compare_to_lines]
176 matcher = difflib.SequenceMatcher(None, compare_from_lines, compare_to_lines)
177 groups = matcher.get_grouped_opcodes(self.options.unified_lines)
178 started = False
179 fromstring = '--- %s\t%s%s' % (self.fromfile, fromfiledate, lineterm)
180 tostring = '+++ %s\t%s%s' % (self.tofile, tofiledate, lineterm)
182 for group in groups:
183 hunk = "".join([line for line in self.unified_diff(group)])
184 if self.options.fromcontains:
185 if self.options.ignore_case_contains:
186 hunk_from_lines = "".join([line.lower() for line in self.get_from_lines(group)])
187 else:
188 hunk_from_lines = "".join(self.get_from_lines(group))
189 for accelerator in self.options.accelchars:
190 hunk_from_lines = hunk_from_lines.replace(accelerator, "")
191 if self.options.fromcontains not in hunk_from_lines:
192 continue
193 if self.options.tocontains:
194 if self.options.ignore_case_contains:
195 hunk_to_lines = "".join([line.lower() for line in self.get_to_lines(group)])
196 else:
197 hunk_to_lines = "".join(self.get_to_lines(group))
198 for accelerator in self.options.accelchars:
199 hunk_to_lines = hunk_to_lines.replace(accelerator, "")
200 if self.options.tocontains not in hunk_to_lines:
201 continue
202 if self.options.contains:
203 if self.options.ignore_case_contains:
204 hunk_lines = "".join([line.lower() for line in self.get_from_lines(group) + self.get_to_lines(group)])
205 else:
206 hunk_lines = "".join(self.get_from_lines(group) + self.get_to_lines(group))
207 for accelerator in self.options.accelchars:
208 hunk_lines = hunk_lines.replace(accelerator, "")
209 if self.options.contains not in hunk_lines:
210 continue
211 if not started:
212 outfile.write(fromstring)
213 outfile.write(tostring)
214 started = True
215 outfile.write(hunk)
216 if not started and self.options.report_identical_files:
217 outfile.write("Files %s and %s are identical\n" % (self.fromfile, self.tofile))
219 def get_from_lines(self, group):
220 """returns the lines referred to by group, from the fromfile"""
221 from_lines = []
222 for tag, i1, i2, j1, j2 in group:
223 from_lines.extend(self.from_lines[i1:i2])
224 return from_lines
226 def get_to_lines(self, group):
227 """returns the lines referred to by group, from the tofile"""
228 to_lines = []
229 for tag, i1, i2, j1, j2 in group:
230 to_lines.extend(self.to_lines[j1:j2])
231 return to_lines
233 def unified_diff(self, group):
234 """takes the group of opcodes and generates a unified diff line by line"""
235 i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
236 yield "@@ -%d,%d +%d,%d @@%s" % (i1+1, i2-i1, j1+1, j2-j1, lineterm)
237 for tag, i1, i2, j1, j2 in group:
238 if tag == 'equal':
239 for line in self.from_lines[i1:i2]:
240 yield ' ' + line
241 continue
242 if tag == 'replace' or tag == 'delete':
243 for line in self.from_lines[i1:i2]:
244 yield '-' + line
245 if tag == 'replace' or tag == 'insert':
246 for line in self.to_lines[j1:j2]:
247 yield '+' + line
249 if __name__ == "__main__":
250 main()