7 from optparse
import OptionParser
10 from git_stats
import parse
13 """A class to store the information of a file diff in.
16 afile: The file used as the left side of the diff.
17 bfile: The file used as the right side of the diff.
18 context: The context of this diff.
19 apos: Where the left side of the diff starts.
20 bpos: Where the right side of the diff starts.
21 linesAdded: Which lines were added.
22 linesDeleted: Which lines were deleted.
25 def __init__(self
, diffHeader
):
31 self
.linesDeleted
= []
33 for line
in diffHeader
:
34 if line
.startswith("--- "):
37 if line
.startswith("+++ "):
41 a
= "Diff for '" + self
.afile
+ "' (" + str(self
.astart
) + ") against '"
42 b
= self
.bfile
+ "' (" + str(self
.bstart
) + ")" + self
.context
+ "."
44 return a
+ b
+ '\n' + str(self
.linesAdded
) + '\n' + str(self
.linesDeleted
)
46 def getCommitDiff(commit
, ignoreWhitespace
=True, noContext
=False):
47 """Returns the commit diff for the specified commit
50 commit: The commit to get the diff for.
52 Returns: The commit diff.
64 args
.append(commit
+ "^")
67 result
= git
.diff_tree(*args
)
72 """Splits off the diff in chunks, one for each file
75 diff: The diff to split up.
77 Returns: A list containing a chunk per file.
85 if line
.startswith("diff"):
87 chunks
.append(content
)
93 chunks
.append(content
)
97 def _splitFileDiff(diff
):
98 """Splits a file diff into chunks, one per area.
101 diff: The diff to split up.
103 Returns: The diff header and a list with all the chunks.
114 if line
.startswith("@@"):
120 for line
in diff
[start
:]:
121 if line
.startswith("@@"):
123 chunks
.append(content
)
129 chunks
.append(content
)
131 return header
, chunks
133 def _parseFileDiff(header
, chunk
):
134 """Takes a file diff and returns the parsed result
137 header: The diff header.
138 chunk: The chunk to parse.
140 Returns: A fileDiff containing the parsed diff.
143 result
= fileDiff(header
)
151 # Find out where the context line ends, skipping the first '@@'
152 to
= chunk
[0].find("@@", 2)
154 # Get the context, skipping the first and last '@@"
155 context
= chunk
[0][3:to
]
157 # Split it at the spaces and store the positions, ignoring '-' and '+'
158 split
= context
.split(' ')
162 apos
= int(a
.split(',')[0])
163 bpos
= int(b
.split(',')[0])
168 # Start at the first line (skip the context line)
169 for line
in chunk
[1:]:
170 if line
.startswith("-"):
171 deleted
.append((apos
, line
[1:]))
174 if line
.startswith("+"):
175 added
.append((bpos
, line
[1:]))
178 result
.linesDeleted
= deleted
179 result
.linesAdded
= added
183 def parseCommitDiff(diff
):
184 """Takes a commit diff and returns the parsed result
187 diff: The diff to parse.
189 Returns: A parsedDiff instance containing the parsed diff.
194 # Split the diff in file sized chunks
195 chunks
= splitDiff(diff
)
197 # Loop over all the file diffs and parse them
199 header
, filechunks
= _splitFileDiff(chunk
)
201 # Loop over all the chunks and parse them
202 for filechunk
in filechunks
:
203 # Get the result and store it
204 fd
= _parseFileDiff(header
, filechunk
)
209 def _compareFileDiffs(adiff
, bdiff
, invert
=False):
210 """Compares two fileDiffs and returns whether they are equal
213 adiff: The first fileDiff.
214 bdiff: The second fileDiff.
215 invert: Whether to compare linesAdded with linesDeleted.
217 Returns: Whether the two diffs are equal.
221 if not adiff
.linesAdded
== bdiff
.linesDeleted
:
223 if not adiff
.linesDeleted
== bdiff
.linesAdded
:
226 if not adiff
.linesAdded
== bdiff
.linesAdded
:
228 if not adiff
.linesDeleted
== bdiff
.linesDeleted
:
231 # Checked everything, accept
234 def _compareDiffs(adiffs
, bdiffs
, compareChanges
=False, invert
=False):
235 """Compares the two diffs and returns whether they are equal
238 adiffs: The first set of diffs.
239 bdiffs: The second set of diffs.
240 compareChanges: Whether to compare not only which lines changed.
241 invert: When compareChanges, invert the comparison of deleted/added.
243 Returns: Whether the diffs are equal.
247 # Look for a match in the bdiffs
248 for theirs
in bdiffs
:
250 # Looks like we have a match
251 if (theirs
.astart
<= fd
.astart
and theirs
.bstart
>= fd
.bstart
) or \
252 (invert
and theirs
.astart
<= fd
.bstart
and theirs
.bstart
>= fd
.astart
):
254 # If we want to compare changes, do they match
256 # Reject if they are inequal
257 if not _compareFileDiffs(fd
, theirs
, invert
):
260 # It was indeed a match, stop searching through bdiffs
264 # Went through all items in bdiffs and couldn't find a matching pair
267 # All items in adiffs checked, all had a matching pair, accept.
270 def _difference(adiffs
, bdiffs
, compareChanges
=False, invert
=False):
271 """Calculates the difference between two diffs and returns it
274 adiffs: The first set of diffs.
275 bdiffs: The second set of diffs.
276 compareChanges: Whether to compare not only which lines changed.
277 invert: When compareChanges, invert the comparison of deleted/added.
279 Returns: Which keys are missing and the difference between both diffs.
282 afiles
= collections
.defaultdict(list)
283 bfiles
= collections
.defaultdict(list)
289 afiles
[(fd
.afile
, fd
.bfile
)].append(fd
)
292 bfiles
[(fd
.afile
, fd
.bfile
)].append(fd
)
294 for key
, fds
in afiles
.iteritems():
295 if not bfiles
.has_key(key
):
301 if not _compareDiffs(fds
, theirs
, compareChanges
, invert
):
302 difference
.append((fds
, theirs
))
304 return missing
, difference
306 def commitdiffEqual(original
, potentialMatch
, threshold
=0,
307 compareChanges
=True, invert
=False, verbose
=True):
308 """Tests whether a commit matches another by a specified threshhold.
311 original: The original commit that is to be checked.
312 potentialMatch: The commit that might match original.
313 threshhold: The threshold for how close they have to match.
314 compareChanges: Whether to compare the changes made or just changes lines.
315 invert: Whether to compare deletions with insertions instead.
317 Returns: Whether the commit diffs are equal.
322 # Get the diff, but ignore whitespace
323 result
= getCommitDiff(original
, noContext
=True)
325 diffOriginal
= result
.split('\n')
327 # Get the diff but ignore whitespace
328 result
= getCommitDiff(potentialMatch
, noContext
=True)
330 diffPotentialMatch
= result
.split('\n')
332 parsedOriginal
= parseCommitDiff(diffOriginal
)
333 parsedPotentialMatch
= parseCommitDiff(diffPotentialMatch
)
335 missing
, diff
= _difference(parsedOriginal
, parsedPotentialMatch
, compareChanges
=compareChanges
, invert
=invert
)
339 print("Missing the following keys:")
344 print("Found the following differences:")
345 for ours
, theirs
in diff
:
349 print("\nDoes not match:\n")
354 # Unequal if something missing, or there is a difference
355 return not (missing
or diff
)
357 def isReverted(commit
, potentialRevert
):
358 """Returns whether the specified commit is reverted by another one
361 commit: The commit that might be reverted.
362 potentialRevert: The commit that might be a revert.
365 return commitdiffEqual(commit
, potentialRevert
, invert
=True, verbose
=False)
368 """Dispatches diff related commands
371 progname
= os
.path
.basename(sys
.argv
[0]) + " diff"
373 parser
= OptionParser(option_class
=parse
.GitOption
, prog
=progname
)
379 help="show whether the two diffs for the specified commits match",
380 metavar
="COMMIT COMMIT")
385 help="the threshold for comparison")
388 "-n", "--no-compare",
389 action
="store_false",
391 help="do not compare the diff content, just look at which lines were touched")
396 help="compare additions with deletions instead of with additions, and vise versa")
398 parser
.set_default("threshold", 0)
399 parser
.set_default("compare", True)
400 parser
.set_default("invert", False)
402 (options
, args
) = parser
.parse_args(list(args
))
405 result
= commitdiffEqual( threshold
=options
.threshold
,
406 compareChanges
=options
.compare
,
407 invert
=options
.invert
,