Show More
@@ -1,548 +1,551 b'' | |||
|
1 | 1 | # record.py |
|
2 | 2 | # |
|
3 | 3 | # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com> |
|
4 | 4 | # |
|
5 | 5 | # This software may be used and distributed according to the terms of the |
|
6 | 6 | # GNU General Public License version 2, incorporated herein by reference. |
|
7 | 7 | |
|
8 | 8 | '''commands to interactively select changes for commit/qrefresh''' |
|
9 | 9 | |
|
10 | 10 | from mercurial.i18n import gettext, _ |
|
11 | 11 | from mercurial import cmdutil, commands, extensions, hg, mdiff, patch |
|
12 | 12 | from mercurial import util |
|
13 | 13 | import copy, cStringIO, errno, operator, os, re, tempfile |
|
14 | 14 | |
|
15 | 15 | lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)') |
|
16 | 16 | |
|
17 | 17 | def scanpatch(fp): |
|
18 | 18 | """like patch.iterhunks, but yield different events |
|
19 | 19 | |
|
20 | 20 | - ('file', [header_lines + fromfile + tofile]) |
|
21 | 21 | - ('context', [context_lines]) |
|
22 | 22 | - ('hunk', [hunk_lines]) |
|
23 | 23 | - ('range', (-start,len, +start,len, diffp)) |
|
24 | 24 | """ |
|
25 | 25 | lr = patch.linereader(fp) |
|
26 | 26 | |
|
27 | 27 | def scanwhile(first, p): |
|
28 | 28 | """scan lr while predicate holds""" |
|
29 | 29 | lines = [first] |
|
30 | 30 | while True: |
|
31 | 31 | line = lr.readline() |
|
32 | 32 | if not line: |
|
33 | 33 | break |
|
34 | 34 | if p(line): |
|
35 | 35 | lines.append(line) |
|
36 | 36 | else: |
|
37 | 37 | lr.push(line) |
|
38 | 38 | break |
|
39 | 39 | return lines |
|
40 | 40 | |
|
41 | 41 | while True: |
|
42 | 42 | line = lr.readline() |
|
43 | 43 | if not line: |
|
44 | 44 | break |
|
45 | 45 | if line.startswith('diff --git a/'): |
|
46 | 46 | def notheader(line): |
|
47 | 47 | s = line.split(None, 1) |
|
48 | 48 | return not s or s[0] not in ('---', 'diff') |
|
49 | 49 | header = scanwhile(line, notheader) |
|
50 | 50 | fromfile = lr.readline() |
|
51 | 51 | if fromfile.startswith('---'): |
|
52 | 52 | tofile = lr.readline() |
|
53 | 53 | header += [fromfile, tofile] |
|
54 | 54 | else: |
|
55 | 55 | lr.push(fromfile) |
|
56 | 56 | yield 'file', header |
|
57 | 57 | elif line[0] == ' ': |
|
58 | 58 | yield 'context', scanwhile(line, lambda l: l[0] in ' \\') |
|
59 | 59 | elif line[0] in '-+': |
|
60 | 60 | yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\') |
|
61 | 61 | else: |
|
62 | 62 | m = lines_re.match(line) |
|
63 | 63 | if m: |
|
64 | 64 | yield 'range', m.groups() |
|
65 | 65 | else: |
|
66 | 66 | raise patch.PatchError('unknown patch content: %r' % line) |
|
67 | 67 | |
|
68 | 68 | class header(object): |
|
69 | 69 | """patch header |
|
70 | 70 | |
|
71 | 71 | XXX shoudn't we move this to mercurial/patch.py ? |
|
72 | 72 | """ |
|
73 | 73 | diff_re = re.compile('diff --git a/(.*) b/(.*)$') |
|
74 | 74 | allhunks_re = re.compile('(?:index|new file|deleted file) ') |
|
75 | 75 | pretty_re = re.compile('(?:new file|deleted file) ') |
|
76 | 76 | special_re = re.compile('(?:index|new|deleted|copy|rename) ') |
|
77 | 77 | |
|
78 | 78 | def __init__(self, header): |
|
79 | 79 | self.header = header |
|
80 | 80 | self.hunks = [] |
|
81 | 81 | |
|
82 | 82 | def binary(self): |
|
83 | 83 | for h in self.header: |
|
84 | 84 | if h.startswith('index '): |
|
85 | 85 | return True |
|
86 | 86 | |
|
87 | 87 | def pretty(self, fp): |
|
88 | 88 | for h in self.header: |
|
89 | 89 | if h.startswith('index '): |
|
90 | 90 | fp.write(_('this modifies a binary file (all or nothing)\n')) |
|
91 | 91 | break |
|
92 | 92 | if self.pretty_re.match(h): |
|
93 | 93 | fp.write(h) |
|
94 | 94 | if self.binary(): |
|
95 | 95 | fp.write(_('this is a binary file\n')) |
|
96 | 96 | break |
|
97 | 97 | if h.startswith('---'): |
|
98 | 98 | fp.write(_('%d hunks, %d lines changed\n') % |
|
99 | 99 | (len(self.hunks), |
|
100 | 100 | sum([h.added + h.removed for h in self.hunks]))) |
|
101 | 101 | break |
|
102 | 102 | fp.write(h) |
|
103 | 103 | |
|
104 | 104 | def write(self, fp): |
|
105 | 105 | fp.write(''.join(self.header)) |
|
106 | 106 | |
|
107 | 107 | def allhunks(self): |
|
108 | 108 | for h in self.header: |
|
109 | 109 | if self.allhunks_re.match(h): |
|
110 | 110 | return True |
|
111 | 111 | |
|
112 | 112 | def files(self): |
|
113 | 113 | fromfile, tofile = self.diff_re.match(self.header[0]).groups() |
|
114 | 114 | if fromfile == tofile: |
|
115 | 115 | return [fromfile] |
|
116 | 116 | return [fromfile, tofile] |
|
117 | 117 | |
|
118 | 118 | def filename(self): |
|
119 | 119 | return self.files()[-1] |
|
120 | 120 | |
|
121 | 121 | def __repr__(self): |
|
122 | 122 | return '<header %s>' % (' '.join(map(repr, self.files()))) |
|
123 | 123 | |
|
124 | 124 | def special(self): |
|
125 | 125 | for h in self.header: |
|
126 | 126 | if self.special_re.match(h): |
|
127 | 127 | return True |
|
128 | 128 | |
|
129 | 129 | def countchanges(hunk): |
|
130 | 130 | """hunk -> (n+,n-)""" |
|
131 | 131 | add = len([h for h in hunk if h[0] == '+']) |
|
132 | 132 | rem = len([h for h in hunk if h[0] == '-']) |
|
133 | 133 | return add, rem |
|
134 | 134 | |
|
135 | 135 | class hunk(object): |
|
136 | 136 | """patch hunk |
|
137 | 137 | |
|
138 | 138 | XXX shouldn't we merge this with patch.hunk ? |
|
139 | 139 | """ |
|
140 | 140 | maxcontext = 3 |
|
141 | 141 | |
|
142 | 142 | def __init__(self, header, fromline, toline, proc, before, hunk, after): |
|
143 | 143 | def trimcontext(number, lines): |
|
144 | 144 | delta = len(lines) - self.maxcontext |
|
145 | 145 | if False and delta > 0: |
|
146 | 146 | return number + delta, lines[:self.maxcontext] |
|
147 | 147 | return number, lines |
|
148 | 148 | |
|
149 | 149 | self.header = header |
|
150 | 150 | self.fromline, self.before = trimcontext(fromline, before) |
|
151 | 151 | self.toline, self.after = trimcontext(toline, after) |
|
152 | 152 | self.proc = proc |
|
153 | 153 | self.hunk = hunk |
|
154 | 154 | self.added, self.removed = countchanges(self.hunk) |
|
155 | 155 | |
|
156 | 156 | def write(self, fp): |
|
157 | 157 | delta = len(self.before) + len(self.after) |
|
158 | 158 | if self.after and self.after[-1] == '\\ No newline at end of file\n': |
|
159 | 159 | delta -= 1 |
|
160 | 160 | fromlen = delta + self.removed |
|
161 | 161 | tolen = delta + self.added |
|
162 | 162 | fp.write('@@ -%d,%d +%d,%d @@%s\n' % |
|
163 | 163 | (self.fromline, fromlen, self.toline, tolen, |
|
164 | 164 | self.proc and (' ' + self.proc))) |
|
165 | 165 | fp.write(''.join(self.before + self.hunk + self.after)) |
|
166 | 166 | |
|
167 | 167 | pretty = write |
|
168 | 168 | |
|
169 | 169 | def filename(self): |
|
170 | 170 | return self.header.filename() |
|
171 | 171 | |
|
172 | 172 | def __repr__(self): |
|
173 | 173 | return '<hunk %r@%d>' % (self.filename(), self.fromline) |
|
174 | 174 | |
|
175 | 175 | def parsepatch(fp): |
|
176 | 176 | """patch -> [] of hunks """ |
|
177 | 177 | class parser(object): |
|
178 | 178 | """patch parsing state machine""" |
|
179 | 179 | def __init__(self): |
|
180 | 180 | self.fromline = 0 |
|
181 | 181 | self.toline = 0 |
|
182 | 182 | self.proc = '' |
|
183 | 183 | self.header = None |
|
184 | 184 | self.context = [] |
|
185 | 185 | self.before = [] |
|
186 | 186 | self.hunk = [] |
|
187 | 187 | self.stream = [] |
|
188 | 188 | |
|
189 | 189 | def addrange(self, (fromstart, fromend, tostart, toend, proc)): |
|
190 | 190 | self.fromline = int(fromstart) |
|
191 | 191 | self.toline = int(tostart) |
|
192 | 192 | self.proc = proc |
|
193 | 193 | |
|
194 | 194 | def addcontext(self, context): |
|
195 | 195 | if self.hunk: |
|
196 | 196 | h = hunk(self.header, self.fromline, self.toline, self.proc, |
|
197 | 197 | self.before, self.hunk, context) |
|
198 | 198 | self.header.hunks.append(h) |
|
199 | 199 | self.stream.append(h) |
|
200 | 200 | self.fromline += len(self.before) + h.removed |
|
201 | 201 | self.toline += len(self.before) + h.added |
|
202 | 202 | self.before = [] |
|
203 | 203 | self.hunk = [] |
|
204 | 204 | self.proc = '' |
|
205 | 205 | self.context = context |
|
206 | 206 | |
|
207 | 207 | def addhunk(self, hunk): |
|
208 | 208 | if self.context: |
|
209 | 209 | self.before = self.context |
|
210 | 210 | self.context = [] |
|
211 | 211 | self.hunk = hunk |
|
212 | 212 | |
|
213 | 213 | def newfile(self, hdr): |
|
214 | 214 | self.addcontext([]) |
|
215 | 215 | h = header(hdr) |
|
216 | 216 | self.stream.append(h) |
|
217 | 217 | self.header = h |
|
218 | 218 | |
|
219 | 219 | def finished(self): |
|
220 | 220 | self.addcontext([]) |
|
221 | 221 | return self.stream |
|
222 | 222 | |
|
223 | 223 | transitions = { |
|
224 | 224 | 'file': {'context': addcontext, |
|
225 | 225 | 'file': newfile, |
|
226 | 226 | 'hunk': addhunk, |
|
227 | 227 | 'range': addrange}, |
|
228 | 228 | 'context': {'file': newfile, |
|
229 | 229 | 'hunk': addhunk, |
|
230 | 230 | 'range': addrange}, |
|
231 | 231 | 'hunk': {'context': addcontext, |
|
232 | 232 | 'file': newfile, |
|
233 | 233 | 'range': addrange}, |
|
234 | 234 | 'range': {'context': addcontext, |
|
235 | 235 | 'hunk': addhunk}, |
|
236 | 236 | } |
|
237 | 237 | |
|
238 | 238 | p = parser() |
|
239 | 239 | |
|
240 | 240 | state = 'context' |
|
241 | 241 | for newstate, data in scanpatch(fp): |
|
242 | 242 | try: |
|
243 | 243 | p.transitions[state][newstate](p, data) |
|
244 | 244 | except KeyError: |
|
245 | 245 | raise patch.PatchError('unhandled transition: %s -> %s' % |
|
246 | 246 | (state, newstate)) |
|
247 | 247 | state = newstate |
|
248 | 248 | return p.finished() |
|
249 | 249 | |
|
250 | 250 | def filterpatch(ui, chunks): |
|
251 | 251 | """Interactively filter patch chunks into applied-only chunks""" |
|
252 | 252 | chunks = list(chunks) |
|
253 | 253 | chunks.reverse() |
|
254 | 254 | seen = set() |
|
255 | 255 | def consumefile(): |
|
256 | 256 | """fetch next portion from chunks until a 'header' is seen |
|
257 | 257 | NB: header == new-file mark |
|
258 | 258 | """ |
|
259 | 259 | consumed = [] |
|
260 | 260 | while chunks: |
|
261 | 261 | if isinstance(chunks[-1], header): |
|
262 | 262 | break |
|
263 | 263 | else: |
|
264 | 264 | consumed.append(chunks.pop()) |
|
265 | 265 | return consumed |
|
266 | 266 | |
|
267 | 267 | resp_all = [None] # this two are changed from inside prompt, |
|
268 | 268 | resp_file = [None] # so can't be usual variables |
|
269 | 269 | applied = {} # 'filename' -> [] of chunks |
|
270 | 270 | def prompt(query): |
|
271 | 271 | """prompt query, and process base inputs |
|
272 | 272 | |
|
273 | 273 | - y/n for the rest of file |
|
274 | 274 | - y/n for the rest |
|
275 | 275 | - ? (help) |
|
276 | 276 | - q (quit) |
|
277 | 277 | |
|
278 | 278 | else, input is returned to the caller. |
|
279 | 279 | """ |
|
280 | 280 | if resp_all[0] is not None: |
|
281 | 281 | return resp_all[0] |
|
282 | 282 | if resp_file[0] is not None: |
|
283 | 283 | return resp_file[0] |
|
284 | 284 | while True: |
|
285 | 285 | resps = _('[Ynsfdaq?]') |
|
286 | 286 | choices = (_('&Yes, record this change'), |
|
287 | 287 | _('&No, skip this change'), |
|
288 | 288 | _('&Skip remaining changes to this file'), |
|
289 | 289 | _('Record remaining changes to this &file'), |
|
290 | 290 | _('&Done, skip remaining changes and files'), |
|
291 | 291 | _('Record &all changes to all remaining files'), |
|
292 | 292 | _('&Quit, recording no changes'), |
|
293 | 293 | _('&?')) |
|
294 |
r = |
|
|
295 | or _('y')).lower() | |
|
296 | if r == _('?'): | |
|
294 | r = ui.promptchoice("%s %s " % (query, resps), choices) | |
|
295 | if r == 7: # ? | |
|
297 | 296 | doc = gettext(record.__doc__) |
|
298 | 297 | c = doc.find(_('y - record this change')) |
|
299 | 298 | for l in doc[c:].splitlines(): |
|
300 | 299 | if l: ui.write(l.strip(), '\n') |
|
301 | 300 | continue |
|
302 |
elif r == |
|
|
303 |
r |
|
|
304 |
elif r == |
|
|
305 |
r |
|
|
306 |
elif r == |
|
|
307 |
r = resp_ |
|
|
308 |
elif r == |
|
|
309 |
r = resp_ |
|
|
310 |
elif r == |
|
|
301 | elif r == 0: # yes | |
|
302 | ret = 'y' | |
|
303 | elif r == 1: # no | |
|
304 | ret = 'n' | |
|
305 | elif r == 2: # Skip | |
|
306 | ret = resp_file[0] = 'n' | |
|
307 | elif r == 3: # file (Record remaining) | |
|
308 | ret = resp_file[0] = 'y' | |
|
309 | elif r == 4: # done, skip remaining | |
|
310 | ret = resp_all[0] = 'n' | |
|
311 | elif r == 5: # all | |
|
312 | ret = resp_all[0] = 'y' | |
|
313 | elif r == 6: # quit | |
|
311 | 314 | raise util.Abort(_('user quit')) |
|
312 | return r | |
|
315 | return ret | |
|
313 | 316 | pos, total = 0, len(chunks) - 1 |
|
314 | 317 | while chunks: |
|
315 | 318 | chunk = chunks.pop() |
|
316 | 319 | if isinstance(chunk, header): |
|
317 | 320 | # new-file mark |
|
318 | 321 | resp_file = [None] |
|
319 | 322 | fixoffset = 0 |
|
320 | 323 | hdr = ''.join(chunk.header) |
|
321 | 324 | if hdr in seen: |
|
322 | 325 | consumefile() |
|
323 | 326 | continue |
|
324 | 327 | seen.add(hdr) |
|
325 | 328 | if resp_all[0] is None: |
|
326 | 329 | chunk.pretty(ui) |
|
327 | 330 | r = prompt(_('examine changes to %s?') % |
|
328 | 331 | _(' and ').join(map(repr, chunk.files()))) |
|
329 | 332 | if r == _('y'): |
|
330 | 333 | applied[chunk.filename()] = [chunk] |
|
331 | 334 | if chunk.allhunks(): |
|
332 | 335 | applied[chunk.filename()] += consumefile() |
|
333 | 336 | else: |
|
334 | 337 | consumefile() |
|
335 | 338 | else: |
|
336 | 339 | # new hunk |
|
337 | 340 | if resp_file[0] is None and resp_all[0] is None: |
|
338 | 341 | chunk.pretty(ui) |
|
339 | 342 | r = total == 1 and prompt(_('record this change to %r?') % |
|
340 | 343 | chunk.filename()) \ |
|
341 | 344 | or prompt(_('record change %d/%d to %r?') % |
|
342 | 345 | (pos, total, chunk.filename())) |
|
343 | 346 | if r == _('y'): |
|
344 | 347 | if fixoffset: |
|
345 | 348 | chunk = copy.copy(chunk) |
|
346 | 349 | chunk.toline += fixoffset |
|
347 | 350 | applied[chunk.filename()].append(chunk) |
|
348 | 351 | else: |
|
349 | 352 | fixoffset += chunk.removed - chunk.added |
|
350 | 353 | pos = pos + 1 |
|
351 | 354 | return reduce(operator.add, [h for h in applied.itervalues() |
|
352 | 355 | if h[0].special() or len(h) > 1], []) |
|
353 | 356 | |
|
354 | 357 | def record(ui, repo, *pats, **opts): |
|
355 | 358 | '''interactively select changes to commit |
|
356 | 359 | |
|
357 | 360 | If a list of files is omitted, all changes reported by "hg status" |
|
358 | 361 | will be candidates for recording. |
|
359 | 362 | |
|
360 | 363 | See 'hg help dates' for a list of formats valid for -d/--date. |
|
361 | 364 | |
|
362 | 365 | You will be prompted for whether to record changes to each |
|
363 | 366 | modified file, and for files with multiple changes, for each |
|
364 | 367 | change to use. For each query, the following responses are |
|
365 | 368 | possible: |
|
366 | 369 | |
|
367 | 370 | y - record this change |
|
368 | 371 | n - skip this change |
|
369 | 372 | |
|
370 | 373 | s - skip remaining changes to this file |
|
371 | 374 | f - record remaining changes to this file |
|
372 | 375 | |
|
373 | 376 | d - done, skip remaining changes and files |
|
374 | 377 | a - record all changes to all remaining files |
|
375 | 378 | q - quit, recording no changes |
|
376 | 379 | |
|
377 | 380 | ? - display help''' |
|
378 | 381 | |
|
379 | 382 | def record_committer(ui, repo, pats, opts): |
|
380 | 383 | commands.commit(ui, repo, *pats, **opts) |
|
381 | 384 | |
|
382 | 385 | dorecord(ui, repo, record_committer, *pats, **opts) |
|
383 | 386 | |
|
384 | 387 | |
|
385 | 388 | def qrecord(ui, repo, patch, *pats, **opts): |
|
386 | 389 | '''interactively record a new patch |
|
387 | 390 | |
|
388 | 391 | See 'hg help qnew' & 'hg help record' for more information and |
|
389 | 392 | usage. |
|
390 | 393 | ''' |
|
391 | 394 | |
|
392 | 395 | try: |
|
393 | 396 | mq = extensions.find('mq') |
|
394 | 397 | except KeyError: |
|
395 | 398 | raise util.Abort(_("'mq' extension not loaded")) |
|
396 | 399 | |
|
397 | 400 | def qrecord_committer(ui, repo, pats, opts): |
|
398 | 401 | mq.new(ui, repo, patch, *pats, **opts) |
|
399 | 402 | |
|
400 | 403 | opts = opts.copy() |
|
401 | 404 | opts['force'] = True # always 'qnew -f' |
|
402 | 405 | dorecord(ui, repo, qrecord_committer, *pats, **opts) |
|
403 | 406 | |
|
404 | 407 | |
|
405 | 408 | def dorecord(ui, repo, committer, *pats, **opts): |
|
406 | 409 | if not ui.interactive(): |
|
407 | 410 | raise util.Abort(_('running non-interactively, use commit instead')) |
|
408 | 411 | |
|
409 | 412 | def recordfunc(ui, repo, message, match, opts): |
|
410 | 413 | """This is generic record driver. |
|
411 | 414 | |
|
412 | 415 | It's job is to interactively filter local changes, and accordingly |
|
413 | 416 | prepare working dir into a state, where the job can be delegated to |
|
414 | 417 | non-interactive commit command such as 'commit' or 'qrefresh'. |
|
415 | 418 | |
|
416 | 419 | After the actual job is done by non-interactive command, working dir |
|
417 | 420 | state is restored to original. |
|
418 | 421 | |
|
419 | 422 | In the end we'll record intresting changes, and everything else will be |
|
420 | 423 | left in place, so the user can continue his work. |
|
421 | 424 | """ |
|
422 | 425 | |
|
423 | 426 | changes = repo.status(match=match)[:3] |
|
424 | 427 | diffopts = mdiff.diffopts(git=True, nodates=True) |
|
425 | 428 | chunks = patch.diff(repo, changes=changes, opts=diffopts) |
|
426 | 429 | fp = cStringIO.StringIO() |
|
427 | 430 | fp.write(''.join(chunks)) |
|
428 | 431 | fp.seek(0) |
|
429 | 432 | |
|
430 | 433 | # 1. filter patch, so we have intending-to apply subset of it |
|
431 | 434 | chunks = filterpatch(ui, parsepatch(fp)) |
|
432 | 435 | del fp |
|
433 | 436 | |
|
434 | 437 | contenders = set() |
|
435 | 438 | for h in chunks: |
|
436 | 439 | try: contenders.update(set(h.files())) |
|
437 | 440 | except AttributeError: pass |
|
438 | 441 | |
|
439 | 442 | changed = changes[0] + changes[1] + changes[2] |
|
440 | 443 | newfiles = [f for f in changed if f in contenders] |
|
441 | 444 | if not newfiles: |
|
442 | 445 | ui.status(_('no changes to record\n')) |
|
443 | 446 | return 0 |
|
444 | 447 | |
|
445 | 448 | modified = set(changes[0]) |
|
446 | 449 | |
|
447 | 450 | # 2. backup changed files, so we can restore them in the end |
|
448 | 451 | backups = {} |
|
449 | 452 | backupdir = repo.join('record-backups') |
|
450 | 453 | try: |
|
451 | 454 | os.mkdir(backupdir) |
|
452 | 455 | except OSError, err: |
|
453 | 456 | if err.errno != errno.EEXIST: |
|
454 | 457 | raise |
|
455 | 458 | try: |
|
456 | 459 | # backup continues |
|
457 | 460 | for f in newfiles: |
|
458 | 461 | if f not in modified: |
|
459 | 462 | continue |
|
460 | 463 | fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.', |
|
461 | 464 | dir=backupdir) |
|
462 | 465 | os.close(fd) |
|
463 | 466 | ui.debug(_('backup %r as %r\n') % (f, tmpname)) |
|
464 | 467 | util.copyfile(repo.wjoin(f), tmpname) |
|
465 | 468 | backups[f] = tmpname |
|
466 | 469 | |
|
467 | 470 | fp = cStringIO.StringIO() |
|
468 | 471 | for c in chunks: |
|
469 | 472 | if c.filename() in backups: |
|
470 | 473 | c.write(fp) |
|
471 | 474 | dopatch = fp.tell() |
|
472 | 475 | fp.seek(0) |
|
473 | 476 | |
|
474 | 477 | # 3a. apply filtered patch to clean repo (clean) |
|
475 | 478 | if backups: |
|
476 | 479 | hg.revert(repo, repo.dirstate.parents()[0], backups.has_key) |
|
477 | 480 | |
|
478 | 481 | # 3b. (apply) |
|
479 | 482 | if dopatch: |
|
480 | 483 | try: |
|
481 | 484 | ui.debug(_('applying patch\n')) |
|
482 | 485 | ui.debug(fp.getvalue()) |
|
483 | 486 | pfiles = {} |
|
484 | 487 | patch.internalpatch(fp, ui, 1, repo.root, files=pfiles, |
|
485 | 488 | eolmode=None) |
|
486 | 489 | patch.updatedir(ui, repo, pfiles) |
|
487 | 490 | except patch.PatchError, err: |
|
488 | 491 | s = str(err) |
|
489 | 492 | if s: |
|
490 | 493 | raise util.Abort(s) |
|
491 | 494 | else: |
|
492 | 495 | raise util.Abort(_('patch failed to apply')) |
|
493 | 496 | del fp |
|
494 | 497 | |
|
495 | 498 | # 4. We prepared working directory according to filtered patch. |
|
496 | 499 | # Now is the time to delegate the job to commit/qrefresh or the like! |
|
497 | 500 | |
|
498 | 501 | # it is important to first chdir to repo root -- we'll call a |
|
499 | 502 | # highlevel command with list of pathnames relative to repo root |
|
500 | 503 | cwd = os.getcwd() |
|
501 | 504 | os.chdir(repo.root) |
|
502 | 505 | try: |
|
503 | 506 | committer(ui, repo, newfiles, opts) |
|
504 | 507 | finally: |
|
505 | 508 | os.chdir(cwd) |
|
506 | 509 | |
|
507 | 510 | return 0 |
|
508 | 511 | finally: |
|
509 | 512 | # 5. finally restore backed-up files |
|
510 | 513 | try: |
|
511 | 514 | for realname, tmpname in backups.iteritems(): |
|
512 | 515 | ui.debug(_('restoring %r to %r\n') % (tmpname, realname)) |
|
513 | 516 | util.copyfile(tmpname, repo.wjoin(realname)) |
|
514 | 517 | os.unlink(tmpname) |
|
515 | 518 | os.rmdir(backupdir) |
|
516 | 519 | except OSError: |
|
517 | 520 | pass |
|
518 | 521 | return cmdutil.commit(ui, repo, recordfunc, pats, opts) |
|
519 | 522 | |
|
520 | 523 | cmdtable = { |
|
521 | 524 | "record": |
|
522 | 525 | (record, |
|
523 | 526 | |
|
524 | 527 | # add commit options |
|
525 | 528 | commands.table['^commit|ci'][1], |
|
526 | 529 | |
|
527 | 530 | _('hg record [OPTION]... [FILE]...')), |
|
528 | 531 | } |
|
529 | 532 | |
|
530 | 533 | |
|
531 | 534 | def extsetup(): |
|
532 | 535 | try: |
|
533 | 536 | mq = extensions.find('mq') |
|
534 | 537 | except KeyError: |
|
535 | 538 | return |
|
536 | 539 | |
|
537 | 540 | qcmdtable = { |
|
538 | 541 | "qrecord": |
|
539 | 542 | (qrecord, |
|
540 | 543 | |
|
541 | 544 | # add qnew options, except '--force' |
|
542 | 545 | [opt for opt in mq.cmdtable['qnew'][1] if opt[1] != 'force'], |
|
543 | 546 | |
|
544 | 547 | _('hg qrecord [OPTION]... PATCH [FILE]...')), |
|
545 | 548 | } |
|
546 | 549 | |
|
547 | 550 | cmdtable.update(qcmdtable) |
|
548 | 551 |
@@ -1,231 +1,231 b'' | |||
|
1 | 1 | # filemerge.py - file-level merge handling for Mercurial |
|
2 | 2 | # |
|
3 | 3 | # Copyright 2006, 2007, 2008 Matt Mackall <mpm@selenic.com> |
|
4 | 4 | # |
|
5 | 5 | # This software may be used and distributed according to the terms of the |
|
6 | 6 | # GNU General Public License version 2, incorporated herein by reference. |
|
7 | 7 | |
|
8 | 8 | from node import short |
|
9 | 9 | from i18n import _ |
|
10 | 10 | import util, simplemerge, match |
|
11 | 11 | import os, tempfile, re, filecmp |
|
12 | 12 | |
|
13 | 13 | def _toolstr(ui, tool, part, default=""): |
|
14 | 14 | return ui.config("merge-tools", tool + "." + part, default) |
|
15 | 15 | |
|
16 | 16 | def _toolbool(ui, tool, part, default=False): |
|
17 | 17 | return ui.configbool("merge-tools", tool + "." + part, default) |
|
18 | 18 | |
|
19 | 19 | _internal = ['internal:' + s |
|
20 | 20 | for s in 'fail local other merge prompt dump'.split()] |
|
21 | 21 | |
|
22 | 22 | def _findtool(ui, tool): |
|
23 | 23 | if tool in _internal: |
|
24 | 24 | return tool |
|
25 | 25 | k = _toolstr(ui, tool, "regkey") |
|
26 | 26 | if k: |
|
27 | 27 | p = util.lookup_reg(k, _toolstr(ui, tool, "regname")) |
|
28 | 28 | if p: |
|
29 | 29 | p = util.find_exe(p + _toolstr(ui, tool, "regappend")) |
|
30 | 30 | if p: |
|
31 | 31 | return p |
|
32 | 32 | return util.find_exe(_toolstr(ui, tool, "executable", tool)) |
|
33 | 33 | |
|
34 | 34 | def _picktool(repo, ui, path, binary, symlink): |
|
35 | 35 | def check(tool, pat, symlink, binary): |
|
36 | 36 | tmsg = tool |
|
37 | 37 | if pat: |
|
38 | 38 | tmsg += " specified for " + pat |
|
39 | 39 | if not _findtool(ui, tool): |
|
40 | 40 | if pat: # explicitly requested tool deserves a warning |
|
41 | 41 | ui.warn(_("couldn't find merge tool %s\n") % tmsg) |
|
42 | 42 | else: # configured but non-existing tools are more silent |
|
43 | 43 | ui.note(_("couldn't find merge tool %s\n") % tmsg) |
|
44 | 44 | elif symlink and not _toolbool(ui, tool, "symlink"): |
|
45 | 45 | ui.warn(_("tool %s can't handle symlinks\n") % tmsg) |
|
46 | 46 | elif binary and not _toolbool(ui, tool, "binary"): |
|
47 | 47 | ui.warn(_("tool %s can't handle binary\n") % tmsg) |
|
48 | 48 | elif not util.gui() and _toolbool(ui, tool, "gui"): |
|
49 | 49 | ui.warn(_("tool %s requires a GUI\n") % tmsg) |
|
50 | 50 | else: |
|
51 | 51 | return True |
|
52 | 52 | return False |
|
53 | 53 | |
|
54 | 54 | # HGMERGE takes precedence |
|
55 | 55 | hgmerge = os.environ.get("HGMERGE") |
|
56 | 56 | if hgmerge: |
|
57 | 57 | return (hgmerge, hgmerge) |
|
58 | 58 | |
|
59 | 59 | # then patterns |
|
60 | 60 | for pat, tool in ui.configitems("merge-patterns"): |
|
61 | 61 | mf = match.match(repo.root, '', [pat]) |
|
62 | 62 | if mf(path) and check(tool, pat, symlink, False): |
|
63 | 63 | toolpath = _findtool(ui, tool) |
|
64 | 64 | return (tool, '"' + toolpath + '"') |
|
65 | 65 | |
|
66 | 66 | # then merge tools |
|
67 | 67 | tools = {} |
|
68 | 68 | for k,v in ui.configitems("merge-tools"): |
|
69 | 69 | t = k.split('.')[0] |
|
70 | 70 | if t not in tools: |
|
71 | 71 | tools[t] = int(_toolstr(ui, t, "priority", "0")) |
|
72 | 72 | names = tools.keys() |
|
73 | 73 | tools = sorted([(-p,t) for t,p in tools.items()]) |
|
74 | 74 | uimerge = ui.config("ui", "merge") |
|
75 | 75 | if uimerge: |
|
76 | 76 | if uimerge not in names: |
|
77 | 77 | return (uimerge, uimerge) |
|
78 | 78 | tools.insert(0, (None, uimerge)) # highest priority |
|
79 | 79 | tools.append((None, "hgmerge")) # the old default, if found |
|
80 | 80 | for p,t in tools: |
|
81 | 81 | if check(t, None, symlink, binary): |
|
82 | 82 | toolpath = _findtool(ui, t) |
|
83 | 83 | return (t, '"' + toolpath + '"') |
|
84 | 84 | # internal merge as last resort |
|
85 | 85 | return (not (symlink or binary) and "internal:merge" or None, None) |
|
86 | 86 | |
|
87 | 87 | def _eoltype(data): |
|
88 | 88 | "Guess the EOL type of a file" |
|
89 | 89 | if '\0' in data: # binary |
|
90 | 90 | return None |
|
91 | 91 | if '\r\n' in data: # Windows |
|
92 | 92 | return '\r\n' |
|
93 | 93 | if '\r' in data: # Old Mac |
|
94 | 94 | return '\r' |
|
95 | 95 | if '\n' in data: # UNIX |
|
96 | 96 | return '\n' |
|
97 | 97 | return None # unknown |
|
98 | 98 | |
|
99 | 99 | def _matcheol(file, origfile): |
|
100 | 100 | "Convert EOL markers in a file to match origfile" |
|
101 | 101 | tostyle = _eoltype(open(origfile, "rb").read()) |
|
102 | 102 | if tostyle: |
|
103 | 103 | data = open(file, "rb").read() |
|
104 | 104 | style = _eoltype(data) |
|
105 | 105 | if style: |
|
106 | 106 | newdata = data.replace(style, tostyle) |
|
107 | 107 | if newdata != data: |
|
108 | 108 | open(file, "wb").write(newdata) |
|
109 | 109 | |
|
110 | 110 | def filemerge(repo, mynode, orig, fcd, fco, fca): |
|
111 | 111 | """perform a 3-way merge in the working directory |
|
112 | 112 | |
|
113 | 113 | mynode = parent node before merge |
|
114 | 114 | orig = original local filename before merge |
|
115 | 115 | fco = other file context |
|
116 | 116 | fca = ancestor file context |
|
117 | 117 | fcd = local file context for current/destination file |
|
118 | 118 | """ |
|
119 | 119 | |
|
120 | 120 | def temp(prefix, ctx): |
|
121 | 121 | pre = "%s~%s." % (os.path.basename(ctx.path()), prefix) |
|
122 | 122 | (fd, name) = tempfile.mkstemp(prefix=pre) |
|
123 | 123 | data = repo.wwritedata(ctx.path(), ctx.data()) |
|
124 | 124 | f = os.fdopen(fd, "wb") |
|
125 | 125 | f.write(data) |
|
126 | 126 | f.close() |
|
127 | 127 | return name |
|
128 | 128 | |
|
129 | 129 | def isbin(ctx): |
|
130 | 130 | try: |
|
131 | 131 | return util.binary(ctx.data()) |
|
132 | 132 | except IOError: |
|
133 | 133 | return False |
|
134 | 134 | |
|
135 | 135 | if not fco.cmp(fcd.data()): # files identical? |
|
136 | 136 | return None |
|
137 | 137 | |
|
138 | 138 | ui = repo.ui |
|
139 | 139 | fd = fcd.path() |
|
140 | 140 | binary = isbin(fcd) or isbin(fco) or isbin(fca) |
|
141 | 141 | symlink = 'l' in fcd.flags() + fco.flags() |
|
142 | 142 | tool, toolpath = _picktool(repo, ui, fd, binary, symlink) |
|
143 | 143 | ui.debug(_("picked tool '%s' for %s (binary %s symlink %s)\n") % |
|
144 | 144 | (tool, fd, binary, symlink)) |
|
145 | 145 | |
|
146 | 146 | if not tool or tool == 'internal:prompt': |
|
147 | 147 | tool = "internal:local" |
|
148 | if ui.prompt(_(" no tool found to merge %s\n" | |
|
148 | if ui.promptchoice(_(" no tool found to merge %s\n" | |
|
149 | 149 | "keep (l)ocal or take (o)ther?") % fd, |
|
150 |
(_("&Local"), _("&Other")), |
|
|
150 | (_("&Local"), _("&Other")), 0): | |
|
151 | 151 | tool = "internal:other" |
|
152 | 152 | if tool == "internal:local": |
|
153 | 153 | return 0 |
|
154 | 154 | if tool == "internal:other": |
|
155 | 155 | repo.wwrite(fd, fco.data(), fco.flags()) |
|
156 | 156 | return 0 |
|
157 | 157 | if tool == "internal:fail": |
|
158 | 158 | return 1 |
|
159 | 159 | |
|
160 | 160 | # do the actual merge |
|
161 | 161 | a = repo.wjoin(fd) |
|
162 | 162 | b = temp("base", fca) |
|
163 | 163 | c = temp("other", fco) |
|
164 | 164 | out = "" |
|
165 | 165 | back = a + ".orig" |
|
166 | 166 | util.copyfile(a, back) |
|
167 | 167 | |
|
168 | 168 | if orig != fco.path(): |
|
169 | 169 | ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd)) |
|
170 | 170 | else: |
|
171 | 171 | ui.status(_("merging %s\n") % fd) |
|
172 | 172 | |
|
173 | 173 | ui.debug(_("my %s other %s ancestor %s\n") % (fcd, fco, fca)) |
|
174 | 174 | |
|
175 | 175 | # do we attempt to simplemerge first? |
|
176 | 176 | if _toolbool(ui, tool, "premerge", not (binary or symlink)): |
|
177 | 177 | r = simplemerge.simplemerge(ui, a, b, c, quiet=True) |
|
178 | 178 | if not r: |
|
179 | 179 | ui.debug(_(" premerge successful\n")) |
|
180 | 180 | os.unlink(back) |
|
181 | 181 | os.unlink(b) |
|
182 | 182 | os.unlink(c) |
|
183 | 183 | return 0 |
|
184 | 184 | util.copyfile(back, a) # restore from backup and try again |
|
185 | 185 | |
|
186 | 186 | env = dict(HG_FILE=fd, |
|
187 | 187 | HG_MY_NODE=short(mynode), |
|
188 | 188 | HG_OTHER_NODE=str(fco.changectx()), |
|
189 | 189 | HG_MY_ISLINK='l' in fcd.flags(), |
|
190 | 190 | HG_OTHER_ISLINK='l' in fco.flags(), |
|
191 | 191 | HG_BASE_ISLINK='l' in fca.flags()) |
|
192 | 192 | |
|
193 | 193 | if tool == "internal:merge": |
|
194 | 194 | r = simplemerge.simplemerge(ui, a, b, c, label=['local', 'other']) |
|
195 | 195 | elif tool == 'internal:dump': |
|
196 | 196 | a = repo.wjoin(fd) |
|
197 | 197 | util.copyfile(a, a + ".local") |
|
198 | 198 | repo.wwrite(fd + ".other", fco.data(), fco.flags()) |
|
199 | 199 | repo.wwrite(fd + ".base", fca.data(), fca.flags()) |
|
200 | 200 | return 1 # unresolved |
|
201 | 201 | else: |
|
202 | 202 | args = _toolstr(ui, tool, "args", '$local $base $other') |
|
203 | 203 | if "$output" in args: |
|
204 | 204 | out, a = a, back # read input from backup, write to original |
|
205 | 205 | replace = dict(local=a, base=b, other=c, output=out) |
|
206 | 206 | args = re.sub("\$(local|base|other|output)", |
|
207 | 207 | lambda x: '"%s"' % replace[x.group()[1:]], args) |
|
208 | 208 | r = util.system(toolpath + ' ' + args, cwd=repo.root, environ=env) |
|
209 | 209 | |
|
210 | 210 | if not r and _toolbool(ui, tool, "checkconflicts"): |
|
211 | 211 | if re.match("^(<<<<<<< .*|=======|>>>>>>> .*)$", fcd.data()): |
|
212 | 212 | r = 1 |
|
213 | 213 | |
|
214 | 214 | if not r and _toolbool(ui, tool, "checkchanged"): |
|
215 | 215 | if filecmp.cmp(repo.wjoin(fd), back): |
|
216 | if ui.prompt(_(" output file %s appears unchanged\n" | |
|
216 | if ui.promptchoice(_(" output file %s appears unchanged\n" | |
|
217 | 217 | "was merge successful (yn)?") % fd, |
|
218 |
(_("&Yes"), _("&No")), |
|
|
218 | (_("&Yes"), _("&No")), 1): | |
|
219 | 219 | r = 1 |
|
220 | 220 | |
|
221 | 221 | if _toolbool(ui, tool, "fixeol"): |
|
222 | 222 | _matcheol(repo.wjoin(fd), back) |
|
223 | 223 | |
|
224 | 224 | if r: |
|
225 | 225 | ui.warn(_("merging %s failed!\n") % fd) |
|
226 | 226 | else: |
|
227 | 227 | os.unlink(back) |
|
228 | 228 | |
|
229 | 229 | os.unlink(b) |
|
230 | 230 | os.unlink(c) |
|
231 | 231 | return r |
@@ -1,479 +1,481 b'' | |||
|
1 | 1 | # merge.py - directory-level update/merge handling for Mercurial |
|
2 | 2 | # |
|
3 | 3 | # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com> |
|
4 | 4 | # |
|
5 | 5 | # This software may be used and distributed according to the terms of the |
|
6 | 6 | # GNU General Public License version 2, incorporated herein by reference. |
|
7 | 7 | |
|
8 | 8 | from node import nullid, nullrev, hex, bin |
|
9 | 9 | from i18n import _ |
|
10 | 10 | import util, filemerge, copies, subrepo |
|
11 | 11 | import errno, os, shutil |
|
12 | 12 | |
|
13 | 13 | class mergestate(object): |
|
14 | 14 | '''track 3-way merge state of individual files''' |
|
15 | 15 | def __init__(self, repo): |
|
16 | 16 | self._repo = repo |
|
17 | 17 | self._read() |
|
18 | 18 | def reset(self, node=None): |
|
19 | 19 | self._state = {} |
|
20 | 20 | if node: |
|
21 | 21 | self._local = node |
|
22 | 22 | shutil.rmtree(self._repo.join("merge"), True) |
|
23 | 23 | def _read(self): |
|
24 | 24 | self._state = {} |
|
25 | 25 | try: |
|
26 | 26 | localnode = None |
|
27 | 27 | f = self._repo.opener("merge/state") |
|
28 | 28 | for i, l in enumerate(f): |
|
29 | 29 | if i == 0: |
|
30 | 30 | localnode = l[:-1] |
|
31 | 31 | else: |
|
32 | 32 | bits = l[:-1].split("\0") |
|
33 | 33 | self._state[bits[0]] = bits[1:] |
|
34 | 34 | self._local = bin(localnode) |
|
35 | 35 | except IOError, err: |
|
36 | 36 | if err.errno != errno.ENOENT: |
|
37 | 37 | raise |
|
38 | 38 | def _write(self): |
|
39 | 39 | f = self._repo.opener("merge/state", "w") |
|
40 | 40 | f.write(hex(self._local) + "\n") |
|
41 | 41 | for d, v in self._state.iteritems(): |
|
42 | 42 | f.write("\0".join([d] + v) + "\n") |
|
43 | 43 | def add(self, fcl, fco, fca, fd, flags): |
|
44 | 44 | hash = util.sha1(fcl.path()).hexdigest() |
|
45 | 45 | self._repo.opener("merge/" + hash, "w").write(fcl.data()) |
|
46 | 46 | self._state[fd] = ['u', hash, fcl.path(), fca.path(), |
|
47 | 47 | hex(fca.filenode()), fco.path(), flags] |
|
48 | 48 | self._write() |
|
49 | 49 | def __contains__(self, dfile): |
|
50 | 50 | return dfile in self._state |
|
51 | 51 | def __getitem__(self, dfile): |
|
52 | 52 | return self._state[dfile][0] |
|
53 | 53 | def __iter__(self): |
|
54 | 54 | l = self._state.keys() |
|
55 | 55 | l.sort() |
|
56 | 56 | for f in l: |
|
57 | 57 | yield f |
|
58 | 58 | def mark(self, dfile, state): |
|
59 | 59 | self._state[dfile][0] = state |
|
60 | 60 | self._write() |
|
61 | 61 | def resolve(self, dfile, wctx, octx): |
|
62 | 62 | if self[dfile] == 'r': |
|
63 | 63 | return 0 |
|
64 | 64 | state, hash, lfile, afile, anode, ofile, flags = self._state[dfile] |
|
65 | 65 | f = self._repo.opener("merge/" + hash) |
|
66 | 66 | self._repo.wwrite(dfile, f.read(), flags) |
|
67 | 67 | fcd = wctx[dfile] |
|
68 | 68 | fco = octx[ofile] |
|
69 | 69 | fca = self._repo.filectx(afile, fileid=anode) |
|
70 | 70 | r = filemerge.filemerge(self._repo, self._local, lfile, fcd, fco, fca) |
|
71 | 71 | if not r: |
|
72 | 72 | self.mark(dfile, 'r') |
|
73 | 73 | return r |
|
74 | 74 | |
|
75 | 75 | def _checkunknown(wctx, mctx): |
|
76 | 76 | "check for collisions between unknown files and files in mctx" |
|
77 | 77 | for f in wctx.unknown(): |
|
78 | 78 | if f in mctx and mctx[f].cmp(wctx[f].data()): |
|
79 | 79 | raise util.Abort(_("untracked file in working directory differs" |
|
80 | 80 | " from file in requested revision: '%s'") % f) |
|
81 | 81 | |
|
82 | 82 | def _checkcollision(mctx): |
|
83 | 83 | "check for case folding collisions in the destination context" |
|
84 | 84 | folded = {} |
|
85 | 85 | for fn in mctx: |
|
86 | 86 | fold = fn.lower() |
|
87 | 87 | if fold in folded: |
|
88 | 88 | raise util.Abort(_("case-folding collision between %s and %s") |
|
89 | 89 | % (fn, folded[fold])) |
|
90 | 90 | folded[fold] = fn |
|
91 | 91 | |
|
92 | 92 | def _forgetremoved(wctx, mctx, branchmerge): |
|
93 | 93 | """ |
|
94 | 94 | Forget removed files |
|
95 | 95 | |
|
96 | 96 | If we're jumping between revisions (as opposed to merging), and if |
|
97 | 97 | neither the working directory nor the target rev has the file, |
|
98 | 98 | then we need to remove it from the dirstate, to prevent the |
|
99 | 99 | dirstate from listing the file when it is no longer in the |
|
100 | 100 | manifest. |
|
101 | 101 | |
|
102 | 102 | If we're merging, and the other revision has removed a file |
|
103 | 103 | that is not present in the working directory, we need to mark it |
|
104 | 104 | as removed. |
|
105 | 105 | """ |
|
106 | 106 | |
|
107 | 107 | action = [] |
|
108 | 108 | state = branchmerge and 'r' or 'f' |
|
109 | 109 | for f in wctx.deleted(): |
|
110 | 110 | if f not in mctx: |
|
111 | 111 | action.append((f, state)) |
|
112 | 112 | |
|
113 | 113 | if not branchmerge: |
|
114 | 114 | for f in wctx.removed(): |
|
115 | 115 | if f not in mctx: |
|
116 | 116 | action.append((f, "f")) |
|
117 | 117 | |
|
118 | 118 | return action |
|
119 | 119 | |
|
120 | 120 | def manifestmerge(repo, p1, p2, pa, overwrite, partial): |
|
121 | 121 | """ |
|
122 | 122 | Merge p1 and p2 with ancestor ma and generate merge action list |
|
123 | 123 | |
|
124 | 124 | overwrite = whether we clobber working files |
|
125 | 125 | partial = function to filter file lists |
|
126 | 126 | """ |
|
127 | 127 | |
|
128 | 128 | def fmerge(f, f2, fa): |
|
129 | 129 | """merge flags""" |
|
130 | 130 | a, m, n = ma.flags(fa), m1.flags(f), m2.flags(f2) |
|
131 | 131 | if m == n: # flags agree |
|
132 | 132 | return m # unchanged |
|
133 | 133 | if m and n and not a: # flags set, don't agree, differ from parent |
|
134 | r = repo.ui.prompt( | |
|
134 | r = repo.ui.promptchoice( | |
|
135 | 135 | _(" conflicting flags for %s\n" |
|
136 | 136 | "(n)one, e(x)ec or sym(l)ink?") % f, |
|
137 |
(_("&None"), _("E&xec"), _("Sym&link")), |
|
|
138 | return r != _("n") and r or '' | |
|
137 | (_("&None"), _("E&xec"), _("Sym&link")), 0) | |
|
138 | if r == 1: return "x" # Exec | |
|
139 | if r == 2: return "l" # Symlink | |
|
140 | return "" | |
|
139 | 141 | if m and m != a: # changed from a to m |
|
140 | 142 | return m |
|
141 | 143 | if n and n != a: # changed from a to n |
|
142 | 144 | return n |
|
143 | 145 | return '' # flag was cleared |
|
144 | 146 | |
|
145 | 147 | def act(msg, m, f, *args): |
|
146 | 148 | repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m)) |
|
147 | 149 | action.append((f, m) + args) |
|
148 | 150 | |
|
149 | 151 | action, copy = [], {} |
|
150 | 152 | |
|
151 | 153 | if overwrite: |
|
152 | 154 | pa = p1 |
|
153 | 155 | elif pa == p2: # backwards |
|
154 | 156 | pa = p1.p1() |
|
155 | 157 | elif pa and repo.ui.configbool("merge", "followcopies", True): |
|
156 | 158 | dirs = repo.ui.configbool("merge", "followdirs", True) |
|
157 | 159 | copy, diverge = copies.copies(repo, p1, p2, pa, dirs) |
|
158 | 160 | for of, fl in diverge.iteritems(): |
|
159 | 161 | act("divergent renames", "dr", of, fl) |
|
160 | 162 | |
|
161 | 163 | repo.ui.note(_("resolving manifests\n")) |
|
162 | 164 | repo.ui.debug(_(" overwrite %s partial %s\n") % (overwrite, bool(partial))) |
|
163 | 165 | repo.ui.debug(_(" ancestor %s local %s remote %s\n") % (pa, p1, p2)) |
|
164 | 166 | |
|
165 | 167 | m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest() |
|
166 | 168 | copied = set(copy.values()) |
|
167 | 169 | |
|
168 | 170 | # Compare manifests |
|
169 | 171 | for f, n in m1.iteritems(): |
|
170 | 172 | if partial and not partial(f): |
|
171 | 173 | continue |
|
172 | 174 | if f in m2: |
|
173 | 175 | rflags = fmerge(f, f, f) |
|
174 | 176 | a = ma.get(f, nullid) |
|
175 | 177 | if n == m2[f] or m2[f] == a: # same or local newer |
|
176 | 178 | if m1.flags(f) != rflags: |
|
177 | 179 | act("update permissions", "e", f, rflags) |
|
178 | 180 | elif n == a: # remote newer |
|
179 | 181 | act("remote is newer", "g", f, rflags) |
|
180 | 182 | else: # both changed |
|
181 | 183 | act("versions differ", "m", f, f, f, rflags, False) |
|
182 | 184 | elif f in copied: # files we'll deal with on m2 side |
|
183 | 185 | pass |
|
184 | 186 | elif f in copy: |
|
185 | 187 | f2 = copy[f] |
|
186 | 188 | if f2 not in m2: # directory rename |
|
187 | 189 | act("remote renamed directory to " + f2, "d", |
|
188 | 190 | f, None, f2, m1.flags(f)) |
|
189 | 191 | else: # case 2 A,B/B/B or case 4,21 A/B/B |
|
190 | 192 | act("local copied/moved to " + f2, "m", |
|
191 | 193 | f, f2, f, fmerge(f, f2, f2), False) |
|
192 | 194 | elif f in ma: # clean, a different, no remote |
|
193 | 195 | if n != ma[f]: |
|
194 | if repo.ui.prompt( | |
|
196 | if repo.ui.promptchoice( | |
|
195 | 197 | _(" local changed %s which remote deleted\n" |
|
196 | 198 | "use (c)hanged version or (d)elete?") % f, |
|
197 |
(_("&Changed"), _("&Delete")), |
|
|
199 | (_("&Changed"), _("&Delete")), 0): | |
|
198 | 200 | act("prompt delete", "r", f) |
|
199 | 201 | else: |
|
200 | 202 | act("prompt keep", "a", f) |
|
201 | 203 | elif n[20:] == "a": # added, no remote |
|
202 | 204 | act("remote deleted", "f", f) |
|
203 | 205 | elif n[20:] != "u": |
|
204 | 206 | act("other deleted", "r", f) |
|
205 | 207 | |
|
206 | 208 | for f, n in m2.iteritems(): |
|
207 | 209 | if partial and not partial(f): |
|
208 | 210 | continue |
|
209 | 211 | if f in m1 or f in copied: # files already visited |
|
210 | 212 | continue |
|
211 | 213 | if f in copy: |
|
212 | 214 | f2 = copy[f] |
|
213 | 215 | if f2 not in m1: # directory rename |
|
214 | 216 | act("local renamed directory to " + f2, "d", |
|
215 | 217 | None, f, f2, m2.flags(f)) |
|
216 | 218 | elif f2 in m2: # rename case 1, A/A,B/A |
|
217 | 219 | act("remote copied to " + f, "m", |
|
218 | 220 | f2, f, f, fmerge(f2, f, f2), False) |
|
219 | 221 | else: # case 3,20 A/B/A |
|
220 | 222 | act("remote moved to " + f, "m", |
|
221 | 223 | f2, f, f, fmerge(f2, f, f2), True) |
|
222 | 224 | elif f not in ma: |
|
223 | 225 | act("remote created", "g", f, m2.flags(f)) |
|
224 | 226 | elif n != ma[f]: |
|
225 | if repo.ui.prompt( | |
|
227 | if repo.ui.promptchoice( | |
|
226 | 228 | _("remote changed %s which local deleted\n" |
|
227 | 229 | "use (c)hanged version or leave (d)eleted?") % f, |
|
228 |
(_("&Changed"), _("&Deleted")), |
|
|
230 | (_("&Changed"), _("&Deleted")), 0) == 0: | |
|
229 | 231 | act("prompt recreating", "g", f, m2.flags(f)) |
|
230 | 232 | |
|
231 | 233 | return action |
|
232 | 234 | |
|
233 | 235 | def actionkey(a): |
|
234 | 236 | return a[1] == 'r' and -1 or 0, a |
|
235 | 237 | |
|
236 | 238 | def applyupdates(repo, action, wctx, mctx): |
|
237 | 239 | "apply the merge action list to the working directory" |
|
238 | 240 | |
|
239 | 241 | updated, merged, removed, unresolved = 0, 0, 0, 0 |
|
240 | 242 | ms = mergestate(repo) |
|
241 | 243 | ms.reset(wctx.parents()[0].node()) |
|
242 | 244 | moves = [] |
|
243 | 245 | action.sort(key=actionkey) |
|
244 | 246 | substate = wctx.substate # prime |
|
245 | 247 | |
|
246 | 248 | # prescan for merges |
|
247 | 249 | for a in action: |
|
248 | 250 | f, m = a[:2] |
|
249 | 251 | if m == 'm': # merge |
|
250 | 252 | f2, fd, flags, move = a[2:] |
|
251 | 253 | if f == '.hgsubstate': # merged internally |
|
252 | 254 | continue |
|
253 | 255 | repo.ui.debug(_("preserving %s for resolve of %s\n") % (f, fd)) |
|
254 | 256 | fcl = wctx[f] |
|
255 | 257 | fco = mctx[f2] |
|
256 | 258 | fca = fcl.ancestor(fco) or repo.filectx(f, fileid=nullrev) |
|
257 | 259 | ms.add(fcl, fco, fca, fd, flags) |
|
258 | 260 | if f != fd and move: |
|
259 | 261 | moves.append(f) |
|
260 | 262 | |
|
261 | 263 | # remove renamed files after safely stored |
|
262 | 264 | for f in moves: |
|
263 | 265 | if util.lexists(repo.wjoin(f)): |
|
264 | 266 | repo.ui.debug(_("removing %s\n") % f) |
|
265 | 267 | os.unlink(repo.wjoin(f)) |
|
266 | 268 | |
|
267 | 269 | audit_path = util.path_auditor(repo.root) |
|
268 | 270 | |
|
269 | 271 | for a in action: |
|
270 | 272 | f, m = a[:2] |
|
271 | 273 | if f and f[0] == "/": |
|
272 | 274 | continue |
|
273 | 275 | if m == "r": # remove |
|
274 | 276 | repo.ui.note(_("removing %s\n") % f) |
|
275 | 277 | audit_path(f) |
|
276 | 278 | if f == '.hgsubstate': # subrepo states need updating |
|
277 | 279 | subrepo.submerge(repo, wctx, mctx, wctx) |
|
278 | 280 | try: |
|
279 | 281 | util.unlink(repo.wjoin(f)) |
|
280 | 282 | except OSError, inst: |
|
281 | 283 | if inst.errno != errno.ENOENT: |
|
282 | 284 | repo.ui.warn(_("update failed to remove %s: %s!\n") % |
|
283 | 285 | (f, inst.strerror)) |
|
284 | 286 | removed += 1 |
|
285 | 287 | elif m == "m": # merge |
|
286 | 288 | if f == '.hgsubstate': # subrepo states need updating |
|
287 | 289 | subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx)) |
|
288 | 290 | continue |
|
289 | 291 | f2, fd, flags, move = a[2:] |
|
290 | 292 | r = ms.resolve(fd, wctx, mctx) |
|
291 | 293 | if r is not None and r > 0: |
|
292 | 294 | unresolved += 1 |
|
293 | 295 | else: |
|
294 | 296 | if r is None: |
|
295 | 297 | updated += 1 |
|
296 | 298 | else: |
|
297 | 299 | merged += 1 |
|
298 | 300 | util.set_flags(repo.wjoin(fd), 'l' in flags, 'x' in flags) |
|
299 | 301 | if f != fd and move and util.lexists(repo.wjoin(f)): |
|
300 | 302 | repo.ui.debug(_("removing %s\n") % f) |
|
301 | 303 | os.unlink(repo.wjoin(f)) |
|
302 | 304 | elif m == "g": # get |
|
303 | 305 | flags = a[2] |
|
304 | 306 | repo.ui.note(_("getting %s\n") % f) |
|
305 | 307 | t = mctx.filectx(f).data() |
|
306 | 308 | repo.wwrite(f, t, flags) |
|
307 | 309 | updated += 1 |
|
308 | 310 | if f == '.hgsubstate': # subrepo states need updating |
|
309 | 311 | subrepo.submerge(repo, wctx, mctx, wctx) |
|
310 | 312 | elif m == "d": # directory rename |
|
311 | 313 | f2, fd, flags = a[2:] |
|
312 | 314 | if f: |
|
313 | 315 | repo.ui.note(_("moving %s to %s\n") % (f, fd)) |
|
314 | 316 | t = wctx.filectx(f).data() |
|
315 | 317 | repo.wwrite(fd, t, flags) |
|
316 | 318 | util.unlink(repo.wjoin(f)) |
|
317 | 319 | if f2: |
|
318 | 320 | repo.ui.note(_("getting %s to %s\n") % (f2, fd)) |
|
319 | 321 | t = mctx.filectx(f2).data() |
|
320 | 322 | repo.wwrite(fd, t, flags) |
|
321 | 323 | updated += 1 |
|
322 | 324 | elif m == "dr": # divergent renames |
|
323 | 325 | fl = a[2] |
|
324 | 326 | repo.ui.warn(_("warning: detected divergent renames of %s to:\n") % f) |
|
325 | 327 | for nf in fl: |
|
326 | 328 | repo.ui.warn(" %s\n" % nf) |
|
327 | 329 | elif m == "e": # exec |
|
328 | 330 | flags = a[2] |
|
329 | 331 | util.set_flags(repo.wjoin(f), 'l' in flags, 'x' in flags) |
|
330 | 332 | |
|
331 | 333 | return updated, merged, removed, unresolved |
|
332 | 334 | |
|
333 | 335 | def recordupdates(repo, action, branchmerge): |
|
334 | 336 | "record merge actions to the dirstate" |
|
335 | 337 | |
|
336 | 338 | for a in action: |
|
337 | 339 | f, m = a[:2] |
|
338 | 340 | if m == "r": # remove |
|
339 | 341 | if branchmerge: |
|
340 | 342 | repo.dirstate.remove(f) |
|
341 | 343 | else: |
|
342 | 344 | repo.dirstate.forget(f) |
|
343 | 345 | elif m == "a": # re-add |
|
344 | 346 | if not branchmerge: |
|
345 | 347 | repo.dirstate.add(f) |
|
346 | 348 | elif m == "f": # forget |
|
347 | 349 | repo.dirstate.forget(f) |
|
348 | 350 | elif m == "e": # exec change |
|
349 | 351 | repo.dirstate.normallookup(f) |
|
350 | 352 | elif m == "g": # get |
|
351 | 353 | if branchmerge: |
|
352 | 354 | repo.dirstate.normaldirty(f) |
|
353 | 355 | else: |
|
354 | 356 | repo.dirstate.normal(f) |
|
355 | 357 | elif m == "m": # merge |
|
356 | 358 | f2, fd, flag, move = a[2:] |
|
357 | 359 | if branchmerge: |
|
358 | 360 | # We've done a branch merge, mark this file as merged |
|
359 | 361 | # so that we properly record the merger later |
|
360 | 362 | repo.dirstate.merge(fd) |
|
361 | 363 | if f != f2: # copy/rename |
|
362 | 364 | if move: |
|
363 | 365 | repo.dirstate.remove(f) |
|
364 | 366 | if f != fd: |
|
365 | 367 | repo.dirstate.copy(f, fd) |
|
366 | 368 | else: |
|
367 | 369 | repo.dirstate.copy(f2, fd) |
|
368 | 370 | else: |
|
369 | 371 | # We've update-merged a locally modified file, so |
|
370 | 372 | # we set the dirstate to emulate a normal checkout |
|
371 | 373 | # of that file some time in the past. Thus our |
|
372 | 374 | # merge will appear as a normal local file |
|
373 | 375 | # modification. |
|
374 | 376 | repo.dirstate.normallookup(fd) |
|
375 | 377 | if move: |
|
376 | 378 | repo.dirstate.forget(f) |
|
377 | 379 | elif m == "d": # directory rename |
|
378 | 380 | f2, fd, flag = a[2:] |
|
379 | 381 | if not f2 and f not in repo.dirstate: |
|
380 | 382 | # untracked file moved |
|
381 | 383 | continue |
|
382 | 384 | if branchmerge: |
|
383 | 385 | repo.dirstate.add(fd) |
|
384 | 386 | if f: |
|
385 | 387 | repo.dirstate.remove(f) |
|
386 | 388 | repo.dirstate.copy(f, fd) |
|
387 | 389 | if f2: |
|
388 | 390 | repo.dirstate.copy(f2, fd) |
|
389 | 391 | else: |
|
390 | 392 | repo.dirstate.normal(fd) |
|
391 | 393 | if f: |
|
392 | 394 | repo.dirstate.forget(f) |
|
393 | 395 | |
|
394 | 396 | def update(repo, node, branchmerge, force, partial): |
|
395 | 397 | """ |
|
396 | 398 | Perform a merge between the working directory and the given node |
|
397 | 399 | |
|
398 | 400 | branchmerge = whether to merge between branches |
|
399 | 401 | force = whether to force branch merging or file overwriting |
|
400 | 402 | partial = a function to filter file lists (dirstate not updated) |
|
401 | 403 | """ |
|
402 | 404 | |
|
403 | 405 | wlock = repo.wlock() |
|
404 | 406 | try: |
|
405 | 407 | wc = repo[None] |
|
406 | 408 | if node is None: |
|
407 | 409 | # tip of current branch |
|
408 | 410 | try: |
|
409 | 411 | node = repo.branchtags()[wc.branch()] |
|
410 | 412 | except KeyError: |
|
411 | 413 | if wc.branch() == "default": # no default branch! |
|
412 | 414 | node = repo.lookup("tip") # update to tip |
|
413 | 415 | else: |
|
414 | 416 | raise util.Abort(_("branch %s not found") % wc.branch()) |
|
415 | 417 | overwrite = force and not branchmerge |
|
416 | 418 | pl = wc.parents() |
|
417 | 419 | p1, p2 = pl[0], repo[node] |
|
418 | 420 | pa = p1.ancestor(p2) |
|
419 | 421 | fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2) |
|
420 | 422 | fastforward = False |
|
421 | 423 | |
|
422 | 424 | ### check phase |
|
423 | 425 | if not overwrite and len(pl) > 1: |
|
424 | 426 | raise util.Abort(_("outstanding uncommitted merges")) |
|
425 | 427 | if branchmerge: |
|
426 | 428 | if pa == p2: |
|
427 | 429 | raise util.Abort(_("can't merge with ancestor")) |
|
428 | 430 | elif pa == p1: |
|
429 | 431 | if p1.branch() != p2.branch(): |
|
430 | 432 | fastforward = True |
|
431 | 433 | else: |
|
432 | 434 | raise util.Abort(_("nothing to merge (use 'hg update'" |
|
433 | 435 | " or check 'hg heads')")) |
|
434 | 436 | if not force and (wc.files() or wc.deleted()): |
|
435 | 437 | raise util.Abort(_("outstanding uncommitted changes " |
|
436 | 438 | "(use 'hg status' to list changes)")) |
|
437 | 439 | elif not overwrite: |
|
438 | 440 | if pa == p1 or pa == p2: # linear |
|
439 | 441 | pass # all good |
|
440 | 442 | elif p1.branch() == p2.branch(): |
|
441 | 443 | if wc.files() or wc.deleted(): |
|
442 | 444 | raise util.Abort(_("crosses branches (use 'hg merge' or " |
|
443 | 445 | "'hg update -C' to discard changes)")) |
|
444 | 446 | raise util.Abort(_("crosses branches (use 'hg merge' " |
|
445 | 447 | "or 'hg update -C')")) |
|
446 | 448 | elif wc.files() or wc.deleted(): |
|
447 | 449 | raise util.Abort(_("crosses named branches (use " |
|
448 | 450 | "'hg update -C' to discard changes)")) |
|
449 | 451 | else: |
|
450 | 452 | # Allow jumping branches if there are no changes |
|
451 | 453 | overwrite = True |
|
452 | 454 | |
|
453 | 455 | ### calculate phase |
|
454 | 456 | action = [] |
|
455 | 457 | if not force: |
|
456 | 458 | _checkunknown(wc, p2) |
|
457 | 459 | if not util.checkcase(repo.path): |
|
458 | 460 | _checkcollision(p2) |
|
459 | 461 | action += _forgetremoved(wc, p2, branchmerge) |
|
460 | 462 | action += manifestmerge(repo, wc, p2, pa, overwrite, partial) |
|
461 | 463 | |
|
462 | 464 | ### apply phase |
|
463 | 465 | if not branchmerge: # just jump to the new rev |
|
464 | 466 | fp1, fp2, xp1, xp2 = fp2, nullid, xp2, '' |
|
465 | 467 | if not partial: |
|
466 | 468 | repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2) |
|
467 | 469 | |
|
468 | 470 | stats = applyupdates(repo, action, wc, p2) |
|
469 | 471 | |
|
470 | 472 | if not partial: |
|
471 | 473 | recordupdates(repo, action, branchmerge) |
|
472 | 474 | repo.dirstate.setparents(fp1, fp2) |
|
473 | 475 | if not branchmerge and not fastforward: |
|
474 | 476 | repo.dirstate.setbranch(p2.branch()) |
|
475 | 477 | repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3]) |
|
476 | 478 | |
|
477 | 479 | return stats |
|
478 | 480 | finally: |
|
479 | 481 | wlock.release() |
@@ -1,193 +1,193 b'' | |||
|
1 | 1 | # subrepo.py - sub-repository handling for Mercurial |
|
2 | 2 | # |
|
3 | 3 | # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com> |
|
4 | 4 | # |
|
5 | 5 | # This software may be used and distributed according to the terms of the |
|
6 | 6 | # GNU General Public License version 2, incorporated herein by reference. |
|
7 | 7 | |
|
8 | 8 | import errno, os |
|
9 | 9 | from i18n import _ |
|
10 | 10 | import config, util, node, error |
|
11 | 11 | localrepo = hg = None |
|
12 | 12 | |
|
13 | 13 | nullstate = ('', '') |
|
14 | 14 | |
|
15 | 15 | def state(ctx): |
|
16 | 16 | p = config.config() |
|
17 | 17 | def read(f, sections=None, remap=None): |
|
18 | 18 | if f in ctx: |
|
19 | 19 | try: |
|
20 | 20 | p.parse(f, ctx[f].data(), sections, remap) |
|
21 | 21 | except IOError, err: |
|
22 | 22 | if err.errno != errno.ENOENT: |
|
23 | 23 | raise |
|
24 | 24 | read('.hgsub') |
|
25 | 25 | |
|
26 | 26 | rev = {} |
|
27 | 27 | if '.hgsubstate' in ctx: |
|
28 | 28 | try: |
|
29 | 29 | for l in ctx['.hgsubstate'].data().splitlines(): |
|
30 | 30 | revision, path = l.split() |
|
31 | 31 | rev[path] = revision |
|
32 | 32 | except IOError, err: |
|
33 | 33 | if err.errno != errno.ENOENT: |
|
34 | 34 | raise |
|
35 | 35 | |
|
36 | 36 | state = {} |
|
37 | 37 | for path, src in p[''].items(): |
|
38 | 38 | state[path] = (src, rev.get(path, '')) |
|
39 | 39 | |
|
40 | 40 | return state |
|
41 | 41 | |
|
42 | 42 | def writestate(repo, state): |
|
43 | 43 | repo.wwrite('.hgsubstate', |
|
44 | 44 | ''.join(['%s %s\n' % (state[s][1], s) |
|
45 | 45 | for s in sorted(state)]), '') |
|
46 | 46 | |
|
47 | 47 | def submerge(repo, wctx, mctx, actx): |
|
48 | 48 | if mctx == actx: # backwards? |
|
49 | 49 | actx = wctx.p1() |
|
50 | 50 | s1 = wctx.substate |
|
51 | 51 | s2 = mctx.substate |
|
52 | 52 | sa = actx.substate |
|
53 | 53 | sm = {} |
|
54 | 54 | |
|
55 | 55 | for s, l in s1.items(): |
|
56 | 56 | a = sa.get(s, nullstate) |
|
57 | 57 | if s in s2: |
|
58 | 58 | r = s2[s] |
|
59 | 59 | if l == r or r == a: # no change or local is newer |
|
60 | 60 | sm[s] = l |
|
61 | 61 | continue |
|
62 | 62 | elif l == a: # other side changed |
|
63 | 63 | wctx.sub(s).get(r) |
|
64 | 64 | sm[s] = r |
|
65 | 65 | elif l[0] != r[0]: # sources differ |
|
66 | if repo.ui.prompt( | |
|
66 | if repo.ui.promptchoice( | |
|
67 | 67 | _(' subrepository sources for %s differ\n' |
|
68 | 68 | 'use (l)ocal source (%s) or (r)emote source (%s)?') |
|
69 | 69 | % (s, l[0], r[0]), |
|
70 |
(_('&Local'), _('&Remote')), |
|
|
70 | (_('&Local'), _('&Remote')), 0): | |
|
71 | 71 | wctx.sub(s).get(r) |
|
72 | 72 | sm[s] = r |
|
73 | 73 | elif l[1] == a[1]: # local side is unchanged |
|
74 | 74 | wctx.sub(s).get(r) |
|
75 | 75 | sm[s] = r |
|
76 | 76 | else: |
|
77 | 77 | wctx.sub(s).merge(r) |
|
78 | 78 | sm[s] = l |
|
79 | 79 | elif l == a: # remote removed, local unchanged |
|
80 | 80 | wctx.sub(s).remove() |
|
81 | 81 | else: |
|
82 | if repo.ui.prompt( | |
|
82 | if repo.ui.promptchoice( | |
|
83 | 83 | _(' local changed subrepository %s which remote removed\n' |
|
84 | 84 | 'use (c)hanged version or (d)elete?') % s, |
|
85 |
(_('&Changed'), _('&Delete')), |
|
|
85 | (_('&Changed'), _('&Delete')), 0): | |
|
86 | 86 | wctx.sub(s).remove() |
|
87 | 87 | |
|
88 | 88 | for s, r in s2.items(): |
|
89 | 89 | if s in s1: |
|
90 | 90 | continue |
|
91 | 91 | elif s not in sa: |
|
92 | 92 | wctx.sub(s).get(r) |
|
93 | 93 | sm[s] = r |
|
94 | 94 | elif r != sa[s]: |
|
95 | if repo.ui.prompt( | |
|
95 | if repo.ui.promptchoice( | |
|
96 | 96 | _(' remote changed subrepository %s which local removed\n' |
|
97 | 97 | 'use (c)hanged version or (d)elete?') % s, |
|
98 |
(_('&Changed'), _('&Delete')), |
|
|
98 | (_('&Changed'), _('&Delete')), 0) == 0: | |
|
99 | 99 | wctx.sub(s).get(r) |
|
100 | 100 | sm[s] = r |
|
101 | 101 | |
|
102 | 102 | # record merged .hgsubstate |
|
103 | 103 | writestate(repo, sm) |
|
104 | 104 | |
|
105 | 105 | def _abssource(repo, push=False): |
|
106 | 106 | if hasattr(repo, '_subparent'): |
|
107 | 107 | source = repo._subsource |
|
108 | 108 | if source.startswith('/') or '://' in source: |
|
109 | 109 | return source |
|
110 | 110 | return os.path.join(_abssource(repo._subparent), repo._subsource) |
|
111 | 111 | if push and repo.ui.config('paths', 'default-push'): |
|
112 | 112 | return repo.ui.config('paths', 'default-push', repo.root) |
|
113 | 113 | return repo.ui.config('paths', 'default', repo.root) |
|
114 | 114 | |
|
115 | 115 | def subrepo(ctx, path): |
|
116 | 116 | # subrepo inherently violates our import layering rules |
|
117 | 117 | # because it wants to make repo objects from deep inside the stack |
|
118 | 118 | # so we manually delay the circular imports to not break |
|
119 | 119 | # scripts that don't use our demand-loading |
|
120 | 120 | global localrepo, hg |
|
121 | 121 | import localrepo as l, hg as h |
|
122 | 122 | localrepo = l |
|
123 | 123 | hg = h |
|
124 | 124 | |
|
125 | 125 | util.path_auditor(ctx._repo.root)(path) |
|
126 | 126 | state = ctx.substate.get(path, nullstate) |
|
127 | 127 | if state[0].startswith('['): # future expansion |
|
128 | 128 | raise error.Abort('unknown subrepo source %s' % state[0]) |
|
129 | 129 | return hgsubrepo(ctx, path, state) |
|
130 | 130 | |
|
131 | 131 | class hgsubrepo(object): |
|
132 | 132 | def __init__(self, ctx, path, state): |
|
133 | 133 | self._path = path |
|
134 | 134 | self._state = state |
|
135 | 135 | r = ctx._repo |
|
136 | 136 | root = r.wjoin(path) |
|
137 | 137 | if os.path.exists(os.path.join(root, '.hg')): |
|
138 | 138 | self._repo = localrepo.localrepository(r.ui, root) |
|
139 | 139 | else: |
|
140 | 140 | util.makedirs(root) |
|
141 | 141 | self._repo = localrepo.localrepository(r.ui, root, create=True) |
|
142 | 142 | self._repo._subparent = r |
|
143 | 143 | self._repo._subsource = state[0] |
|
144 | 144 | |
|
145 | 145 | def dirty(self): |
|
146 | 146 | r = self._state[1] |
|
147 | 147 | if r == '': |
|
148 | 148 | return True |
|
149 | 149 | w = self._repo[None] |
|
150 | 150 | if w.p1() != self._repo[r]: # version checked out changed |
|
151 | 151 | return True |
|
152 | 152 | return w.dirty() # working directory changed |
|
153 | 153 | |
|
154 | 154 | def commit(self, text, user, date): |
|
155 | 155 | n = self._repo.commit(text, user, date) |
|
156 | 156 | if not n: |
|
157 | 157 | return self._repo['.'].hex() # different version checked out |
|
158 | 158 | return node.hex(n) |
|
159 | 159 | |
|
160 | 160 | def remove(self): |
|
161 | 161 | # we can't fully delete the repository as it may contain |
|
162 | 162 | # local-only history |
|
163 | 163 | self._repo.ui.note(_('removing subrepo %s\n') % self._path) |
|
164 | 164 | hg.clean(self._repo, node.nullid, False) |
|
165 | 165 | |
|
166 | 166 | def get(self, state): |
|
167 | 167 | source, revision = state |
|
168 | 168 | try: |
|
169 | 169 | self._repo.lookup(revision) |
|
170 | 170 | except error.RepoError: |
|
171 | 171 | self._repo._subsource = source |
|
172 | 172 | self._repo.ui.status(_('pulling subrepo %s\n') % self._path) |
|
173 | 173 | srcurl = _abssource(self._repo) |
|
174 | 174 | other = hg.repository(self._repo.ui, srcurl) |
|
175 | 175 | self._repo.pull(other) |
|
176 | 176 | |
|
177 | 177 | hg.clean(self._repo, revision, False) |
|
178 | 178 | |
|
179 | 179 | def merge(self, state): |
|
180 | 180 | hg.merge(self._repo, state[1], remind=False) |
|
181 | 181 | |
|
182 | 182 | def push(self, force): |
|
183 | 183 | # push subrepos depth-first for coherent ordering |
|
184 | 184 | c = self._repo[''] |
|
185 | 185 | subs = c.substate # only repos that are committed |
|
186 | 186 | for s in sorted(subs): |
|
187 | 187 | c.sub(s).push(force) |
|
188 | 188 | |
|
189 | 189 | self._repo.ui.status(_('pushing subrepo %s\n') % self._path) |
|
190 | 190 | dsturl = _abssource(self._repo, True) |
|
191 | 191 | other = hg.repository(self._repo.ui, dsturl) |
|
192 | 192 | self._repo.push(other, force) |
|
193 | 193 |
@@ -1,346 +1,351 b'' | |||
|
1 | 1 | # ui.py - user interface bits for mercurial |
|
2 | 2 | # |
|
3 | 3 | # Copyright 2005-2007 Matt Mackall <mpm@selenic.com> |
|
4 | 4 | # |
|
5 | 5 | # This software may be used and distributed according to the terms of the |
|
6 | 6 | # GNU General Public License version 2, incorporated herein by reference. |
|
7 | 7 | |
|
8 | 8 | from i18n import _ |
|
9 | 9 | import errno, getpass, os, socket, sys, tempfile, traceback |
|
10 | 10 | import config, util, error |
|
11 | 11 | |
|
12 | 12 | _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, |
|
13 | 13 | '0': False, 'no': False, 'false': False, 'off': False} |
|
14 | 14 | |
|
15 | 15 | class ui(object): |
|
16 | 16 | def __init__(self, src=None): |
|
17 | 17 | self._buffers = [] |
|
18 | 18 | self.quiet = self.verbose = self.debugflag = self._traceback = False |
|
19 | 19 | self._reportuntrusted = True |
|
20 | 20 | self._ocfg = config.config() # overlay |
|
21 | 21 | self._tcfg = config.config() # trusted |
|
22 | 22 | self._ucfg = config.config() # untrusted |
|
23 | 23 | self._trustusers = set() |
|
24 | 24 | self._trustgroups = set() |
|
25 | 25 | |
|
26 | 26 | if src: |
|
27 | 27 | self._tcfg = src._tcfg.copy() |
|
28 | 28 | self._ucfg = src._ucfg.copy() |
|
29 | 29 | self._ocfg = src._ocfg.copy() |
|
30 | 30 | self._trustusers = src._trustusers.copy() |
|
31 | 31 | self._trustgroups = src._trustgroups.copy() |
|
32 | 32 | self.fixconfig() |
|
33 | 33 | else: |
|
34 | 34 | # we always trust global config files |
|
35 | 35 | for f in util.rcpath(): |
|
36 | 36 | self.readconfig(f, trust=True) |
|
37 | 37 | |
|
38 | 38 | def copy(self): |
|
39 | 39 | return self.__class__(self) |
|
40 | 40 | |
|
41 | 41 | def _is_trusted(self, fp, f): |
|
42 | 42 | st = util.fstat(fp) |
|
43 | 43 | if util.isowner(st): |
|
44 | 44 | return True |
|
45 | 45 | |
|
46 | 46 | tusers, tgroups = self._trustusers, self._trustgroups |
|
47 | 47 | if '*' in tusers or '*' in tgroups: |
|
48 | 48 | return True |
|
49 | 49 | |
|
50 | 50 | user = util.username(st.st_uid) |
|
51 | 51 | group = util.groupname(st.st_gid) |
|
52 | 52 | if user in tusers or group in tgroups or user == util.username(): |
|
53 | 53 | return True |
|
54 | 54 | |
|
55 | 55 | if self._reportuntrusted: |
|
56 | 56 | self.warn(_('Not trusting file %s from untrusted ' |
|
57 | 57 | 'user %s, group %s\n') % (f, user, group)) |
|
58 | 58 | return False |
|
59 | 59 | |
|
60 | 60 | def readconfig(self, filename, root=None, trust=False, |
|
61 | 61 | sections=None, remap=None): |
|
62 | 62 | try: |
|
63 | 63 | fp = open(filename) |
|
64 | 64 | except IOError: |
|
65 | 65 | if not sections: # ignore unless we were looking for something |
|
66 | 66 | return |
|
67 | 67 | raise |
|
68 | 68 | |
|
69 | 69 | cfg = config.config() |
|
70 | 70 | trusted = sections or trust or self._is_trusted(fp, filename) |
|
71 | 71 | |
|
72 | 72 | try: |
|
73 | 73 | cfg.read(filename, fp, sections=sections, remap=remap) |
|
74 | 74 | except error.ConfigError, inst: |
|
75 | 75 | if trusted: |
|
76 | 76 | raise |
|
77 | 77 | self.warn(_("Ignored: %s\n") % str(inst)) |
|
78 | 78 | |
|
79 | 79 | if trusted: |
|
80 | 80 | self._tcfg.update(cfg) |
|
81 | 81 | self._tcfg.update(self._ocfg) |
|
82 | 82 | self._ucfg.update(cfg) |
|
83 | 83 | self._ucfg.update(self._ocfg) |
|
84 | 84 | |
|
85 | 85 | if root is None: |
|
86 | 86 | root = os.path.expanduser('~') |
|
87 | 87 | self.fixconfig(root=root) |
|
88 | 88 | |
|
89 | 89 | def fixconfig(self, root=None): |
|
90 | 90 | # translate paths relative to root (or home) into absolute paths |
|
91 | 91 | root = root or os.getcwd() |
|
92 | 92 | for c in self._tcfg, self._ucfg, self._ocfg: |
|
93 | 93 | for n, p in c.items('paths'): |
|
94 | 94 | if p and "://" not in p and not os.path.isabs(p): |
|
95 | 95 | c.set("paths", n, os.path.normpath(os.path.join(root, p))) |
|
96 | 96 | |
|
97 | 97 | # update ui options |
|
98 | 98 | self.debugflag = self.configbool('ui', 'debug') |
|
99 | 99 | self.verbose = self.debugflag or self.configbool('ui', 'verbose') |
|
100 | 100 | self.quiet = not self.debugflag and self.configbool('ui', 'quiet') |
|
101 | 101 | if self.verbose and self.quiet: |
|
102 | 102 | self.quiet = self.verbose = False |
|
103 | 103 | self._reportuntrusted = self.configbool("ui", "report_untrusted", True) |
|
104 | 104 | self._traceback = self.configbool('ui', 'traceback', False) |
|
105 | 105 | |
|
106 | 106 | # update trust information |
|
107 | 107 | self._trustusers.update(self.configlist('trusted', 'users')) |
|
108 | 108 | self._trustgroups.update(self.configlist('trusted', 'groups')) |
|
109 | 109 | |
|
110 | 110 | def setconfig(self, section, name, value): |
|
111 | 111 | for cfg in (self._ocfg, self._tcfg, self._ucfg): |
|
112 | 112 | cfg.set(section, name, value) |
|
113 | 113 | self.fixconfig() |
|
114 | 114 | |
|
115 | 115 | def _data(self, untrusted): |
|
116 | 116 | return untrusted and self._ucfg or self._tcfg |
|
117 | 117 | |
|
118 | 118 | def configsource(self, section, name, untrusted=False): |
|
119 | 119 | return self._data(untrusted).source(section, name) or 'none' |
|
120 | 120 | |
|
121 | 121 | def config(self, section, name, default=None, untrusted=False): |
|
122 | 122 | value = self._data(untrusted).get(section, name, default) |
|
123 | 123 | if self.debugflag and not untrusted and self._reportuntrusted: |
|
124 | 124 | uvalue = self._ucfg.get(section, name) |
|
125 | 125 | if uvalue is not None and uvalue != value: |
|
126 | 126 | self.debug(_("ignoring untrusted configuration option " |
|
127 | 127 | "%s.%s = %s\n") % (section, name, uvalue)) |
|
128 | 128 | return value |
|
129 | 129 | |
|
130 | 130 | def configbool(self, section, name, default=False, untrusted=False): |
|
131 | 131 | v = self.config(section, name, None, untrusted) |
|
132 | 132 | if v is None: |
|
133 | 133 | return default |
|
134 | 134 | if v.lower() not in _booleans: |
|
135 | 135 | raise error.ConfigError(_("%s.%s not a boolean ('%s')") |
|
136 | 136 | % (section, name, v)) |
|
137 | 137 | return _booleans[v.lower()] |
|
138 | 138 | |
|
139 | 139 | def configlist(self, section, name, default=None, untrusted=False): |
|
140 | 140 | """Return a list of comma/space separated strings""" |
|
141 | 141 | result = self.config(section, name, untrusted=untrusted) |
|
142 | 142 | if result is None: |
|
143 | 143 | result = default or [] |
|
144 | 144 | if isinstance(result, basestring): |
|
145 | 145 | result = result.replace(",", " ").split() |
|
146 | 146 | return result |
|
147 | 147 | |
|
148 | 148 | def has_section(self, section, untrusted=False): |
|
149 | 149 | '''tell whether section exists in config.''' |
|
150 | 150 | return section in self._data(untrusted) |
|
151 | 151 | |
|
152 | 152 | def configitems(self, section, untrusted=False): |
|
153 | 153 | items = self._data(untrusted).items(section) |
|
154 | 154 | if self.debugflag and not untrusted and self._reportuntrusted: |
|
155 | 155 | for k, v in self._ucfg.items(section): |
|
156 | 156 | if self._tcfg.get(section, k) != v: |
|
157 | 157 | self.debug(_("ignoring untrusted configuration option " |
|
158 | 158 | "%s.%s = %s\n") % (section, k, v)) |
|
159 | 159 | return items |
|
160 | 160 | |
|
161 | 161 | def walkconfig(self, untrusted=False): |
|
162 | 162 | cfg = self._data(untrusted) |
|
163 | 163 | for section in cfg.sections(): |
|
164 | 164 | for name, value in self.configitems(section, untrusted): |
|
165 | 165 | yield section, name, str(value).replace('\n', '\\n') |
|
166 | 166 | |
|
167 | 167 | def username(self): |
|
168 | 168 | """Return default username to be used in commits. |
|
169 | 169 | |
|
170 | 170 | Searched in this order: $HGUSER, [ui] section of hgrcs, $EMAIL |
|
171 | 171 | and stop searching if one of these is set. |
|
172 | 172 | If not found and ui.askusername is True, ask the user, else use |
|
173 | 173 | ($LOGNAME or $USER or $LNAME or $USERNAME) + "@full.hostname". |
|
174 | 174 | """ |
|
175 | 175 | user = os.environ.get("HGUSER") |
|
176 | 176 | if user is None: |
|
177 | 177 | user = self.config("ui", "username") |
|
178 | 178 | if user is None: |
|
179 | 179 | user = os.environ.get("EMAIL") |
|
180 | 180 | if user is None and self.configbool("ui", "askusername"): |
|
181 | 181 | user = self.prompt(_("enter a commit username:"), default=None) |
|
182 | 182 | if user is None: |
|
183 | 183 | try: |
|
184 | 184 | user = '%s@%s' % (util.getuser(), socket.getfqdn()) |
|
185 | 185 | self.warn(_("No username found, using '%s' instead\n") % user) |
|
186 | 186 | except KeyError: |
|
187 | 187 | pass |
|
188 | 188 | if not user: |
|
189 | 189 | raise util.Abort(_("Please specify a username.")) |
|
190 | 190 | if "\n" in user: |
|
191 | 191 | raise util.Abort(_("username %s contains a newline\n") % repr(user)) |
|
192 | 192 | return user |
|
193 | 193 | |
|
194 | 194 | def shortuser(self, user): |
|
195 | 195 | """Return a short representation of a user name or email address.""" |
|
196 | 196 | if not self.verbose: user = util.shortuser(user) |
|
197 | 197 | return user |
|
198 | 198 | |
|
199 | 199 | def _path(self, loc): |
|
200 | 200 | p = self.config('paths', loc) |
|
201 | 201 | if p and '%%' in p: |
|
202 | 202 | self.warn('(deprecated \'%%\' in path %s=%s from %s)\n' % |
|
203 | 203 | (loc, p, self.configsource('paths', loc))) |
|
204 | 204 | p = p.replace('%%', '%') |
|
205 | 205 | return p |
|
206 | 206 | |
|
207 | 207 | def expandpath(self, loc, default=None): |
|
208 | 208 | """Return repository location relative to cwd or from [paths]""" |
|
209 | 209 | if "://" in loc or os.path.isdir(os.path.join(loc, '.hg')): |
|
210 | 210 | return loc |
|
211 | 211 | |
|
212 | 212 | path = self._path(loc) |
|
213 | 213 | if not path and default is not None: |
|
214 | 214 | path = self._path(default) |
|
215 | 215 | return path or loc |
|
216 | 216 | |
|
217 | 217 | def pushbuffer(self): |
|
218 | 218 | self._buffers.append([]) |
|
219 | 219 | |
|
220 | 220 | def popbuffer(self): |
|
221 | 221 | return "".join(self._buffers.pop()) |
|
222 | 222 | |
|
223 | 223 | def write(self, *args): |
|
224 | 224 | if self._buffers: |
|
225 | 225 | self._buffers[-1].extend([str(a) for a in args]) |
|
226 | 226 | else: |
|
227 | 227 | for a in args: |
|
228 | 228 | sys.stdout.write(str(a)) |
|
229 | 229 | |
|
230 | 230 | def write_err(self, *args): |
|
231 | 231 | try: |
|
232 | 232 | if not sys.stdout.closed: sys.stdout.flush() |
|
233 | 233 | for a in args: |
|
234 | 234 | sys.stderr.write(str(a)) |
|
235 | 235 | # stderr may be buffered under win32 when redirected to files, |
|
236 | 236 | # including stdout. |
|
237 | 237 | if not sys.stderr.closed: sys.stderr.flush() |
|
238 | 238 | except IOError, inst: |
|
239 | 239 | if inst.errno != errno.EPIPE: |
|
240 | 240 | raise |
|
241 | 241 | |
|
242 | 242 | def flush(self): |
|
243 | 243 | try: sys.stdout.flush() |
|
244 | 244 | except: pass |
|
245 | 245 | try: sys.stderr.flush() |
|
246 | 246 | except: pass |
|
247 | 247 | |
|
248 | 248 | def interactive(self): |
|
249 | 249 | i = self.configbool("ui", "interactive", None) |
|
250 | 250 | if i is None: |
|
251 | 251 | return sys.stdin.isatty() |
|
252 | 252 | return i |
|
253 | 253 | |
|
254 | 254 | def _readline(self, prompt=''): |
|
255 | 255 | if sys.stdin.isatty(): |
|
256 | 256 | try: |
|
257 | 257 | # magically add command line editing support, where |
|
258 | 258 | # available |
|
259 | 259 | import readline |
|
260 | 260 | # force demandimport to really load the module |
|
261 | 261 | readline.read_history_file |
|
262 | 262 | # windows sometimes raises something other than ImportError |
|
263 | 263 | except Exception: |
|
264 | 264 | pass |
|
265 | 265 | line = raw_input(prompt) |
|
266 | 266 | # When stdin is in binary mode on Windows, it can cause |
|
267 | 267 | # raw_input() to emit an extra trailing carriage return |
|
268 | 268 | if os.linesep == '\r\n' and line and line[-1] == '\r': |
|
269 | 269 | line = line[:-1] |
|
270 | 270 | return line |
|
271 | 271 | |
|
272 |
def prompt(self, msg, |
|
|
273 |
"""Prompt user with msg, read response |
|
|
274 | one of the provided choices. choices is a sequence of acceptable | |
|
275 | responses with the format: ('&None', 'E&xec', 'Sym&link') | |
|
276 | No sequence implies no response checking. Responses are case | |
|
277 | insensitive. If ui is not interactive, the default is returned. | |
|
272 | def prompt(self, msg, default="y"): | |
|
273 | """Prompt user with msg, read response. | |
|
274 | If ui is not interactive, the default is returned. | |
|
278 | 275 | """ |
|
279 | 276 | if not self.interactive(): |
|
280 | 277 | self.write(msg, ' ', default, "\n") |
|
281 | 278 | return default |
|
282 | while True: | |
|
283 | 279 |
|
|
284 | 280 |
|
|
285 | 281 |
|
|
286 | 282 |
|
|
287 | if not choices: | |
|
288 | 283 |
|
|
289 | resps = [s[s.index('&')+1].lower() for s in choices] | |
|
290 | if r.lower() in resps: | |
|
291 | return r.lower() | |
|
292 | else: | |
|
293 | self.write(_("unrecognized response\n")) | |
|
294 | 284 |
|
|
295 | 285 |
|
|
296 | 286 | |
|
287 | def promptchoice(self, msg, choices, default=0): | |
|
288 | """Prompt user with msg, read response, and ensure it matches | |
|
289 | one of the provided choices. The index of the choice is returned. | |
|
290 | choices is a sequence of acceptable responses with the format: | |
|
291 | ('&None', 'E&xec', 'Sym&link') Responses are case insensitive. | |
|
292 | If ui is not interactive, the default is returned. | |
|
293 | """ | |
|
294 | resps = [s[s.index('&')+1].lower() for s in choices] | |
|
295 | while True: | |
|
296 | r = self.prompt(msg, resps[default]) | |
|
297 | if r.lower() in resps: | |
|
298 | return resps.index(r.lower()) | |
|
299 | self.write(_("unrecognized response\n")) | |
|
300 | ||
|
301 | ||
|
297 | 302 | def getpass(self, prompt=None, default=None): |
|
298 | 303 | if not self.interactive(): return default |
|
299 | 304 | try: |
|
300 | 305 | return getpass.getpass(prompt or _('password: ')) |
|
301 | 306 | except EOFError: |
|
302 | 307 | raise util.Abort(_('response expected')) |
|
303 | 308 | def status(self, *msg): |
|
304 | 309 | if not self.quiet: self.write(*msg) |
|
305 | 310 | def warn(self, *msg): |
|
306 | 311 | self.write_err(*msg) |
|
307 | 312 | def note(self, *msg): |
|
308 | 313 | if self.verbose: self.write(*msg) |
|
309 | 314 | def debug(self, *msg): |
|
310 | 315 | if self.debugflag: self.write(*msg) |
|
311 | 316 | def edit(self, text, user): |
|
312 | 317 | (fd, name) = tempfile.mkstemp(prefix="hg-editor-", suffix=".txt", |
|
313 | 318 | text=True) |
|
314 | 319 | try: |
|
315 | 320 | f = os.fdopen(fd, "w") |
|
316 | 321 | f.write(text) |
|
317 | 322 | f.close() |
|
318 | 323 | |
|
319 | 324 | editor = self.geteditor() |
|
320 | 325 | |
|
321 | 326 | util.system("%s \"%s\"" % (editor, name), |
|
322 | 327 | environ={'HGUSER': user}, |
|
323 | 328 | onerr=util.Abort, errprefix=_("edit failed")) |
|
324 | 329 | |
|
325 | 330 | f = open(name) |
|
326 | 331 | t = f.read() |
|
327 | 332 | f.close() |
|
328 | 333 | finally: |
|
329 | 334 | os.unlink(name) |
|
330 | 335 | |
|
331 | 336 | return t |
|
332 | 337 | |
|
333 | 338 | def traceback(self): |
|
334 | 339 | '''print exception traceback if traceback printing enabled. |
|
335 | 340 | only to call in exception handler. returns true if traceback |
|
336 | 341 | printed.''' |
|
337 | 342 | if self._traceback: |
|
338 | 343 | traceback.print_exc() |
|
339 | 344 | return self._traceback |
|
340 | 345 | |
|
341 | 346 | def geteditor(self): |
|
342 | 347 | '''return editor to use''' |
|
343 | 348 | return (os.environ.get("HGEDITOR") or |
|
344 | 349 | self.config("ui", "editor") or |
|
345 | 350 | os.environ.get("VISUAL") or |
|
346 | 351 | os.environ.get("EDITOR", "vi")) |
General Comments 0
You need to be logged in to leave comments.
Login now