]> scripts.mit.edu Git - wizard.git/blob - wizard/resolve.py
910802f8cbb3cc40be307ef2671ef3e1aee6f11a
[wizard.git] / wizard / resolve.py
1 """
2 .. highlight:: diff
3
4 This module contains algorithms for performing conflict
5 resolution after Git performs its recursive merge.  It
6 defines a simple domain specific language (that, at
7 its simplest form, merely involves copying conflict markers
8 and writing in the form that they should be resolved as) for
9 specifying how to resolve conflicts.  These are mostly relevant
10 for resolving conflicts in configuration files.
11
12 The conflict resolution DSL is described here:
13
14 Resolutions are specified as input-output pairs.  An input
15 is a string with the conflict resolution markers ("<" * 7,
16 "=" * 7 and ">" * 7), with the HEAD content above the equals
17 divider, and the upstream content below the equals divider.
18 Lines can also be marked as "***N***" where N is a natural
19 number greater than 0 (i.e. 1 or more), which means that
20 an arbitrary number of lines may be matched and available for output.
21
22 Output is a list of integers and strings.  Integers expand
23 to lines that were specified earlier; -1 and 0 are special integers
24 that correspond to the entire HEAD text, and the entire upstream
25 text, respectively.  Strings can be used to insert custom lines.
26
27 The DSL does not currently claim to support character level granularity.
28 It also does not claim to support contiguous conflicts.
29 Our hope is that this simple syntax will be sufficient to cover
30 most common merge failures.
31
32 Here are some examples::
33
34     <<<<<<<
35     downstream
36     =======
37     upstream
38     >>>>>>>
39
40 With ``[-1]`` would discard all upstream changes, whereas with ``[0]``
41 would discard downstream changes (you would probably want to be
42 careful about wildcarding in the upstream string).
43
44 Pattern matching in action::
45
46     <<<<<<<
47     ***1***
48     old upstream
49     ***2***
50     old upstream
51     ***3***
52     =======
53     new upstream
54     >>>>>>>
55
56 With ``[0, 1, 2, 3]`` would resolve with the new upstream text, and
57 then the user matched globs.
58 """
59
60 import re
61 import itertools
62
63 re_var = re.compile("^\*\*\*(\d+)\*\*\*\\\n", re.MULTILINE)
64
65 def spec_to_regex(spec):
66     """
67     Translates a specification string into a regular expression tuple.
68     Note that pattern matches are out of order, so the second element
69     of the tuple is a dict specified strings to subpattern numbers.
70     Requires re.DOTALL for correct operation.
71     """
72     ours, _, theirs = "".join(spec.strip().splitlines(True)[1:-1]).partition("=======\n")
73     def regexify(text, fullmatch, matchno):
74         text_split = re.split(re_var, text)
75         ret = ""
76         mappings = {fullmatch: matchno}
77         for is_var, line in zip(itertools.cycle([False, True]), text_split):
78             if is_var:
79                 ret += "(.*\\\n)"
80                 matchno += 1
81                 mappings[int(line)] = matchno
82             else:
83                 ret += re.escape(line)
84         return ("(" + ret + ")", mappings)
85     ours_regex, ours_mappings = regexify(ours, -1, 1)
86     theirs_regex, theirs_mappings = regexify(theirs, 0, len(ours_mappings) + 1)
87     ours_mappings.update(theirs_mappings)
88     return ("<<<<<<<[^\n]*\\\n" + ours_regex + "=======\\\n" + theirs_regex + ">>>>>>>[^\n]*(\\\n|$)", ours_mappings)
89
90 def result_to_repl(result, mappings):
91     def ritem_to_string(r):
92         if type(r) is int:
93             return "\\%d" % mappings[r]
94         else:
95             return r + "\n"
96     return "".join(map(ritem_to_string, result))
97
98 def resolve(contents, spec, result):
99     rstring, mappings = spec_to_regex(spec)
100     regex = re.compile(rstring, re.DOTALL)
101     repl = result_to_repl(result, mappings)
102     ret = ""
103     conflict = ""
104     status = 0
105     for line in contents.splitlines(True):
106         if status == 0 and line.startswith("<<<<<<<"):
107             status = 1
108         elif status == 1 and line.startswith("======="):
109             status = 2
110         # ok, now process
111         if status == 2 and line.startswith(">>>>>>>"):
112             status = 0
113             conflict += line
114             ret += regex.sub(repl, conflict)
115             conflict = ""
116         elif status:
117             conflict += line
118         else:
119             ret += line
120     return ret
121
122 def is_conflict(contents):
123     # Really really simple heuristic
124     return "<<<<<<<" in contents
125
126 def fix_newlines(file, log=True):
127     """
128     Normalizes newlines in a file into UNIX file endings.  If
129     ``log`` is ``True`` an info log mesage is printed if
130     any normalization occurs.  Return value is ``True`` if
131     normalization occurred.
132     """
133     old_contents = open(file, "r").read()
134     contents = old_contents
135     while "\r\n" in contents:
136         contents = contents.replace("\r\n", "\n")
137     contents = contents.replace("\r", "\n")
138     if contents != old_contents:
139         logging.info("Converted %s to UNIX file endings" % file)
140         open(file, "w").write(contents)
141         return True
142     return False