##// END OF EJS Templates
imerge: simplify 1d5ebb0d366f
Brendan Cully -
r5165:ec24bfd8 default
parent child Browse files
Show More
@@ -1,362 +1,362 b''
1 1 # Copyright (C) 2007 Brendan Cully <brendan@kublai.com>
2 2 # Published under the GNU GPL
3 3
4 4 '''
5 5 imerge - interactive merge
6 6 '''
7 7
8 8 from mercurial.i18n import _
9 9 from mercurial.node import *
10 10 from mercurial import commands, cmdutil, fancyopts, hg, merge, util
11 11 import os, tarfile
12 12
13 13 class InvalidStateFileException(Exception): pass
14 14
15 15 class ImergeStateFile(object):
16 16 def __init__(self, im):
17 17 self.im = im
18 18
19 19 def save(self, dest):
20 20 tf = tarfile.open(dest, 'w:gz')
21 21
22 22 st = os.path.join(self.im.path, 'status')
23 23 tf.add(st, os.path.join('.hg', 'imerge', 'status'))
24 24
25 25 for f in self.im.resolved:
26 26 (fd, fo) = self.im.conflicts[f]
27 27 abssrc = self.im.repo.wjoin(fd)
28 28 tf.add(abssrc, fd)
29 29
30 30 tf.close()
31 31
32 32 def load(self, source):
33 33 wlock = self.im.repo.wlock()
34 34 lock = self.im.repo.lock()
35 35
36 36 tf = tarfile.open(source, 'r')
37 37 contents = tf.getnames()
38 38 # tarfile normalizes path separators to '/'
39 statusfile = '/'.join(['.hg', 'imerge', 'status'])
39 statusfile = '.hg/imerge/status'
40 40 if statusfile not in contents:
41 41 raise InvalidStateFileException('no status file')
42 42
43 43 tf.extract(statusfile, self.im.repo.root)
44 44 p1, p2 = self.im.load()
45 45 if self.im.repo.dirstate.parents()[0] != p1.node():
46 46 hg.clean(self.im.repo, p1.node())
47 47 self.im.start(p2.node())
48 48 for tarinfo in tf:
49 49 tf.extract(tarinfo, self.im.repo.root)
50 50 self.im.load()
51 51
52 52 class Imerge(object):
53 53 def __init__(self, ui, repo):
54 54 self.ui = ui
55 55 self.repo = repo
56 56
57 57 self.path = repo.join('imerge')
58 58 self.opener = util.opener(self.path)
59 59
60 60 self.wctx = self.repo.workingctx()
61 61 self.conflicts = {}
62 62 self.resolved = []
63 63
64 64 def merging(self):
65 65 return len(self.wctx.parents()) > 1
66 66
67 67 def load(self):
68 68 # status format. \0-delimited file, fields are
69 69 # p1, p2, conflict count, conflict filenames, resolved filenames
70 70 # conflict filenames are tuples of localname, remoteorig, remotenew
71 71
72 72 statusfile = self.opener('status')
73 73
74 74 status = statusfile.read().split('\0')
75 75 if len(status) < 3:
76 76 raise util.Abort('invalid imerge status file')
77 77
78 78 try:
79 79 parents = [self.repo.changectx(n) for n in status[:2]]
80 80 except LookupError:
81 81 raise util.Abort('merge parent %s not in repository' % short(p))
82 82
83 83 status = status[2:]
84 84 conflicts = int(status.pop(0)) * 3
85 85 self.resolved = status[conflicts:]
86 86 for i in xrange(0, conflicts, 3):
87 87 self.conflicts[status[i]] = (status[i+1], status[i+2])
88 88
89 89 return parents
90 90
91 91 def save(self):
92 92 lock = self.repo.lock()
93 93
94 94 if not os.path.isdir(self.path):
95 95 os.mkdir(self.path)
96 96 statusfile = self.opener('status', 'wb')
97 97
98 98 out = [hex(n.node()) for n in self.wctx.parents()]
99 99 out.append(str(len(self.conflicts)))
100 100 conflicts = self.conflicts.items()
101 101 conflicts.sort()
102 102 for fw, fd_fo in conflicts:
103 103 out.append(fw)
104 104 out.extend(fd_fo)
105 105 out.extend(self.resolved)
106 106
107 107 statusfile.write('\0'.join(out))
108 108
109 109 def remaining(self):
110 110 return [f for f in self.conflicts if f not in self.resolved]
111 111
112 112 def filemerge(self, fn):
113 113 wlock = self.repo.wlock()
114 114
115 115 (fd, fo) = self.conflicts[fn]
116 116 p2 = self.wctx.parents()[1]
117 117 return merge.filemerge(self.repo, fn, fd, fo, self.wctx, p2)
118 118
119 119 def start(self, rev=None):
120 120 _filemerge = merge.filemerge
121 121 def filemerge(repo, fw, fd, fo, wctx, mctx):
122 122 self.conflicts[fw] = (fd, fo)
123 123
124 124 merge.filemerge = filemerge
125 125 commands.merge(self.ui, self.repo, rev=rev)
126 126 merge.filemerge = _filemerge
127 127
128 128 self.wctx = self.repo.workingctx()
129 129 self.save()
130 130
131 131 def resume(self):
132 132 self.load()
133 133
134 134 dp = self.repo.dirstate.parents()
135 135 p1, p2 = self.wctx.parents()
136 136 if p1.node() != dp[0] or p2.node() != dp[1]:
137 137 raise util.Abort('imerge state does not match working directory')
138 138
139 139 def next(self):
140 140 remaining = self.remaining()
141 141 return remaining and remaining[0]
142 142
143 143 def resolve(self, files):
144 144 resolved = dict.fromkeys(self.resolved)
145 145 for fn in files:
146 146 if fn not in self.conflicts:
147 147 raise util.Abort('%s is not in the merge set' % fn)
148 148 resolved[fn] = True
149 149 self.resolved = resolved.keys()
150 150 self.resolved.sort()
151 151 self.save()
152 152 return 0
153 153
154 154 def unresolve(self, files):
155 155 resolved = dict.fromkeys(self.resolved)
156 156 for fn in files:
157 157 if fn not in resolved:
158 158 raise util.Abort('%s is not resolved' % fn)
159 159 del resolved[fn]
160 160 self.resolved = resolved.keys()
161 161 self.resolved.sort()
162 162 self.save()
163 163 return 0
164 164
165 165 def pickle(self, dest):
166 166 '''write current merge state to file to be resumed elsewhere'''
167 167 state = ImergeStateFile(self)
168 168 return state.save(dest)
169 169
170 170 def unpickle(self, source):
171 171 '''read merge state from file'''
172 172 state = ImergeStateFile(self)
173 173 return state.load(source)
174 174
175 175 def load(im, source):
176 176 if im.merging():
177 177 raise util.Abort('there is already a merge in progress '
178 178 '(update -C <rev> to abort it)' )
179 179 m, a, r, d = im.repo.status()[:4]
180 180 if m or a or r or d:
181 181 raise util.Abort('working directory has uncommitted changes')
182 182
183 183 rc = im.unpickle(source)
184 184 if not rc:
185 185 status(im)
186 186 return rc
187 187
188 188 def merge_(im, filename=None):
189 189 if not filename:
190 190 filename = im.next()
191 191 if not filename:
192 192 im.ui.write('all conflicts resolved\n')
193 193 return 0
194 194
195 195 rc = im.filemerge(filename)
196 196 if not rc:
197 197 im.resolve([filename])
198 198 if not im.next():
199 199 im.ui.write('all conflicts resolved\n')
200 200 return 0
201 201 return rc
202 202
203 203 def next(im):
204 204 n = im.next()
205 205 if n:
206 206 im.ui.write('%s\n' % n)
207 207 else:
208 208 im.ui.write('all conflicts resolved\n')
209 209 return 0
210 210
211 211 def resolve(im, *files):
212 212 if not files:
213 213 raise util.Abort('resolve requires at least one filename')
214 214 return im.resolve(files)
215 215
216 216 def save(im, dest):
217 217 return im.pickle(dest)
218 218
219 219 def status(im, **opts):
220 220 if not opts.get('resolved') and not opts.get('unresolved'):
221 221 opts['resolved'] = True
222 222 opts['unresolved'] = True
223 223
224 224 if im.ui.verbose:
225 225 p1, p2 = [short(p.node()) for p in im.wctx.parents()]
226 226 im.ui.note(_('merging %s and %s\n') % (p1, p2))
227 227
228 228 conflicts = im.conflicts.keys()
229 229 conflicts.sort()
230 230 remaining = dict.fromkeys(im.remaining())
231 231 st = []
232 232 for fn in conflicts:
233 233 if opts.get('no_status'):
234 234 mode = ''
235 235 elif fn in remaining:
236 236 mode = 'U '
237 237 else:
238 238 mode = 'R '
239 239 if ((opts.get('resolved') and fn not in remaining)
240 240 or (opts.get('unresolved') and fn in remaining)):
241 241 st.append((mode, fn))
242 242 st.sort()
243 243 for (mode, fn) in st:
244 244 if im.ui.verbose:
245 245 fo, fd = im.conflicts[fn]
246 246 if fd != fn:
247 247 fn = '%s (%s)' % (fn, fd)
248 248 im.ui.write('%s%s\n' % (mode, fn))
249 249 if opts.get('unresolved') and not remaining:
250 250 im.ui.write(_('all conflicts resolved\n'))
251 251
252 252 return 0
253 253
254 254 def unresolve(im, *files):
255 255 if not files:
256 256 raise util.Abort('unresolve requires at least one filename')
257 257 return im.unresolve(files)
258 258
259 259 subcmdtable = {
260 260 'load': (load, []),
261 261 'merge': (merge_, []),
262 262 'next': (next, []),
263 263 'resolve': (resolve, []),
264 264 'save': (save, []),
265 265 'status': (status,
266 266 [('n', 'no-status', None, _('hide status prefix')),
267 267 ('', 'resolved', None, _('only show resolved conflicts')),
268 268 ('', 'unresolved', None, _('only show unresolved conflicts'))]),
269 269 'unresolve': (unresolve, [])
270 270 }
271 271
272 272 def dispatch(im, args, opts):
273 273 def complete(s, choices):
274 274 candidates = []
275 275 for choice in choices:
276 276 if choice.startswith(s):
277 277 candidates.append(choice)
278 278 return candidates
279 279
280 280 c, args = args[0], list(args[1:])
281 281 cmd = complete(c, subcmdtable.keys())
282 282 if not cmd:
283 283 raise cmdutil.UnknownCommand('imerge ' + c)
284 284 if len(cmd) > 1:
285 285 cmd.sort()
286 286 raise cmdutil.AmbiguousCommand('imerge ' + c, cmd)
287 287 cmd = cmd[0]
288 288
289 289 func, optlist = subcmdtable[cmd]
290 290 opts = {}
291 291 try:
292 292 args = fancyopts.fancyopts(args, optlist, opts)
293 293 return func(im, *args, **opts)
294 294 except fancyopts.getopt.GetoptError, inst:
295 295 raise cmdutil.ParseError('imerge', '%s: %s' % (cmd, inst))
296 296 except TypeError:
297 297 raise cmdutil.ParseError('imerge', _('%s: invalid arguments') % cmd)
298 298
299 299 def imerge(ui, repo, *args, **opts):
300 300 '''interactive merge
301 301
302 302 imerge lets you split a merge into pieces. When you start a merge
303 303 with imerge, the names of all files with conflicts are recorded.
304 304 You can then merge any of these files, and if the merge is
305 305 successful, they will be marked as resolved. When all files are
306 306 resolved, the merge is complete.
307 307
308 308 If no merge is in progress, hg imerge [rev] will merge the working
309 309 directory with rev (defaulting to the other head if the repository
310 310 only has two heads). You may also resume a saved merge with
311 311 hg imerge load <file>.
312 312
313 313 If a merge is in progress, hg imerge will default to merging the
314 314 next unresolved file.
315 315
316 316 The following subcommands are available:
317 317
318 318 status:
319 319 show the current state of the merge
320 320 next:
321 321 show the next unresolved file merge
322 322 merge [<file>]:
323 323 merge <file>. If the file merge is successful, the file will be
324 324 recorded as resolved. If no file is given, the next unresolved
325 325 file will be merged.
326 326 resolve <file>...:
327 327 mark files as successfully merged
328 328 unresolve <file>...:
329 329 mark files as requiring merging.
330 330 save <file>:
331 331 save the state of the merge to a file to be resumed elsewhere
332 332 load <file>:
333 333 load the state of the merge from a file created by save
334 334 '''
335 335
336 336 im = Imerge(ui, repo)
337 337
338 338 if im.merging():
339 339 im.resume()
340 340 else:
341 341 rev = opts.get('rev')
342 342 if rev and args:
343 343 raise util.Abort('please specify just one revision')
344 344
345 345 if len(args) == 2 and args[0] == 'load':
346 346 pass
347 347 else:
348 348 if args:
349 349 rev = args[0]
350 350 im.start(rev=rev)
351 351 args = ['status']
352 352
353 353 if not args:
354 354 args = ['merge']
355 355
356 356 return dispatch(im, args, opts)
357 357
358 358 cmdtable = {
359 359 '^imerge':
360 360 (imerge,
361 361 [('r', 'rev', '', _('revision to merge'))], 'hg imerge [command]')
362 362 }
General Comments 0
You need to be logged in to leave comments. Login now