2 # -*- coding: utf-8 -*-
4 # Copyright 2005, 2006 Zuza Software Foundation
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"""
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.')
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()
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
)
75 parser
.error("File %s is a directory while file %s is a regular file" % (fromfile
, tofile
))
77 if os
.path
.isdir(tofile
):
78 parser
.error("File %s is a regular file while file %s is a directory" % (fromfile
, tofile
))
80 differ
= FileDiffer(fromfile
, tofile
, options
)
81 differ
.writediff(sys
.stdout
)
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
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
):
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()
104 for difffile
in difffiles
:
105 if self
.isexcluded(difffile
):
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
)
118 outfile
.write("Common subdirectories: %s and %s\n" % (fromfile
, tofile
))
120 outfile
.write("File %s is a directory while file %s is a regular file\n" % (fromfile
, tofile
))
122 if os
.path
.isdir(tofile
):
123 parser
.error("File %s is a regular file while file %s is a directory\n" % (fromfile
, tofile
))
125 filediffer
= FileDiffer(fromfile
, tofile
, self
.options
)
126 filediffer
.writediff(outfile
)
128 outfile
.write("Only in %s: %s\n" % (self
.fromdir
, difffile
))
130 outfile
.write("Only in %s: %s\n" % (self
.todir
, difffile
))
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
138 self
.options
= options
140 def writediff(self
, outfile
):
141 """writes the actual diff to the given file"""
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
:
153 outfile
.write("%s: No such file or directory\n" % self
.fromfile
)
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
:
165 outfile
.write("%s: No such file or directory\n" % self
.tofile
)
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
)
179 fromstring
= '--- %s\t%s%s' % (self
.fromfile
, fromfiledate
, lineterm
)
180 tostring
= '+++ %s\t%s%s' % (self
.tofile
, tofiledate
, lineterm
)
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
)])
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
:
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
)])
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
:
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
)])
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
:
212 outfile
.write(fromstring
)
213 outfile
.write(tostring
)
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"""
222 for tag
, i1
, i2
, j1
, j2
in group
:
223 from_lines
.extend(self
.from_lines
[i1
:i2
])
226 def get_to_lines(self
, group
):
227 """returns the lines referred to by group, from the tofile"""
229 for tag
, i1
, i2
, j1
, j2
in group
:
230 to_lines
.extend(self
.to_lines
[j1
:j2
])
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
:
239 for line
in self
.from_lines
[i1
:i2
]:
242 if tag
== 'replace' or tag
== 'delete':
243 for line
in self
.from_lines
[i1
:i2
]:
245 if tag
== 'replace' or tag
== 'insert':
246 for line
in self
.to_lines
[j1
:j2
]:
249 if __name__
== "__main__":