##// END OF EJS Templates
record: raise an exception correctly if we can't create a backup directory
Bryan O'Sullivan -
r5129:eca5b31c default
parent child Browse files
Show More
@@ -1,382 +1,382 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
6 6 # the GNU General Public License, incorporated herein by reference.
7 7
8 8 '''interactive change selection during commit'''
9 9
10 10 from mercurial.i18n import _
11 11 from mercurial import cmdutil, commands, cmdutil, hg, mdiff, patch, revlog
12 12 from mercurial import util
13 13 import copy, cStringIO, errno, operator, os, re, shutil, tempfile
14 14
15 15 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
16 16
17 17 def scanpatch(fp):
18 18 lr = patch.linereader(fp)
19 19
20 20 def scanwhile(first, p):
21 21 lines = [first]
22 22 while True:
23 23 line = lr.readline()
24 24 if not line:
25 25 break
26 26 if p(line):
27 27 lines.append(line)
28 28 else:
29 29 lr.push(line)
30 30 break
31 31 return lines
32 32
33 33 while True:
34 34 line = lr.readline()
35 35 if not line:
36 36 break
37 37 if line.startswith('diff --git a/'):
38 38 def notheader(line):
39 39 s = line.split(None, 1)
40 40 return not s or s[0] not in ('---', 'diff')
41 41 header = scanwhile(line, notheader)
42 42 fromfile = lr.readline()
43 43 if fromfile.startswith('---'):
44 44 tofile = lr.readline()
45 45 header += [fromfile, tofile]
46 46 else:
47 47 lr.push(fromfile)
48 48 yield 'file', header
49 49 elif line[0] == ' ':
50 50 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
51 51 elif line[0] in '-+':
52 52 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
53 53 else:
54 54 m = lines_re.match(line)
55 55 if m:
56 56 yield 'range', m.groups()
57 57 else:
58 58 raise patch.PatchError('unknown patch content: %r' % line)
59 59
60 60 class header(object):
61 61 diff_re = re.compile('diff --git a/(.*) b/(.*)$')
62 62 allhunks_re = re.compile('(?:index|new file|deleted file) ')
63 63 pretty_re = re.compile('(?:new file|deleted file) ')
64 64 special_re = re.compile('(?:index|new|deleted|copy|rename) ')
65 65
66 66 def __init__(self, header):
67 67 self.header = header
68 68 self.hunks = []
69 69
70 70 def binary(self):
71 71 for h in self.header:
72 72 if h.startswith('index '):
73 73 return True
74 74
75 75 def pretty(self, fp):
76 76 for h in self.header:
77 77 if h.startswith('index '):
78 78 fp.write(_('this modifies a binary file (all or nothing)\n'))
79 79 break
80 80 if self.pretty_re.match(h):
81 81 fp.write(h)
82 82 if self.binary():
83 83 fp.write(_('this is a binary file\n'))
84 84 break
85 85 if h.startswith('---'):
86 86 fp.write(_('%d hunks, %d lines changed\n') %
87 87 (len(self.hunks),
88 88 sum([h.added + h.removed for h in self.hunks])))
89 89 break
90 90 fp.write(h)
91 91
92 92 def write(self, fp):
93 93 fp.write(''.join(self.header))
94 94
95 95 def allhunks(self):
96 96 for h in self.header:
97 97 if self.allhunks_re.match(h):
98 98 return True
99 99
100 100 def files(self):
101 101 fromfile, tofile = self.diff_re.match(self.header[0]).groups()
102 102 if fromfile == tofile:
103 103 return [fromfile]
104 104 return [fromfile, tofile]
105 105
106 106 def filename(self):
107 107 return self.files()[-1]
108 108
109 109 def __repr__(self):
110 110 return '<header %s>' % (' '.join(map(repr, self.files())))
111 111
112 112 def special(self):
113 113 for h in self.header:
114 114 if self.special_re.match(h):
115 115 return True
116 116
117 117 def countchanges(hunk):
118 118 add = len([h for h in hunk if h[0] == '+'])
119 119 rem = len([h for h in hunk if h[0] == '-'])
120 120 return add, rem
121 121
122 122 class hunk(object):
123 123 maxcontext = 3
124 124
125 125 def __init__(self, header, fromline, toline, proc, before, hunk, after):
126 126 def trimcontext(number, lines):
127 127 delta = len(lines) - self.maxcontext
128 128 if False and delta > 0:
129 129 return number + delta, lines[:self.maxcontext]
130 130 return number, lines
131 131
132 132 self.header = header
133 133 self.fromline, self.before = trimcontext(fromline, before)
134 134 self.toline, self.after = trimcontext(toline, after)
135 135 self.proc = proc
136 136 self.hunk = hunk
137 137 self.added, self.removed = countchanges(self.hunk)
138 138
139 139 def write(self, fp):
140 140 delta = len(self.before) + len(self.after)
141 141 fromlen = delta + self.removed
142 142 tolen = delta + self.added
143 143 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
144 144 (self.fromline, fromlen, self.toline, tolen,
145 145 self.proc and (' ' + self.proc)))
146 146 fp.write(''.join(self.before + self.hunk + self.after))
147 147
148 148 pretty = write
149 149
150 150 def filename(self):
151 151 return self.header.filename()
152 152
153 153 def __repr__(self):
154 154 return '<hunk %r@%d>' % (self.filename(), self.fromline)
155 155
156 156 def parsepatch(fp):
157 157 class parser(object):
158 158 def __init__(self):
159 159 self.fromline = 0
160 160 self.toline = 0
161 161 self.proc = ''
162 162 self.header = None
163 163 self.context = []
164 164 self.before = []
165 165 self.hunk = []
166 166 self.stream = []
167 167
168 168 def addrange(self, (fromstart, fromend, tostart, toend, proc)):
169 169 self.fromline = int(fromstart)
170 170 self.toline = int(tostart)
171 171 self.proc = proc
172 172
173 173 def addcontext(self, context):
174 174 if self.hunk:
175 175 h = hunk(self.header, self.fromline, self.toline, self.proc,
176 176 self.before, self.hunk, context)
177 177 self.header.hunks.append(h)
178 178 self.stream.append(h)
179 179 self.fromline += len(self.before) + h.removed
180 180 self.toline += len(self.before) + h.added
181 181 self.before = []
182 182 self.hunk = []
183 183 self.proc = ''
184 184 self.context = context
185 185
186 186 def addhunk(self, hunk):
187 187 if self.context:
188 188 self.before = self.context
189 189 self.context = []
190 190 self.hunk = data
191 191
192 192 def newfile(self, hdr):
193 193 self.addcontext([])
194 194 h = header(hdr)
195 195 self.stream.append(h)
196 196 self.header = h
197 197
198 198 def finished(self):
199 199 self.addcontext([])
200 200 return self.stream
201 201
202 202 transitions = {
203 203 'file': {'context': addcontext,
204 204 'file': newfile,
205 205 'hunk': addhunk,
206 206 'range': addrange},
207 207 'context': {'file': newfile,
208 208 'hunk': addhunk,
209 209 'range': addrange},
210 210 'hunk': {'context': addcontext,
211 211 'file': newfile,
212 212 'range': addrange},
213 213 'range': {'context': addcontext,
214 214 'hunk': addhunk},
215 215 }
216 216
217 217 p = parser()
218 218
219 219 state = 'context'
220 220 for newstate, data in scanpatch(fp):
221 221 try:
222 222 p.transitions[state][newstate](p, data)
223 223 except KeyError:
224 224 raise patch.PatchError('unhandled transition: %s -> %s' %
225 225 (state, newstate))
226 226 state = newstate
227 227 return p.finished()
228 228
229 229 def filterpatch(ui, chunks):
230 230 chunks = list(chunks)
231 231 chunks.reverse()
232 232 seen = {}
233 233 def consumefile():
234 234 consumed = []
235 235 while chunks:
236 236 if isinstance(chunks[-1], header):
237 237 break
238 238 else:
239 239 consumed.append(chunks.pop())
240 240 return consumed
241 241 resp = None
242 242 applied = {}
243 243 while chunks:
244 244 chunk = chunks.pop()
245 245 if isinstance(chunk, header):
246 246 fixoffset = 0
247 247 hdr = ''.join(chunk.header)
248 248 if hdr in seen:
249 249 consumefile()
250 250 continue
251 251 seen[hdr] = True
252 252 if not resp:
253 253 chunk.pretty(ui)
254 254 r = resp or ui.prompt(_('record changes to %s? [y]es [n]o') %
255 255 _(' and ').join(map(repr, chunk.files())),
256 256 '(?:|[yYnNqQaA])$') or 'y'
257 257 if r in 'aA':
258 258 r = 'y'
259 259 resp = 'y'
260 260 if r in 'qQ':
261 261 raise util.Abort(_('user quit'))
262 262 if r in 'yY':
263 263 applied[chunk.filename()] = [chunk]
264 264 if chunk.allhunks():
265 265 applied[chunk.filename()] += consumefile()
266 266 else:
267 267 consumefile()
268 268 else:
269 269 if not resp:
270 270 chunk.pretty(ui)
271 271 r = resp or ui.prompt(_('record this change to %r? [y]es [n]o') %
272 272 chunk.filename(), '(?:|[yYnNqQaA])$') or 'y'
273 273 if r in 'aA':
274 274 r = 'y'
275 275 resp = 'y'
276 276 if r in 'qQ':
277 277 raise util.Abort(_('user quit'))
278 278 if r in 'yY':
279 279 if fixoffset:
280 280 chunk = copy.copy(chunk)
281 281 chunk.toline += fixoffset
282 282 applied[chunk.filename()].append(chunk)
283 283 else:
284 284 fixoffset += chunk.removed - chunk.added
285 285 return reduce(operator.add, [h for h in applied.itervalues()
286 286 if h[0].special() or len(h) > 1], [])
287 287
288 288 def record(ui, repo, *pats, **opts):
289 289 '''interactively select changes to commit'''
290 290
291 291 if not ui.interactive:
292 292 raise util.Abort(_('running non-interactively, use commit instead'))
293 293
294 294 def recordfunc(ui, repo, files, message, match, opts):
295 295 if files:
296 296 changes = None
297 297 else:
298 298 changes = repo.status(files=files, match=match)[:5]
299 299 modified, added, removed = changes[:3]
300 300 files = modified + added + removed
301 301 diffopts = mdiff.diffopts(git=True, nodates=True)
302 302 fp = cStringIO.StringIO()
303 303 patch.diff(repo, repo.dirstate.parents()[0], files=files,
304 304 match=match, changes=changes, opts=diffopts, fp=fp)
305 305 fp.seek(0)
306 306
307 307 chunks = filterpatch(ui, parsepatch(fp))
308 308 del fp
309 309
310 310 contenders = {}
311 311 for h in chunks:
312 312 try: contenders.update(dict.fromkeys(h.files()))
313 313 except AttributeError: pass
314 314
315 315 newfiles = [f for f in files if f in contenders]
316 316
317 317 if not newfiles:
318 318 ui.status(_('no changes to record\n'))
319 319 return 0
320 320
321 321 if changes is None:
322 322 changes = repo.status(files=newfiles, match=match)[:5]
323 323 modified = dict.fromkeys(changes[0])
324 324
325 325 backups = {}
326 326 backupdir = repo.join('record-backups')
327 327 try:
328 328 os.mkdir(backupdir)
329 329 except OSError, err:
330 if err.errno == errno.EEXIST:
331 pass
330 if err.errno != errno.EEXIST:
331 raise
332 332 try:
333 333 for f in newfiles:
334 334 if f not in modified:
335 335 continue
336 336 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
337 337 dir=backupdir)
338 338 os.close(fd)
339 339 ui.debug('backup %r as %r\n' % (f, tmpname))
340 340 util.copyfile(repo.wjoin(f), tmpname)
341 341 backups[f] = tmpname
342 342
343 343 fp = cStringIO.StringIO()
344 344 for c in chunks:
345 345 if c.filename() in backups:
346 346 c.write(fp)
347 347 dopatch = fp.tell()
348 348 fp.seek(0)
349 349
350 350 if backups:
351 351 hg.revert(repo, repo.dirstate.parents()[0], backups.has_key)
352 352
353 353 if dopatch:
354 354 ui.debug('applying patch\n')
355 355 ui.debug(fp.getvalue())
356 356 patch.internalpatch(fp, ui, 1, repo.root)
357 357 del fp
358 358
359 359 repo.commit(newfiles, message, opts['user'], opts['date'], match,
360 360 force_editor=opts.get('force_editor'))
361 361 return 0
362 362 finally:
363 363 try:
364 364 for realname, tmpname in backups.iteritems():
365 365 ui.debug('restoring %r to %r\n' % (tmpname, realname))
366 366 util.copyfile(tmpname, repo.wjoin(realname))
367 367 os.unlink(tmpname)
368 368 os.rmdir(backupdir)
369 369 except OSError:
370 370 pass
371 371 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
372 372
373 373 cmdtable = {
374 374 "record":
375 375 (record,
376 376 [('A', 'addremove', None,
377 377 _('mark new/missing files as added/removed before committing')),
378 378 ('d', 'date', '', _('record datecode as commit date')),
379 379 ('u', 'user', '', _('record user as commiter')),
380 380 ] + commands.walkopts + commands.commitopts,
381 381 _('hg record [OPTION]... [FILE]...')),
382 382 }
General Comments 0
You need to be logged in to leave comments. Login now