1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 """diff tool like GNU diff, but lets you have special options that are useful in dealing with PO files"""
23
24 import difflib
25 import optparse
26 import time
27 import os
28 import sys
29 import fnmatch
30
31 lineterm = "\n"
32
33
35 """main program for pydiff"""
36 usage = "usage: %prog [options] fromfile tofile"
37 parser = optparse.OptionParser(usage)
38
39 parser.add_option("-i", "--ignore-case", default=False, action="store_true",
40 help='Ignore case differences in file contents.')
41 parser.add_option("-U", "--unified", type="int", metavar="NUM", default=3, dest="unified_lines",
42 help='Output NUM (default 3) lines of unified context')
43 parser.add_option("-r", "--recursive", default=False, action="store_true",
44 help='Recursively compare any subdirectories found.')
45 parser.add_option("-N", "--new-file", default=False, action="store_true",
46 help='Treat absent files as empty.')
47 parser.add_option("", "--unidirectional-new-file", default=False, action="store_true",
48 help='Treat absent first files as empty.')
49 parser.add_option("-s", "--report-identical-files", default=False, action="store_true",
50 help='Report when two files are the same.')
51 parser.add_option("-x", "--exclude", default=["CVS", "*.po~"], action="append", metavar="PAT",
52 help='Exclude files that match PAT.')
53
54 parser.add_option("", "--fromcontains", type="string", default=None, metavar="TEXT",
55 help='Only show changes where fromfile contains TEXT')
56 parser.add_option("", "--tocontains", type="string", default=None, metavar="TEXT",
57 help='Only show changes where tofile contains TEXT')
58 parser.add_option("", "--contains", type="string", default=None, metavar="TEXT",
59 help='Only show changes where fromfile or tofile contains TEXT')
60 parser.add_option("-I", "--ignore-case-contains", default=False, action="store_true",
61 help='Ignore case differences when matching any of the changes')
62 parser.add_option("", "--accelerator", dest="accelchars", default="",
63 metavar="ACCELERATORS", help="ignores the given accelerator characters when matching")
64 (options, args) = parser.parse_args()
65
66 if len(args) != 2:
67 parser.error("fromfile and tofile required")
68 fromfile, tofile = args
69 if fromfile == "-" and tofile == "-":
70 parser.error("Only one of fromfile and tofile can be read from stdin")
71
72 if os.path.isdir(fromfile):
73 if os.path.isdir(tofile):
74 differ = DirDiffer(fromfile, tofile, options)
75 else:
76 parser.error("File %s is a directory while file %s is a regular file" % (fromfile, tofile))
77 else:
78 if os.path.isdir(tofile):
79 parser.error("File %s is a regular file while file %s is a directory" % (fromfile, tofile))
80 else:
81 differ = FileDiffer(fromfile, tofile, options)
82 differ.writediff(sys.stdout)
83
84
86 """generates diffs between directories"""
87
88 - def __init__(self, fromdir, todir, options):
89 """constructs a comparison between the two dirs using the given options"""
90 self.fromdir = fromdir
91 self.todir = todir
92 self.options = options
93
95 """checks if the given filename has been excluded from the diff"""
96 for exclude_pat in self.options.exclude:
97 if fnmatch.fnmatch(difffile, exclude_pat):
98 return True
99 return False
100
102 """writes the actual diff to the given file"""
103 fromfiles = os.listdir(self.fromdir)
104 tofiles = os.listdir(self.todir)
105 difffiles = dict.fromkeys(fromfiles + tofiles).keys()
106 difffiles.sort()
107 for difffile in difffiles:
108 if self.isexcluded(difffile):
109 continue
110 from_ok = (difffile in fromfiles or self.options.new_file or self.options.unidirectional_new_file)
111 to_ok = (difffile in tofiles or self.options.new_file)
112 if from_ok and to_ok:
113 fromfile = os.path.join(self.fromdir, difffile)
114 tofile = os.path.join(self.todir, difffile)
115 if os.path.isdir(fromfile):
116 if os.path.isdir(tofile):
117 if self.options.recursive:
118 differ = DirDiffer(fromfile, tofile, self.options)
119 differ.writediff(outfile)
120 else:
121 outfile.write("Common subdirectories: %s and %s\n" % (fromfile, tofile))
122 else:
123 outfile.write("File %s is a directory while file %s is a regular file\n" % (fromfile, tofile))
124 else:
125 if os.path.isdir(tofile):
126 parser.error("File %s is a regular file while file %s is a directory\n" % (fromfile, tofile))
127 else:
128 filediffer = FileDiffer(fromfile, tofile, self.options)
129 filediffer.writediff(outfile)
130 elif from_ok:
131 outfile.write("Only in %s: %s\n" % (self.fromdir, difffile))
132 elif to_ok:
133 outfile.write("Only in %s: %s\n" % (self.todir, difffile))
134
135
137 """generates diffs between files"""
138
139 - def __init__(self, fromfile, tofile, options):
140 """constructs a comparison between the two files using the given options"""
141 self.fromfile = fromfile
142 self.tofile = tofile
143 self.options = options
144
146 """writes the actual diff to the given file"""
147 validfiles = True
148 if os.path.exists(self.fromfile):
149 self.from_lines = open(self.fromfile, 'U').readlines()
150 fromfiledate = os.stat(self.fromfile).st_mtime
151 elif self.fromfile == "-":
152 self.from_lines = sys.stdin.readlines()
153 fromfiledate = time.time()
154 elif self.options.new_file or self.options.unidirectional_new_file:
155 self.from_lines = []
156 fromfiledate = 0
157 else:
158 outfile.write("%s: No such file or directory\n" % self.fromfile)
159 validfiles = False
160 if os.path.exists(self.tofile):
161 self.to_lines = open(self.tofile, 'U').readlines()
162 tofiledate = os.stat(self.tofile).st_mtime
163 elif self.tofile == "-":
164 self.to_lines = sys.stdin.readlines()
165 tofiledate = time.time()
166 elif self.options.new_file:
167 self.to_lines = []
168 tofiledate = 0
169 else:
170 outfile.write("%s: No such file or directory\n" % self.tofile)
171 validfiles = False
172 if not validfiles:
173 return
174 fromfiledate = time.ctime(fromfiledate)
175 tofiledate = time.ctime(tofiledate)
176 compare_from_lines = self.from_lines
177 compare_to_lines = self.to_lines
178 if self.options.ignore_case:
179 compare_from_lines = [line.lower() for line in compare_from_lines]
180 compare_to_lines = [line.lower() for line in compare_to_lines]
181 matcher = difflib.SequenceMatcher(None, compare_from_lines, compare_to_lines)
182 groups = matcher.get_grouped_opcodes(self.options.unified_lines)
183 started = False
184 fromstring = '--- %s\t%s%s' % (self.fromfile, fromfiledate, lineterm)
185 tostring = '+++ %s\t%s%s' % (self.tofile, tofiledate, lineterm)
186
187 for group in groups:
188 hunk = "".join([line for line in self.unified_diff(group)])
189 if self.options.fromcontains:
190 if self.options.ignore_case_contains:
191 hunk_from_lines = "".join([line.lower() for line in self.get_from_lines(group)])
192 else:
193 hunk_from_lines = "".join(self.get_from_lines(group))
194 for accelerator in self.options.accelchars:
195 hunk_from_lines = hunk_from_lines.replace(accelerator, "")
196 if self.options.fromcontains not in hunk_from_lines:
197 continue
198 if self.options.tocontains:
199 if self.options.ignore_case_contains:
200 hunk_to_lines = "".join([line.lower() for line in self.get_to_lines(group)])
201 else:
202 hunk_to_lines = "".join(self.get_to_lines(group))
203 for accelerator in self.options.accelchars:
204 hunk_to_lines = hunk_to_lines.replace(accelerator, "")
205 if self.options.tocontains not in hunk_to_lines:
206 continue
207 if self.options.contains:
208 if self.options.ignore_case_contains:
209 hunk_lines = "".join([line.lower() for line in self.get_from_lines(group) + self.get_to_lines(group)])
210 else:
211 hunk_lines = "".join(self.get_from_lines(group) + self.get_to_lines(group))
212 for accelerator in self.options.accelchars:
213 hunk_lines = hunk_lines.replace(accelerator, "")
214 if self.options.contains not in hunk_lines:
215 continue
216 if not started:
217 outfile.write(fromstring)
218 outfile.write(tostring)
219 started = True
220 outfile.write(hunk)
221 if not started and self.options.report_identical_files:
222 outfile.write("Files %s and %s are identical\n" % (self.fromfile, self.tofile))
223
225 """returns the lines referred to by group, from the fromfile"""
226 from_lines = []
227 for tag, i1, i2, j1, j2 in group:
228 from_lines.extend(self.from_lines[i1:i2])
229 return from_lines
230
232 """returns the lines referred to by group, from the tofile"""
233 to_lines = []
234 for tag, i1, i2, j1, j2 in group:
235 to_lines.extend(self.to_lines[j1:j2])
236 return to_lines
237
239 """takes the group of opcodes and generates a unified diff line by line"""
240 i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
241 yield "@@ -%d,%d +%d,%d @@%s" % (i1+1, i2-i1, j1+1, j2-j1, lineterm)
242 for tag, i1, i2, j1, j2 in group:
243 if tag == 'equal':
244 for line in self.from_lines[i1:i2]:
245 yield ' ' + line
246 continue
247 if tag == 'replace' or tag == 'delete':
248 for line in self.from_lines[i1:i2]:
249 yield '-' + line
250 if tag == 'replace' or tag == 'insert':
251 for line in self.to_lines[j1:j2]:
252 yield '+' + line
253
254
255 if __name__ == "__main__":
256 main()
257