##// END OF EJS Templates
subrepo: use posixpath when diffing, for consistent paths...
Bryan O'Sullivan -
r17968:a9f4a607 default
parent child Browse files
Show More
@@ -1,1890 +1,1890 b''
1 # patch.py - patch file parsing routines
1 # patch.py - patch file parsing routines
2 #
2 #
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 import cStringIO, email.Parser, os, errno, re
9 import cStringIO, email.Parser, os, errno, re, posixpath
10 import tempfile, zlib, shutil
10 import tempfile, zlib, shutil
11
11
12 from i18n import _
12 from i18n import _
13 from node import hex, nullid, short
13 from node import hex, nullid, short
14 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding, error
14 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding, error
15 import context
15 import context
16
16
17 gitre = re.compile('diff --git a/(.*) b/(.*)')
17 gitre = re.compile('diff --git a/(.*) b/(.*)')
18
18
19 class PatchError(Exception):
19 class PatchError(Exception):
20 pass
20 pass
21
21
22
22
23 # public functions
23 # public functions
24
24
25 def split(stream):
25 def split(stream):
26 '''return an iterator of individual patches from a stream'''
26 '''return an iterator of individual patches from a stream'''
27 def isheader(line, inheader):
27 def isheader(line, inheader):
28 if inheader and line[0] in (' ', '\t'):
28 if inheader and line[0] in (' ', '\t'):
29 # continuation
29 # continuation
30 return True
30 return True
31 if line[0] in (' ', '-', '+'):
31 if line[0] in (' ', '-', '+'):
32 # diff line - don't check for header pattern in there
32 # diff line - don't check for header pattern in there
33 return False
33 return False
34 l = line.split(': ', 1)
34 l = line.split(': ', 1)
35 return len(l) == 2 and ' ' not in l[0]
35 return len(l) == 2 and ' ' not in l[0]
36
36
37 def chunk(lines):
37 def chunk(lines):
38 return cStringIO.StringIO(''.join(lines))
38 return cStringIO.StringIO(''.join(lines))
39
39
40 def hgsplit(stream, cur):
40 def hgsplit(stream, cur):
41 inheader = True
41 inheader = True
42
42
43 for line in stream:
43 for line in stream:
44 if not line.strip():
44 if not line.strip():
45 inheader = False
45 inheader = False
46 if not inheader and line.startswith('# HG changeset patch'):
46 if not inheader and line.startswith('# HG changeset patch'):
47 yield chunk(cur)
47 yield chunk(cur)
48 cur = []
48 cur = []
49 inheader = True
49 inheader = True
50
50
51 cur.append(line)
51 cur.append(line)
52
52
53 if cur:
53 if cur:
54 yield chunk(cur)
54 yield chunk(cur)
55
55
56 def mboxsplit(stream, cur):
56 def mboxsplit(stream, cur):
57 for line in stream:
57 for line in stream:
58 if line.startswith('From '):
58 if line.startswith('From '):
59 for c in split(chunk(cur[1:])):
59 for c in split(chunk(cur[1:])):
60 yield c
60 yield c
61 cur = []
61 cur = []
62
62
63 cur.append(line)
63 cur.append(line)
64
64
65 if cur:
65 if cur:
66 for c in split(chunk(cur[1:])):
66 for c in split(chunk(cur[1:])):
67 yield c
67 yield c
68
68
69 def mimesplit(stream, cur):
69 def mimesplit(stream, cur):
70 def msgfp(m):
70 def msgfp(m):
71 fp = cStringIO.StringIO()
71 fp = cStringIO.StringIO()
72 g = email.Generator.Generator(fp, mangle_from_=False)
72 g = email.Generator.Generator(fp, mangle_from_=False)
73 g.flatten(m)
73 g.flatten(m)
74 fp.seek(0)
74 fp.seek(0)
75 return fp
75 return fp
76
76
77 for line in stream:
77 for line in stream:
78 cur.append(line)
78 cur.append(line)
79 c = chunk(cur)
79 c = chunk(cur)
80
80
81 m = email.Parser.Parser().parse(c)
81 m = email.Parser.Parser().parse(c)
82 if not m.is_multipart():
82 if not m.is_multipart():
83 yield msgfp(m)
83 yield msgfp(m)
84 else:
84 else:
85 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
85 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
86 for part in m.walk():
86 for part in m.walk():
87 ct = part.get_content_type()
87 ct = part.get_content_type()
88 if ct not in ok_types:
88 if ct not in ok_types:
89 continue
89 continue
90 yield msgfp(part)
90 yield msgfp(part)
91
91
92 def headersplit(stream, cur):
92 def headersplit(stream, cur):
93 inheader = False
93 inheader = False
94
94
95 for line in stream:
95 for line in stream:
96 if not inheader and isheader(line, inheader):
96 if not inheader and isheader(line, inheader):
97 yield chunk(cur)
97 yield chunk(cur)
98 cur = []
98 cur = []
99 inheader = True
99 inheader = True
100 if inheader and not isheader(line, inheader):
100 if inheader and not isheader(line, inheader):
101 inheader = False
101 inheader = False
102
102
103 cur.append(line)
103 cur.append(line)
104
104
105 if cur:
105 if cur:
106 yield chunk(cur)
106 yield chunk(cur)
107
107
108 def remainder(cur):
108 def remainder(cur):
109 yield chunk(cur)
109 yield chunk(cur)
110
110
111 class fiter(object):
111 class fiter(object):
112 def __init__(self, fp):
112 def __init__(self, fp):
113 self.fp = fp
113 self.fp = fp
114
114
115 def __iter__(self):
115 def __iter__(self):
116 return self
116 return self
117
117
118 def next(self):
118 def next(self):
119 l = self.fp.readline()
119 l = self.fp.readline()
120 if not l:
120 if not l:
121 raise StopIteration
121 raise StopIteration
122 return l
122 return l
123
123
124 inheader = False
124 inheader = False
125 cur = []
125 cur = []
126
126
127 mimeheaders = ['content-type']
127 mimeheaders = ['content-type']
128
128
129 if not util.safehasattr(stream, 'next'):
129 if not util.safehasattr(stream, 'next'):
130 # http responses, for example, have readline but not next
130 # http responses, for example, have readline but not next
131 stream = fiter(stream)
131 stream = fiter(stream)
132
132
133 for line in stream:
133 for line in stream:
134 cur.append(line)
134 cur.append(line)
135 if line.startswith('# HG changeset patch'):
135 if line.startswith('# HG changeset patch'):
136 return hgsplit(stream, cur)
136 return hgsplit(stream, cur)
137 elif line.startswith('From '):
137 elif line.startswith('From '):
138 return mboxsplit(stream, cur)
138 return mboxsplit(stream, cur)
139 elif isheader(line, inheader):
139 elif isheader(line, inheader):
140 inheader = True
140 inheader = True
141 if line.split(':', 1)[0].lower() in mimeheaders:
141 if line.split(':', 1)[0].lower() in mimeheaders:
142 # let email parser handle this
142 # let email parser handle this
143 return mimesplit(stream, cur)
143 return mimesplit(stream, cur)
144 elif line.startswith('--- ') and inheader:
144 elif line.startswith('--- ') and inheader:
145 # No evil headers seen by diff start, split by hand
145 # No evil headers seen by diff start, split by hand
146 return headersplit(stream, cur)
146 return headersplit(stream, cur)
147 # Not enough info, keep reading
147 # Not enough info, keep reading
148
148
149 # if we are here, we have a very plain patch
149 # if we are here, we have a very plain patch
150 return remainder(cur)
150 return remainder(cur)
151
151
152 def extract(ui, fileobj):
152 def extract(ui, fileobj):
153 '''extract patch from data read from fileobj.
153 '''extract patch from data read from fileobj.
154
154
155 patch can be a normal patch or contained in an email message.
155 patch can be a normal patch or contained in an email message.
156
156
157 return tuple (filename, message, user, date, branch, node, p1, p2).
157 return tuple (filename, message, user, date, branch, node, p1, p2).
158 Any item in the returned tuple can be None. If filename is None,
158 Any item in the returned tuple can be None. If filename is None,
159 fileobj did not contain a patch. Caller must unlink filename when done.'''
159 fileobj did not contain a patch. Caller must unlink filename when done.'''
160
160
161 # attempt to detect the start of a patch
161 # attempt to detect the start of a patch
162 # (this heuristic is borrowed from quilt)
162 # (this heuristic is borrowed from quilt)
163 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
163 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
164 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
164 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
165 r'---[ \t].*?^\+\+\+[ \t]|'
165 r'---[ \t].*?^\+\+\+[ \t]|'
166 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
166 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
167
167
168 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
168 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
169 tmpfp = os.fdopen(fd, 'w')
169 tmpfp = os.fdopen(fd, 'w')
170 try:
170 try:
171 msg = email.Parser.Parser().parse(fileobj)
171 msg = email.Parser.Parser().parse(fileobj)
172
172
173 subject = msg['Subject']
173 subject = msg['Subject']
174 user = msg['From']
174 user = msg['From']
175 if not subject and not user:
175 if not subject and not user:
176 # Not an email, restore parsed headers if any
176 # Not an email, restore parsed headers if any
177 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
177 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
178
178
179 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
179 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
180 # should try to parse msg['Date']
180 # should try to parse msg['Date']
181 date = None
181 date = None
182 nodeid = None
182 nodeid = None
183 branch = None
183 branch = None
184 parents = []
184 parents = []
185
185
186 if subject:
186 if subject:
187 if subject.startswith('[PATCH'):
187 if subject.startswith('[PATCH'):
188 pend = subject.find(']')
188 pend = subject.find(']')
189 if pend >= 0:
189 if pend >= 0:
190 subject = subject[pend + 1:].lstrip()
190 subject = subject[pend + 1:].lstrip()
191 subject = re.sub(r'\n[ \t]+', ' ', subject)
191 subject = re.sub(r'\n[ \t]+', ' ', subject)
192 ui.debug('Subject: %s\n' % subject)
192 ui.debug('Subject: %s\n' % subject)
193 if user:
193 if user:
194 ui.debug('From: %s\n' % user)
194 ui.debug('From: %s\n' % user)
195 diffs_seen = 0
195 diffs_seen = 0
196 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
196 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
197 message = ''
197 message = ''
198 for part in msg.walk():
198 for part in msg.walk():
199 content_type = part.get_content_type()
199 content_type = part.get_content_type()
200 ui.debug('Content-Type: %s\n' % content_type)
200 ui.debug('Content-Type: %s\n' % content_type)
201 if content_type not in ok_types:
201 if content_type not in ok_types:
202 continue
202 continue
203 payload = part.get_payload(decode=True)
203 payload = part.get_payload(decode=True)
204 m = diffre.search(payload)
204 m = diffre.search(payload)
205 if m:
205 if m:
206 hgpatch = False
206 hgpatch = False
207 hgpatchheader = False
207 hgpatchheader = False
208 ignoretext = False
208 ignoretext = False
209
209
210 ui.debug('found patch at byte %d\n' % m.start(0))
210 ui.debug('found patch at byte %d\n' % m.start(0))
211 diffs_seen += 1
211 diffs_seen += 1
212 cfp = cStringIO.StringIO()
212 cfp = cStringIO.StringIO()
213 for line in payload[:m.start(0)].splitlines():
213 for line in payload[:m.start(0)].splitlines():
214 if line.startswith('# HG changeset patch') and not hgpatch:
214 if line.startswith('# HG changeset patch') and not hgpatch:
215 ui.debug('patch generated by hg export\n')
215 ui.debug('patch generated by hg export\n')
216 hgpatch = True
216 hgpatch = True
217 hgpatchheader = True
217 hgpatchheader = True
218 # drop earlier commit message content
218 # drop earlier commit message content
219 cfp.seek(0)
219 cfp.seek(0)
220 cfp.truncate()
220 cfp.truncate()
221 subject = None
221 subject = None
222 elif hgpatchheader:
222 elif hgpatchheader:
223 if line.startswith('# User '):
223 if line.startswith('# User '):
224 user = line[7:]
224 user = line[7:]
225 ui.debug('From: %s\n' % user)
225 ui.debug('From: %s\n' % user)
226 elif line.startswith("# Date "):
226 elif line.startswith("# Date "):
227 date = line[7:]
227 date = line[7:]
228 elif line.startswith("# Branch "):
228 elif line.startswith("# Branch "):
229 branch = line[9:]
229 branch = line[9:]
230 elif line.startswith("# Node ID "):
230 elif line.startswith("# Node ID "):
231 nodeid = line[10:]
231 nodeid = line[10:]
232 elif line.startswith("# Parent "):
232 elif line.startswith("# Parent "):
233 parents.append(line[9:].lstrip())
233 parents.append(line[9:].lstrip())
234 elif not line.startswith("# "):
234 elif not line.startswith("# "):
235 hgpatchheader = False
235 hgpatchheader = False
236 elif line == '---' and gitsendmail:
236 elif line == '---' and gitsendmail:
237 ignoretext = True
237 ignoretext = True
238 if not hgpatchheader and not ignoretext:
238 if not hgpatchheader and not ignoretext:
239 cfp.write(line)
239 cfp.write(line)
240 cfp.write('\n')
240 cfp.write('\n')
241 message = cfp.getvalue()
241 message = cfp.getvalue()
242 if tmpfp:
242 if tmpfp:
243 tmpfp.write(payload)
243 tmpfp.write(payload)
244 if not payload.endswith('\n'):
244 if not payload.endswith('\n'):
245 tmpfp.write('\n')
245 tmpfp.write('\n')
246 elif not diffs_seen and message and content_type == 'text/plain':
246 elif not diffs_seen and message and content_type == 'text/plain':
247 message += '\n' + payload
247 message += '\n' + payload
248 except: # re-raises
248 except: # re-raises
249 tmpfp.close()
249 tmpfp.close()
250 os.unlink(tmpname)
250 os.unlink(tmpname)
251 raise
251 raise
252
252
253 if subject and not message.startswith(subject):
253 if subject and not message.startswith(subject):
254 message = '%s\n%s' % (subject, message)
254 message = '%s\n%s' % (subject, message)
255 tmpfp.close()
255 tmpfp.close()
256 if not diffs_seen:
256 if not diffs_seen:
257 os.unlink(tmpname)
257 os.unlink(tmpname)
258 return None, message, user, date, branch, None, None, None
258 return None, message, user, date, branch, None, None, None
259 p1 = parents and parents.pop(0) or None
259 p1 = parents and parents.pop(0) or None
260 p2 = parents and parents.pop(0) or None
260 p2 = parents and parents.pop(0) or None
261 return tmpname, message, user, date, branch, nodeid, p1, p2
261 return tmpname, message, user, date, branch, nodeid, p1, p2
262
262
263 class patchmeta(object):
263 class patchmeta(object):
264 """Patched file metadata
264 """Patched file metadata
265
265
266 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
266 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
267 or COPY. 'path' is patched file path. 'oldpath' is set to the
267 or COPY. 'path' is patched file path. 'oldpath' is set to the
268 origin file when 'op' is either COPY or RENAME, None otherwise. If
268 origin file when 'op' is either COPY or RENAME, None otherwise. If
269 file mode is changed, 'mode' is a tuple (islink, isexec) where
269 file mode is changed, 'mode' is a tuple (islink, isexec) where
270 'islink' is True if the file is a symlink and 'isexec' is True if
270 'islink' is True if the file is a symlink and 'isexec' is True if
271 the file is executable. Otherwise, 'mode' is None.
271 the file is executable. Otherwise, 'mode' is None.
272 """
272 """
273 def __init__(self, path):
273 def __init__(self, path):
274 self.path = path
274 self.path = path
275 self.oldpath = None
275 self.oldpath = None
276 self.mode = None
276 self.mode = None
277 self.op = 'MODIFY'
277 self.op = 'MODIFY'
278 self.binary = False
278 self.binary = False
279
279
280 def setmode(self, mode):
280 def setmode(self, mode):
281 islink = mode & 020000
281 islink = mode & 020000
282 isexec = mode & 0100
282 isexec = mode & 0100
283 self.mode = (islink, isexec)
283 self.mode = (islink, isexec)
284
284
285 def copy(self):
285 def copy(self):
286 other = patchmeta(self.path)
286 other = patchmeta(self.path)
287 other.oldpath = self.oldpath
287 other.oldpath = self.oldpath
288 other.mode = self.mode
288 other.mode = self.mode
289 other.op = self.op
289 other.op = self.op
290 other.binary = self.binary
290 other.binary = self.binary
291 return other
291 return other
292
292
293 def _ispatchinga(self, afile):
293 def _ispatchinga(self, afile):
294 if afile == '/dev/null':
294 if afile == '/dev/null':
295 return self.op == 'ADD'
295 return self.op == 'ADD'
296 return afile == 'a/' + (self.oldpath or self.path)
296 return afile == 'a/' + (self.oldpath or self.path)
297
297
298 def _ispatchingb(self, bfile):
298 def _ispatchingb(self, bfile):
299 if bfile == '/dev/null':
299 if bfile == '/dev/null':
300 return self.op == 'DELETE'
300 return self.op == 'DELETE'
301 return bfile == 'b/' + self.path
301 return bfile == 'b/' + self.path
302
302
303 def ispatching(self, afile, bfile):
303 def ispatching(self, afile, bfile):
304 return self._ispatchinga(afile) and self._ispatchingb(bfile)
304 return self._ispatchinga(afile) and self._ispatchingb(bfile)
305
305
306 def __repr__(self):
306 def __repr__(self):
307 return "<patchmeta %s %r>" % (self.op, self.path)
307 return "<patchmeta %s %r>" % (self.op, self.path)
308
308
309 def readgitpatch(lr):
309 def readgitpatch(lr):
310 """extract git-style metadata about patches from <patchname>"""
310 """extract git-style metadata about patches from <patchname>"""
311
311
312 # Filter patch for git information
312 # Filter patch for git information
313 gp = None
313 gp = None
314 gitpatches = []
314 gitpatches = []
315 for line in lr:
315 for line in lr:
316 line = line.rstrip(' \r\n')
316 line = line.rstrip(' \r\n')
317 if line.startswith('diff --git'):
317 if line.startswith('diff --git'):
318 m = gitre.match(line)
318 m = gitre.match(line)
319 if m:
319 if m:
320 if gp:
320 if gp:
321 gitpatches.append(gp)
321 gitpatches.append(gp)
322 dst = m.group(2)
322 dst = m.group(2)
323 gp = patchmeta(dst)
323 gp = patchmeta(dst)
324 elif gp:
324 elif gp:
325 if line.startswith('--- '):
325 if line.startswith('--- '):
326 gitpatches.append(gp)
326 gitpatches.append(gp)
327 gp = None
327 gp = None
328 continue
328 continue
329 if line.startswith('rename from '):
329 if line.startswith('rename from '):
330 gp.op = 'RENAME'
330 gp.op = 'RENAME'
331 gp.oldpath = line[12:]
331 gp.oldpath = line[12:]
332 elif line.startswith('rename to '):
332 elif line.startswith('rename to '):
333 gp.path = line[10:]
333 gp.path = line[10:]
334 elif line.startswith('copy from '):
334 elif line.startswith('copy from '):
335 gp.op = 'COPY'
335 gp.op = 'COPY'
336 gp.oldpath = line[10:]
336 gp.oldpath = line[10:]
337 elif line.startswith('copy to '):
337 elif line.startswith('copy to '):
338 gp.path = line[8:]
338 gp.path = line[8:]
339 elif line.startswith('deleted file'):
339 elif line.startswith('deleted file'):
340 gp.op = 'DELETE'
340 gp.op = 'DELETE'
341 elif line.startswith('new file mode '):
341 elif line.startswith('new file mode '):
342 gp.op = 'ADD'
342 gp.op = 'ADD'
343 gp.setmode(int(line[-6:], 8))
343 gp.setmode(int(line[-6:], 8))
344 elif line.startswith('new mode '):
344 elif line.startswith('new mode '):
345 gp.setmode(int(line[-6:], 8))
345 gp.setmode(int(line[-6:], 8))
346 elif line.startswith('GIT binary patch'):
346 elif line.startswith('GIT binary patch'):
347 gp.binary = True
347 gp.binary = True
348 if gp:
348 if gp:
349 gitpatches.append(gp)
349 gitpatches.append(gp)
350
350
351 return gitpatches
351 return gitpatches
352
352
353 class linereader(object):
353 class linereader(object):
354 # simple class to allow pushing lines back into the input stream
354 # simple class to allow pushing lines back into the input stream
355 def __init__(self, fp):
355 def __init__(self, fp):
356 self.fp = fp
356 self.fp = fp
357 self.buf = []
357 self.buf = []
358
358
359 def push(self, line):
359 def push(self, line):
360 if line is not None:
360 if line is not None:
361 self.buf.append(line)
361 self.buf.append(line)
362
362
363 def readline(self):
363 def readline(self):
364 if self.buf:
364 if self.buf:
365 l = self.buf[0]
365 l = self.buf[0]
366 del self.buf[0]
366 del self.buf[0]
367 return l
367 return l
368 return self.fp.readline()
368 return self.fp.readline()
369
369
370 def __iter__(self):
370 def __iter__(self):
371 while True:
371 while True:
372 l = self.readline()
372 l = self.readline()
373 if not l:
373 if not l:
374 break
374 break
375 yield l
375 yield l
376
376
377 class abstractbackend(object):
377 class abstractbackend(object):
378 def __init__(self, ui):
378 def __init__(self, ui):
379 self.ui = ui
379 self.ui = ui
380
380
381 def getfile(self, fname):
381 def getfile(self, fname):
382 """Return target file data and flags as a (data, (islink,
382 """Return target file data and flags as a (data, (islink,
383 isexec)) tuple.
383 isexec)) tuple.
384 """
384 """
385 raise NotImplementedError
385 raise NotImplementedError
386
386
387 def setfile(self, fname, data, mode, copysource):
387 def setfile(self, fname, data, mode, copysource):
388 """Write data to target file fname and set its mode. mode is a
388 """Write data to target file fname and set its mode. mode is a
389 (islink, isexec) tuple. If data is None, the file content should
389 (islink, isexec) tuple. If data is None, the file content should
390 be left unchanged. If the file is modified after being copied,
390 be left unchanged. If the file is modified after being copied,
391 copysource is set to the original file name.
391 copysource is set to the original file name.
392 """
392 """
393 raise NotImplementedError
393 raise NotImplementedError
394
394
395 def unlink(self, fname):
395 def unlink(self, fname):
396 """Unlink target file."""
396 """Unlink target file."""
397 raise NotImplementedError
397 raise NotImplementedError
398
398
399 def writerej(self, fname, failed, total, lines):
399 def writerej(self, fname, failed, total, lines):
400 """Write rejected lines for fname. total is the number of hunks
400 """Write rejected lines for fname. total is the number of hunks
401 which failed to apply and total the total number of hunks for this
401 which failed to apply and total the total number of hunks for this
402 files.
402 files.
403 """
403 """
404 pass
404 pass
405
405
406 def exists(self, fname):
406 def exists(self, fname):
407 raise NotImplementedError
407 raise NotImplementedError
408
408
409 class fsbackend(abstractbackend):
409 class fsbackend(abstractbackend):
410 def __init__(self, ui, basedir):
410 def __init__(self, ui, basedir):
411 super(fsbackend, self).__init__(ui)
411 super(fsbackend, self).__init__(ui)
412 self.opener = scmutil.opener(basedir)
412 self.opener = scmutil.opener(basedir)
413
413
414 def _join(self, f):
414 def _join(self, f):
415 return os.path.join(self.opener.base, f)
415 return os.path.join(self.opener.base, f)
416
416
417 def getfile(self, fname):
417 def getfile(self, fname):
418 path = self._join(fname)
418 path = self._join(fname)
419 if os.path.islink(path):
419 if os.path.islink(path):
420 return (os.readlink(path), (True, False))
420 return (os.readlink(path), (True, False))
421 isexec = False
421 isexec = False
422 try:
422 try:
423 isexec = os.lstat(path).st_mode & 0100 != 0
423 isexec = os.lstat(path).st_mode & 0100 != 0
424 except OSError, e:
424 except OSError, e:
425 if e.errno != errno.ENOENT:
425 if e.errno != errno.ENOENT:
426 raise
426 raise
427 return (self.opener.read(fname), (False, isexec))
427 return (self.opener.read(fname), (False, isexec))
428
428
429 def setfile(self, fname, data, mode, copysource):
429 def setfile(self, fname, data, mode, copysource):
430 islink, isexec = mode
430 islink, isexec = mode
431 if data is None:
431 if data is None:
432 util.setflags(self._join(fname), islink, isexec)
432 util.setflags(self._join(fname), islink, isexec)
433 return
433 return
434 if islink:
434 if islink:
435 self.opener.symlink(data, fname)
435 self.opener.symlink(data, fname)
436 else:
436 else:
437 self.opener.write(fname, data)
437 self.opener.write(fname, data)
438 if isexec:
438 if isexec:
439 util.setflags(self._join(fname), False, True)
439 util.setflags(self._join(fname), False, True)
440
440
441 def unlink(self, fname):
441 def unlink(self, fname):
442 try:
442 try:
443 util.unlinkpath(self._join(fname))
443 util.unlinkpath(self._join(fname))
444 except OSError, inst:
444 except OSError, inst:
445 if inst.errno != errno.ENOENT:
445 if inst.errno != errno.ENOENT:
446 raise
446 raise
447
447
448 def writerej(self, fname, failed, total, lines):
448 def writerej(self, fname, failed, total, lines):
449 fname = fname + ".rej"
449 fname = fname + ".rej"
450 self.ui.warn(
450 self.ui.warn(
451 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
451 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
452 (failed, total, fname))
452 (failed, total, fname))
453 fp = self.opener(fname, 'w')
453 fp = self.opener(fname, 'w')
454 fp.writelines(lines)
454 fp.writelines(lines)
455 fp.close()
455 fp.close()
456
456
457 def exists(self, fname):
457 def exists(self, fname):
458 return os.path.lexists(self._join(fname))
458 return os.path.lexists(self._join(fname))
459
459
460 class workingbackend(fsbackend):
460 class workingbackend(fsbackend):
461 def __init__(self, ui, repo, similarity):
461 def __init__(self, ui, repo, similarity):
462 super(workingbackend, self).__init__(ui, repo.root)
462 super(workingbackend, self).__init__(ui, repo.root)
463 self.repo = repo
463 self.repo = repo
464 self.similarity = similarity
464 self.similarity = similarity
465 self.removed = set()
465 self.removed = set()
466 self.changed = set()
466 self.changed = set()
467 self.copied = []
467 self.copied = []
468
468
469 def _checkknown(self, fname):
469 def _checkknown(self, fname):
470 if self.repo.dirstate[fname] == '?' and self.exists(fname):
470 if self.repo.dirstate[fname] == '?' and self.exists(fname):
471 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
471 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
472
472
473 def setfile(self, fname, data, mode, copysource):
473 def setfile(self, fname, data, mode, copysource):
474 self._checkknown(fname)
474 self._checkknown(fname)
475 super(workingbackend, self).setfile(fname, data, mode, copysource)
475 super(workingbackend, self).setfile(fname, data, mode, copysource)
476 if copysource is not None:
476 if copysource is not None:
477 self.copied.append((copysource, fname))
477 self.copied.append((copysource, fname))
478 self.changed.add(fname)
478 self.changed.add(fname)
479
479
480 def unlink(self, fname):
480 def unlink(self, fname):
481 self._checkknown(fname)
481 self._checkknown(fname)
482 super(workingbackend, self).unlink(fname)
482 super(workingbackend, self).unlink(fname)
483 self.removed.add(fname)
483 self.removed.add(fname)
484 self.changed.add(fname)
484 self.changed.add(fname)
485
485
486 def close(self):
486 def close(self):
487 wctx = self.repo[None]
487 wctx = self.repo[None]
488 addremoved = set(self.changed)
488 addremoved = set(self.changed)
489 for src, dst in self.copied:
489 for src, dst in self.copied:
490 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
490 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
491 if self.removed:
491 if self.removed:
492 wctx.forget(sorted(self.removed))
492 wctx.forget(sorted(self.removed))
493 for f in self.removed:
493 for f in self.removed:
494 if f not in self.repo.dirstate:
494 if f not in self.repo.dirstate:
495 # File was deleted and no longer belongs to the
495 # File was deleted and no longer belongs to the
496 # dirstate, it was probably marked added then
496 # dirstate, it was probably marked added then
497 # deleted, and should not be considered by
497 # deleted, and should not be considered by
498 # addremove().
498 # addremove().
499 addremoved.discard(f)
499 addremoved.discard(f)
500 if addremoved:
500 if addremoved:
501 cwd = self.repo.getcwd()
501 cwd = self.repo.getcwd()
502 if cwd:
502 if cwd:
503 addremoved = [util.pathto(self.repo.root, cwd, f)
503 addremoved = [util.pathto(self.repo.root, cwd, f)
504 for f in addremoved]
504 for f in addremoved]
505 scmutil.addremove(self.repo, addremoved, similarity=self.similarity)
505 scmutil.addremove(self.repo, addremoved, similarity=self.similarity)
506 return sorted(self.changed)
506 return sorted(self.changed)
507
507
508 class filestore(object):
508 class filestore(object):
509 def __init__(self, maxsize=None):
509 def __init__(self, maxsize=None):
510 self.opener = None
510 self.opener = None
511 self.files = {}
511 self.files = {}
512 self.created = 0
512 self.created = 0
513 self.maxsize = maxsize
513 self.maxsize = maxsize
514 if self.maxsize is None:
514 if self.maxsize is None:
515 self.maxsize = 4*(2**20)
515 self.maxsize = 4*(2**20)
516 self.size = 0
516 self.size = 0
517 self.data = {}
517 self.data = {}
518
518
519 def setfile(self, fname, data, mode, copied=None):
519 def setfile(self, fname, data, mode, copied=None):
520 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
520 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
521 self.data[fname] = (data, mode, copied)
521 self.data[fname] = (data, mode, copied)
522 self.size += len(data)
522 self.size += len(data)
523 else:
523 else:
524 if self.opener is None:
524 if self.opener is None:
525 root = tempfile.mkdtemp(prefix='hg-patch-')
525 root = tempfile.mkdtemp(prefix='hg-patch-')
526 self.opener = scmutil.opener(root)
526 self.opener = scmutil.opener(root)
527 # Avoid filename issues with these simple names
527 # Avoid filename issues with these simple names
528 fn = str(self.created)
528 fn = str(self.created)
529 self.opener.write(fn, data)
529 self.opener.write(fn, data)
530 self.created += 1
530 self.created += 1
531 self.files[fname] = (fn, mode, copied)
531 self.files[fname] = (fn, mode, copied)
532
532
533 def getfile(self, fname):
533 def getfile(self, fname):
534 if fname in self.data:
534 if fname in self.data:
535 return self.data[fname]
535 return self.data[fname]
536 if not self.opener or fname not in self.files:
536 if not self.opener or fname not in self.files:
537 raise IOError
537 raise IOError
538 fn, mode, copied = self.files[fname]
538 fn, mode, copied = self.files[fname]
539 return self.opener.read(fn), mode, copied
539 return self.opener.read(fn), mode, copied
540
540
541 def close(self):
541 def close(self):
542 if self.opener:
542 if self.opener:
543 shutil.rmtree(self.opener.base)
543 shutil.rmtree(self.opener.base)
544
544
545 class repobackend(abstractbackend):
545 class repobackend(abstractbackend):
546 def __init__(self, ui, repo, ctx, store):
546 def __init__(self, ui, repo, ctx, store):
547 super(repobackend, self).__init__(ui)
547 super(repobackend, self).__init__(ui)
548 self.repo = repo
548 self.repo = repo
549 self.ctx = ctx
549 self.ctx = ctx
550 self.store = store
550 self.store = store
551 self.changed = set()
551 self.changed = set()
552 self.removed = set()
552 self.removed = set()
553 self.copied = {}
553 self.copied = {}
554
554
555 def _checkknown(self, fname):
555 def _checkknown(self, fname):
556 if fname not in self.ctx:
556 if fname not in self.ctx:
557 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
557 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
558
558
559 def getfile(self, fname):
559 def getfile(self, fname):
560 try:
560 try:
561 fctx = self.ctx[fname]
561 fctx = self.ctx[fname]
562 except error.LookupError:
562 except error.LookupError:
563 raise IOError
563 raise IOError
564 flags = fctx.flags()
564 flags = fctx.flags()
565 return fctx.data(), ('l' in flags, 'x' in flags)
565 return fctx.data(), ('l' in flags, 'x' in flags)
566
566
567 def setfile(self, fname, data, mode, copysource):
567 def setfile(self, fname, data, mode, copysource):
568 if copysource:
568 if copysource:
569 self._checkknown(copysource)
569 self._checkknown(copysource)
570 if data is None:
570 if data is None:
571 data = self.ctx[fname].data()
571 data = self.ctx[fname].data()
572 self.store.setfile(fname, data, mode, copysource)
572 self.store.setfile(fname, data, mode, copysource)
573 self.changed.add(fname)
573 self.changed.add(fname)
574 if copysource:
574 if copysource:
575 self.copied[fname] = copysource
575 self.copied[fname] = copysource
576
576
577 def unlink(self, fname):
577 def unlink(self, fname):
578 self._checkknown(fname)
578 self._checkknown(fname)
579 self.removed.add(fname)
579 self.removed.add(fname)
580
580
581 def exists(self, fname):
581 def exists(self, fname):
582 return fname in self.ctx
582 return fname in self.ctx
583
583
584 def close(self):
584 def close(self):
585 return self.changed | self.removed
585 return self.changed | self.removed
586
586
587 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
587 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
588 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
588 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
589 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
589 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
590 eolmodes = ['strict', 'crlf', 'lf', 'auto']
590 eolmodes = ['strict', 'crlf', 'lf', 'auto']
591
591
592 class patchfile(object):
592 class patchfile(object):
593 def __init__(self, ui, gp, backend, store, eolmode='strict'):
593 def __init__(self, ui, gp, backend, store, eolmode='strict'):
594 self.fname = gp.path
594 self.fname = gp.path
595 self.eolmode = eolmode
595 self.eolmode = eolmode
596 self.eol = None
596 self.eol = None
597 self.backend = backend
597 self.backend = backend
598 self.ui = ui
598 self.ui = ui
599 self.lines = []
599 self.lines = []
600 self.exists = False
600 self.exists = False
601 self.missing = True
601 self.missing = True
602 self.mode = gp.mode
602 self.mode = gp.mode
603 self.copysource = gp.oldpath
603 self.copysource = gp.oldpath
604 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
604 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
605 self.remove = gp.op == 'DELETE'
605 self.remove = gp.op == 'DELETE'
606 try:
606 try:
607 if self.copysource is None:
607 if self.copysource is None:
608 data, mode = backend.getfile(self.fname)
608 data, mode = backend.getfile(self.fname)
609 self.exists = True
609 self.exists = True
610 else:
610 else:
611 data, mode = store.getfile(self.copysource)[:2]
611 data, mode = store.getfile(self.copysource)[:2]
612 self.exists = backend.exists(self.fname)
612 self.exists = backend.exists(self.fname)
613 self.missing = False
613 self.missing = False
614 if data:
614 if data:
615 self.lines = mdiff.splitnewlines(data)
615 self.lines = mdiff.splitnewlines(data)
616 if self.mode is None:
616 if self.mode is None:
617 self.mode = mode
617 self.mode = mode
618 if self.lines:
618 if self.lines:
619 # Normalize line endings
619 # Normalize line endings
620 if self.lines[0].endswith('\r\n'):
620 if self.lines[0].endswith('\r\n'):
621 self.eol = '\r\n'
621 self.eol = '\r\n'
622 elif self.lines[0].endswith('\n'):
622 elif self.lines[0].endswith('\n'):
623 self.eol = '\n'
623 self.eol = '\n'
624 if eolmode != 'strict':
624 if eolmode != 'strict':
625 nlines = []
625 nlines = []
626 for l in self.lines:
626 for l in self.lines:
627 if l.endswith('\r\n'):
627 if l.endswith('\r\n'):
628 l = l[:-2] + '\n'
628 l = l[:-2] + '\n'
629 nlines.append(l)
629 nlines.append(l)
630 self.lines = nlines
630 self.lines = nlines
631 except IOError:
631 except IOError:
632 if self.create:
632 if self.create:
633 self.missing = False
633 self.missing = False
634 if self.mode is None:
634 if self.mode is None:
635 self.mode = (False, False)
635 self.mode = (False, False)
636 if self.missing:
636 if self.missing:
637 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
637 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
638
638
639 self.hash = {}
639 self.hash = {}
640 self.dirty = 0
640 self.dirty = 0
641 self.offset = 0
641 self.offset = 0
642 self.skew = 0
642 self.skew = 0
643 self.rej = []
643 self.rej = []
644 self.fileprinted = False
644 self.fileprinted = False
645 self.printfile(False)
645 self.printfile(False)
646 self.hunks = 0
646 self.hunks = 0
647
647
648 def writelines(self, fname, lines, mode):
648 def writelines(self, fname, lines, mode):
649 if self.eolmode == 'auto':
649 if self.eolmode == 'auto':
650 eol = self.eol
650 eol = self.eol
651 elif self.eolmode == 'crlf':
651 elif self.eolmode == 'crlf':
652 eol = '\r\n'
652 eol = '\r\n'
653 else:
653 else:
654 eol = '\n'
654 eol = '\n'
655
655
656 if self.eolmode != 'strict' and eol and eol != '\n':
656 if self.eolmode != 'strict' and eol and eol != '\n':
657 rawlines = []
657 rawlines = []
658 for l in lines:
658 for l in lines:
659 if l and l[-1] == '\n':
659 if l and l[-1] == '\n':
660 l = l[:-1] + eol
660 l = l[:-1] + eol
661 rawlines.append(l)
661 rawlines.append(l)
662 lines = rawlines
662 lines = rawlines
663
663
664 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
664 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
665
665
666 def printfile(self, warn):
666 def printfile(self, warn):
667 if self.fileprinted:
667 if self.fileprinted:
668 return
668 return
669 if warn or self.ui.verbose:
669 if warn or self.ui.verbose:
670 self.fileprinted = True
670 self.fileprinted = True
671 s = _("patching file %s\n") % self.fname
671 s = _("patching file %s\n") % self.fname
672 if warn:
672 if warn:
673 self.ui.warn(s)
673 self.ui.warn(s)
674 else:
674 else:
675 self.ui.note(s)
675 self.ui.note(s)
676
676
677
677
678 def findlines(self, l, linenum):
678 def findlines(self, l, linenum):
679 # looks through the hash and finds candidate lines. The
679 # looks through the hash and finds candidate lines. The
680 # result is a list of line numbers sorted based on distance
680 # result is a list of line numbers sorted based on distance
681 # from linenum
681 # from linenum
682
682
683 cand = self.hash.get(l, [])
683 cand = self.hash.get(l, [])
684 if len(cand) > 1:
684 if len(cand) > 1:
685 # resort our list of potentials forward then back.
685 # resort our list of potentials forward then back.
686 cand.sort(key=lambda x: abs(x - linenum))
686 cand.sort(key=lambda x: abs(x - linenum))
687 return cand
687 return cand
688
688
689 def write_rej(self):
689 def write_rej(self):
690 # our rejects are a little different from patch(1). This always
690 # our rejects are a little different from patch(1). This always
691 # creates rejects in the same form as the original patch. A file
691 # creates rejects in the same form as the original patch. A file
692 # header is inserted so that you can run the reject through patch again
692 # header is inserted so that you can run the reject through patch again
693 # without having to type the filename.
693 # without having to type the filename.
694 if not self.rej:
694 if not self.rej:
695 return
695 return
696 base = os.path.basename(self.fname)
696 base = os.path.basename(self.fname)
697 lines = ["--- %s\n+++ %s\n" % (base, base)]
697 lines = ["--- %s\n+++ %s\n" % (base, base)]
698 for x in self.rej:
698 for x in self.rej:
699 for l in x.hunk:
699 for l in x.hunk:
700 lines.append(l)
700 lines.append(l)
701 if l[-1] != '\n':
701 if l[-1] != '\n':
702 lines.append("\n\ No newline at end of file\n")
702 lines.append("\n\ No newline at end of file\n")
703 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
703 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
704
704
705 def apply(self, h):
705 def apply(self, h):
706 if not h.complete():
706 if not h.complete():
707 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
707 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
708 (h.number, h.desc, len(h.a), h.lena, len(h.b),
708 (h.number, h.desc, len(h.a), h.lena, len(h.b),
709 h.lenb))
709 h.lenb))
710
710
711 self.hunks += 1
711 self.hunks += 1
712
712
713 if self.missing:
713 if self.missing:
714 self.rej.append(h)
714 self.rej.append(h)
715 return -1
715 return -1
716
716
717 if self.exists and self.create:
717 if self.exists and self.create:
718 if self.copysource:
718 if self.copysource:
719 self.ui.warn(_("cannot create %s: destination already "
719 self.ui.warn(_("cannot create %s: destination already "
720 "exists\n" % self.fname))
720 "exists\n" % self.fname))
721 else:
721 else:
722 self.ui.warn(_("file %s already exists\n") % self.fname)
722 self.ui.warn(_("file %s already exists\n") % self.fname)
723 self.rej.append(h)
723 self.rej.append(h)
724 return -1
724 return -1
725
725
726 if isinstance(h, binhunk):
726 if isinstance(h, binhunk):
727 if self.remove:
727 if self.remove:
728 self.backend.unlink(self.fname)
728 self.backend.unlink(self.fname)
729 else:
729 else:
730 self.lines[:] = h.new()
730 self.lines[:] = h.new()
731 self.offset += len(h.new())
731 self.offset += len(h.new())
732 self.dirty = True
732 self.dirty = True
733 return 0
733 return 0
734
734
735 horig = h
735 horig = h
736 if (self.eolmode in ('crlf', 'lf')
736 if (self.eolmode in ('crlf', 'lf')
737 or self.eolmode == 'auto' and self.eol):
737 or self.eolmode == 'auto' and self.eol):
738 # If new eols are going to be normalized, then normalize
738 # If new eols are going to be normalized, then normalize
739 # hunk data before patching. Otherwise, preserve input
739 # hunk data before patching. Otherwise, preserve input
740 # line-endings.
740 # line-endings.
741 h = h.getnormalized()
741 h = h.getnormalized()
742
742
743 # fast case first, no offsets, no fuzz
743 # fast case first, no offsets, no fuzz
744 old, oldstart, new, newstart = h.fuzzit(0, False)
744 old, oldstart, new, newstart = h.fuzzit(0, False)
745 oldstart += self.offset
745 oldstart += self.offset
746 orig_start = oldstart
746 orig_start = oldstart
747 # if there's skew we want to emit the "(offset %d lines)" even
747 # if there's skew we want to emit the "(offset %d lines)" even
748 # when the hunk cleanly applies at start + skew, so skip the
748 # when the hunk cleanly applies at start + skew, so skip the
749 # fast case code
749 # fast case code
750 if (self.skew == 0 and
750 if (self.skew == 0 and
751 diffhelpers.testhunk(old, self.lines, oldstart) == 0):
751 diffhelpers.testhunk(old, self.lines, oldstart) == 0):
752 if self.remove:
752 if self.remove:
753 self.backend.unlink(self.fname)
753 self.backend.unlink(self.fname)
754 else:
754 else:
755 self.lines[oldstart:oldstart + len(old)] = new
755 self.lines[oldstart:oldstart + len(old)] = new
756 self.offset += len(new) - len(old)
756 self.offset += len(new) - len(old)
757 self.dirty = True
757 self.dirty = True
758 return 0
758 return 0
759
759
760 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
760 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
761 self.hash = {}
761 self.hash = {}
762 for x, s in enumerate(self.lines):
762 for x, s in enumerate(self.lines):
763 self.hash.setdefault(s, []).append(x)
763 self.hash.setdefault(s, []).append(x)
764
764
765 for fuzzlen in xrange(3):
765 for fuzzlen in xrange(3):
766 for toponly in [True, False]:
766 for toponly in [True, False]:
767 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
767 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
768 oldstart = oldstart + self.offset + self.skew
768 oldstart = oldstart + self.offset + self.skew
769 oldstart = min(oldstart, len(self.lines))
769 oldstart = min(oldstart, len(self.lines))
770 if old:
770 if old:
771 cand = self.findlines(old[0][1:], oldstart)
771 cand = self.findlines(old[0][1:], oldstart)
772 else:
772 else:
773 # Only adding lines with no or fuzzed context, just
773 # Only adding lines with no or fuzzed context, just
774 # take the skew in account
774 # take the skew in account
775 cand = [oldstart]
775 cand = [oldstart]
776
776
777 for l in cand:
777 for l in cand:
778 if not old or diffhelpers.testhunk(old, self.lines, l) == 0:
778 if not old or diffhelpers.testhunk(old, self.lines, l) == 0:
779 self.lines[l : l + len(old)] = new
779 self.lines[l : l + len(old)] = new
780 self.offset += len(new) - len(old)
780 self.offset += len(new) - len(old)
781 self.skew = l - orig_start
781 self.skew = l - orig_start
782 self.dirty = True
782 self.dirty = True
783 offset = l - orig_start - fuzzlen
783 offset = l - orig_start - fuzzlen
784 if fuzzlen:
784 if fuzzlen:
785 msg = _("Hunk #%d succeeded at %d "
785 msg = _("Hunk #%d succeeded at %d "
786 "with fuzz %d "
786 "with fuzz %d "
787 "(offset %d lines).\n")
787 "(offset %d lines).\n")
788 self.printfile(True)
788 self.printfile(True)
789 self.ui.warn(msg %
789 self.ui.warn(msg %
790 (h.number, l + 1, fuzzlen, offset))
790 (h.number, l + 1, fuzzlen, offset))
791 else:
791 else:
792 msg = _("Hunk #%d succeeded at %d "
792 msg = _("Hunk #%d succeeded at %d "
793 "(offset %d lines).\n")
793 "(offset %d lines).\n")
794 self.ui.note(msg % (h.number, l + 1, offset))
794 self.ui.note(msg % (h.number, l + 1, offset))
795 return fuzzlen
795 return fuzzlen
796 self.printfile(True)
796 self.printfile(True)
797 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
797 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
798 self.rej.append(horig)
798 self.rej.append(horig)
799 return -1
799 return -1
800
800
801 def close(self):
801 def close(self):
802 if self.dirty:
802 if self.dirty:
803 self.writelines(self.fname, self.lines, self.mode)
803 self.writelines(self.fname, self.lines, self.mode)
804 self.write_rej()
804 self.write_rej()
805 return len(self.rej)
805 return len(self.rej)
806
806
807 class hunk(object):
807 class hunk(object):
808 def __init__(self, desc, num, lr, context):
808 def __init__(self, desc, num, lr, context):
809 self.number = num
809 self.number = num
810 self.desc = desc
810 self.desc = desc
811 self.hunk = [desc]
811 self.hunk = [desc]
812 self.a = []
812 self.a = []
813 self.b = []
813 self.b = []
814 self.starta = self.lena = None
814 self.starta = self.lena = None
815 self.startb = self.lenb = None
815 self.startb = self.lenb = None
816 if lr is not None:
816 if lr is not None:
817 if context:
817 if context:
818 self.read_context_hunk(lr)
818 self.read_context_hunk(lr)
819 else:
819 else:
820 self.read_unified_hunk(lr)
820 self.read_unified_hunk(lr)
821
821
822 def getnormalized(self):
822 def getnormalized(self):
823 """Return a copy with line endings normalized to LF."""
823 """Return a copy with line endings normalized to LF."""
824
824
825 def normalize(lines):
825 def normalize(lines):
826 nlines = []
826 nlines = []
827 for line in lines:
827 for line in lines:
828 if line.endswith('\r\n'):
828 if line.endswith('\r\n'):
829 line = line[:-2] + '\n'
829 line = line[:-2] + '\n'
830 nlines.append(line)
830 nlines.append(line)
831 return nlines
831 return nlines
832
832
833 # Dummy object, it is rebuilt manually
833 # Dummy object, it is rebuilt manually
834 nh = hunk(self.desc, self.number, None, None)
834 nh = hunk(self.desc, self.number, None, None)
835 nh.number = self.number
835 nh.number = self.number
836 nh.desc = self.desc
836 nh.desc = self.desc
837 nh.hunk = self.hunk
837 nh.hunk = self.hunk
838 nh.a = normalize(self.a)
838 nh.a = normalize(self.a)
839 nh.b = normalize(self.b)
839 nh.b = normalize(self.b)
840 nh.starta = self.starta
840 nh.starta = self.starta
841 nh.startb = self.startb
841 nh.startb = self.startb
842 nh.lena = self.lena
842 nh.lena = self.lena
843 nh.lenb = self.lenb
843 nh.lenb = self.lenb
844 return nh
844 return nh
845
845
846 def read_unified_hunk(self, lr):
846 def read_unified_hunk(self, lr):
847 m = unidesc.match(self.desc)
847 m = unidesc.match(self.desc)
848 if not m:
848 if not m:
849 raise PatchError(_("bad hunk #%d") % self.number)
849 raise PatchError(_("bad hunk #%d") % self.number)
850 self.starta, self.lena, self.startb, self.lenb = m.groups()
850 self.starta, self.lena, self.startb, self.lenb = m.groups()
851 if self.lena is None:
851 if self.lena is None:
852 self.lena = 1
852 self.lena = 1
853 else:
853 else:
854 self.lena = int(self.lena)
854 self.lena = int(self.lena)
855 if self.lenb is None:
855 if self.lenb is None:
856 self.lenb = 1
856 self.lenb = 1
857 else:
857 else:
858 self.lenb = int(self.lenb)
858 self.lenb = int(self.lenb)
859 self.starta = int(self.starta)
859 self.starta = int(self.starta)
860 self.startb = int(self.startb)
860 self.startb = int(self.startb)
861 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a,
861 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a,
862 self.b)
862 self.b)
863 # if we hit eof before finishing out the hunk, the last line will
863 # if we hit eof before finishing out the hunk, the last line will
864 # be zero length. Lets try to fix it up.
864 # be zero length. Lets try to fix it up.
865 while len(self.hunk[-1]) == 0:
865 while len(self.hunk[-1]) == 0:
866 del self.hunk[-1]
866 del self.hunk[-1]
867 del self.a[-1]
867 del self.a[-1]
868 del self.b[-1]
868 del self.b[-1]
869 self.lena -= 1
869 self.lena -= 1
870 self.lenb -= 1
870 self.lenb -= 1
871 self._fixnewline(lr)
871 self._fixnewline(lr)
872
872
873 def read_context_hunk(self, lr):
873 def read_context_hunk(self, lr):
874 self.desc = lr.readline()
874 self.desc = lr.readline()
875 m = contextdesc.match(self.desc)
875 m = contextdesc.match(self.desc)
876 if not m:
876 if not m:
877 raise PatchError(_("bad hunk #%d") % self.number)
877 raise PatchError(_("bad hunk #%d") % self.number)
878 self.starta, aend = m.groups()
878 self.starta, aend = m.groups()
879 self.starta = int(self.starta)
879 self.starta = int(self.starta)
880 if aend is None:
880 if aend is None:
881 aend = self.starta
881 aend = self.starta
882 self.lena = int(aend) - self.starta
882 self.lena = int(aend) - self.starta
883 if self.starta:
883 if self.starta:
884 self.lena += 1
884 self.lena += 1
885 for x in xrange(self.lena):
885 for x in xrange(self.lena):
886 l = lr.readline()
886 l = lr.readline()
887 if l.startswith('---'):
887 if l.startswith('---'):
888 # lines addition, old block is empty
888 # lines addition, old block is empty
889 lr.push(l)
889 lr.push(l)
890 break
890 break
891 s = l[2:]
891 s = l[2:]
892 if l.startswith('- ') or l.startswith('! '):
892 if l.startswith('- ') or l.startswith('! '):
893 u = '-' + s
893 u = '-' + s
894 elif l.startswith(' '):
894 elif l.startswith(' '):
895 u = ' ' + s
895 u = ' ' + s
896 else:
896 else:
897 raise PatchError(_("bad hunk #%d old text line %d") %
897 raise PatchError(_("bad hunk #%d old text line %d") %
898 (self.number, x))
898 (self.number, x))
899 self.a.append(u)
899 self.a.append(u)
900 self.hunk.append(u)
900 self.hunk.append(u)
901
901
902 l = lr.readline()
902 l = lr.readline()
903 if l.startswith('\ '):
903 if l.startswith('\ '):
904 s = self.a[-1][:-1]
904 s = self.a[-1][:-1]
905 self.a[-1] = s
905 self.a[-1] = s
906 self.hunk[-1] = s
906 self.hunk[-1] = s
907 l = lr.readline()
907 l = lr.readline()
908 m = contextdesc.match(l)
908 m = contextdesc.match(l)
909 if not m:
909 if not m:
910 raise PatchError(_("bad hunk #%d") % self.number)
910 raise PatchError(_("bad hunk #%d") % self.number)
911 self.startb, bend = m.groups()
911 self.startb, bend = m.groups()
912 self.startb = int(self.startb)
912 self.startb = int(self.startb)
913 if bend is None:
913 if bend is None:
914 bend = self.startb
914 bend = self.startb
915 self.lenb = int(bend) - self.startb
915 self.lenb = int(bend) - self.startb
916 if self.startb:
916 if self.startb:
917 self.lenb += 1
917 self.lenb += 1
918 hunki = 1
918 hunki = 1
919 for x in xrange(self.lenb):
919 for x in xrange(self.lenb):
920 l = lr.readline()
920 l = lr.readline()
921 if l.startswith('\ '):
921 if l.startswith('\ '):
922 # XXX: the only way to hit this is with an invalid line range.
922 # XXX: the only way to hit this is with an invalid line range.
923 # The no-eol marker is not counted in the line range, but I
923 # The no-eol marker is not counted in the line range, but I
924 # guess there are diff(1) out there which behave differently.
924 # guess there are diff(1) out there which behave differently.
925 s = self.b[-1][:-1]
925 s = self.b[-1][:-1]
926 self.b[-1] = s
926 self.b[-1] = s
927 self.hunk[hunki - 1] = s
927 self.hunk[hunki - 1] = s
928 continue
928 continue
929 if not l:
929 if not l:
930 # line deletions, new block is empty and we hit EOF
930 # line deletions, new block is empty and we hit EOF
931 lr.push(l)
931 lr.push(l)
932 break
932 break
933 s = l[2:]
933 s = l[2:]
934 if l.startswith('+ ') or l.startswith('! '):
934 if l.startswith('+ ') or l.startswith('! '):
935 u = '+' + s
935 u = '+' + s
936 elif l.startswith(' '):
936 elif l.startswith(' '):
937 u = ' ' + s
937 u = ' ' + s
938 elif len(self.b) == 0:
938 elif len(self.b) == 0:
939 # line deletions, new block is empty
939 # line deletions, new block is empty
940 lr.push(l)
940 lr.push(l)
941 break
941 break
942 else:
942 else:
943 raise PatchError(_("bad hunk #%d old text line %d") %
943 raise PatchError(_("bad hunk #%d old text line %d") %
944 (self.number, x))
944 (self.number, x))
945 self.b.append(s)
945 self.b.append(s)
946 while True:
946 while True:
947 if hunki >= len(self.hunk):
947 if hunki >= len(self.hunk):
948 h = ""
948 h = ""
949 else:
949 else:
950 h = self.hunk[hunki]
950 h = self.hunk[hunki]
951 hunki += 1
951 hunki += 1
952 if h == u:
952 if h == u:
953 break
953 break
954 elif h.startswith('-'):
954 elif h.startswith('-'):
955 continue
955 continue
956 else:
956 else:
957 self.hunk.insert(hunki - 1, u)
957 self.hunk.insert(hunki - 1, u)
958 break
958 break
959
959
960 if not self.a:
960 if not self.a:
961 # this happens when lines were only added to the hunk
961 # this happens when lines were only added to the hunk
962 for x in self.hunk:
962 for x in self.hunk:
963 if x.startswith('-') or x.startswith(' '):
963 if x.startswith('-') or x.startswith(' '):
964 self.a.append(x)
964 self.a.append(x)
965 if not self.b:
965 if not self.b:
966 # this happens when lines were only deleted from the hunk
966 # this happens when lines were only deleted from the hunk
967 for x in self.hunk:
967 for x in self.hunk:
968 if x.startswith('+') or x.startswith(' '):
968 if x.startswith('+') or x.startswith(' '):
969 self.b.append(x[1:])
969 self.b.append(x[1:])
970 # @@ -start,len +start,len @@
970 # @@ -start,len +start,len @@
971 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
971 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
972 self.startb, self.lenb)
972 self.startb, self.lenb)
973 self.hunk[0] = self.desc
973 self.hunk[0] = self.desc
974 self._fixnewline(lr)
974 self._fixnewline(lr)
975
975
976 def _fixnewline(self, lr):
976 def _fixnewline(self, lr):
977 l = lr.readline()
977 l = lr.readline()
978 if l.startswith('\ '):
978 if l.startswith('\ '):
979 diffhelpers.fix_newline(self.hunk, self.a, self.b)
979 diffhelpers.fix_newline(self.hunk, self.a, self.b)
980 else:
980 else:
981 lr.push(l)
981 lr.push(l)
982
982
983 def complete(self):
983 def complete(self):
984 return len(self.a) == self.lena and len(self.b) == self.lenb
984 return len(self.a) == self.lena and len(self.b) == self.lenb
985
985
986 def _fuzzit(self, old, new, fuzz, toponly):
986 def _fuzzit(self, old, new, fuzz, toponly):
987 # this removes context lines from the top and bottom of list 'l'. It
987 # this removes context lines from the top and bottom of list 'l'. It
988 # checks the hunk to make sure only context lines are removed, and then
988 # checks the hunk to make sure only context lines are removed, and then
989 # returns a new shortened list of lines.
989 # returns a new shortened list of lines.
990 fuzz = min(fuzz, len(old))
990 fuzz = min(fuzz, len(old))
991 if fuzz:
991 if fuzz:
992 top = 0
992 top = 0
993 bot = 0
993 bot = 0
994 hlen = len(self.hunk)
994 hlen = len(self.hunk)
995 for x in xrange(hlen - 1):
995 for x in xrange(hlen - 1):
996 # the hunk starts with the @@ line, so use x+1
996 # the hunk starts with the @@ line, so use x+1
997 if self.hunk[x + 1][0] == ' ':
997 if self.hunk[x + 1][0] == ' ':
998 top += 1
998 top += 1
999 else:
999 else:
1000 break
1000 break
1001 if not toponly:
1001 if not toponly:
1002 for x in xrange(hlen - 1):
1002 for x in xrange(hlen - 1):
1003 if self.hunk[hlen - bot - 1][0] == ' ':
1003 if self.hunk[hlen - bot - 1][0] == ' ':
1004 bot += 1
1004 bot += 1
1005 else:
1005 else:
1006 break
1006 break
1007
1007
1008 bot = min(fuzz, bot)
1008 bot = min(fuzz, bot)
1009 top = min(fuzz, top)
1009 top = min(fuzz, top)
1010 return old[top:len(old)-bot], new[top:len(new)-bot], top
1010 return old[top:len(old)-bot], new[top:len(new)-bot], top
1011 return old, new, 0
1011 return old, new, 0
1012
1012
1013 def fuzzit(self, fuzz, toponly):
1013 def fuzzit(self, fuzz, toponly):
1014 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1014 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1015 oldstart = self.starta + top
1015 oldstart = self.starta + top
1016 newstart = self.startb + top
1016 newstart = self.startb + top
1017 # zero length hunk ranges already have their start decremented
1017 # zero length hunk ranges already have their start decremented
1018 if self.lena and oldstart > 0:
1018 if self.lena and oldstart > 0:
1019 oldstart -= 1
1019 oldstart -= 1
1020 if self.lenb and newstart > 0:
1020 if self.lenb and newstart > 0:
1021 newstart -= 1
1021 newstart -= 1
1022 return old, oldstart, new, newstart
1022 return old, oldstart, new, newstart
1023
1023
1024 class binhunk(object):
1024 class binhunk(object):
1025 'A binary patch file. Only understands literals so far.'
1025 'A binary patch file. Only understands literals so far.'
1026 def __init__(self, lr, fname):
1026 def __init__(self, lr, fname):
1027 self.text = None
1027 self.text = None
1028 self.hunk = ['GIT binary patch\n']
1028 self.hunk = ['GIT binary patch\n']
1029 self._fname = fname
1029 self._fname = fname
1030 self._read(lr)
1030 self._read(lr)
1031
1031
1032 def complete(self):
1032 def complete(self):
1033 return self.text is not None
1033 return self.text is not None
1034
1034
1035 def new(self):
1035 def new(self):
1036 return [self.text]
1036 return [self.text]
1037
1037
1038 def _read(self, lr):
1038 def _read(self, lr):
1039 def getline(lr, hunk):
1039 def getline(lr, hunk):
1040 l = lr.readline()
1040 l = lr.readline()
1041 hunk.append(l)
1041 hunk.append(l)
1042 return l.rstrip('\r\n')
1042 return l.rstrip('\r\n')
1043
1043
1044 while True:
1044 while True:
1045 line = getline(lr, self.hunk)
1045 line = getline(lr, self.hunk)
1046 if not line:
1046 if not line:
1047 raise PatchError(_('could not extract "%s" binary data')
1047 raise PatchError(_('could not extract "%s" binary data')
1048 % self._fname)
1048 % self._fname)
1049 if line.startswith('literal '):
1049 if line.startswith('literal '):
1050 break
1050 break
1051 size = int(line[8:].rstrip())
1051 size = int(line[8:].rstrip())
1052 dec = []
1052 dec = []
1053 line = getline(lr, self.hunk)
1053 line = getline(lr, self.hunk)
1054 while len(line) > 1:
1054 while len(line) > 1:
1055 l = line[0]
1055 l = line[0]
1056 if l <= 'Z' and l >= 'A':
1056 if l <= 'Z' and l >= 'A':
1057 l = ord(l) - ord('A') + 1
1057 l = ord(l) - ord('A') + 1
1058 else:
1058 else:
1059 l = ord(l) - ord('a') + 27
1059 l = ord(l) - ord('a') + 27
1060 try:
1060 try:
1061 dec.append(base85.b85decode(line[1:])[:l])
1061 dec.append(base85.b85decode(line[1:])[:l])
1062 except ValueError, e:
1062 except ValueError, e:
1063 raise PatchError(_('could not decode "%s" binary patch: %s')
1063 raise PatchError(_('could not decode "%s" binary patch: %s')
1064 % (self._fname, str(e)))
1064 % (self._fname, str(e)))
1065 line = getline(lr, self.hunk)
1065 line = getline(lr, self.hunk)
1066 text = zlib.decompress(''.join(dec))
1066 text = zlib.decompress(''.join(dec))
1067 if len(text) != size:
1067 if len(text) != size:
1068 raise PatchError(_('"%s" length is %d bytes, should be %d')
1068 raise PatchError(_('"%s" length is %d bytes, should be %d')
1069 % (self._fname, len(text), size))
1069 % (self._fname, len(text), size))
1070 self.text = text
1070 self.text = text
1071
1071
1072 def parsefilename(str):
1072 def parsefilename(str):
1073 # --- filename \t|space stuff
1073 # --- filename \t|space stuff
1074 s = str[4:].rstrip('\r\n')
1074 s = str[4:].rstrip('\r\n')
1075 i = s.find('\t')
1075 i = s.find('\t')
1076 if i < 0:
1076 if i < 0:
1077 i = s.find(' ')
1077 i = s.find(' ')
1078 if i < 0:
1078 if i < 0:
1079 return s
1079 return s
1080 return s[:i]
1080 return s[:i]
1081
1081
1082 def pathstrip(path, strip):
1082 def pathstrip(path, strip):
1083 pathlen = len(path)
1083 pathlen = len(path)
1084 i = 0
1084 i = 0
1085 if strip == 0:
1085 if strip == 0:
1086 return '', path.rstrip()
1086 return '', path.rstrip()
1087 count = strip
1087 count = strip
1088 while count > 0:
1088 while count > 0:
1089 i = path.find('/', i)
1089 i = path.find('/', i)
1090 if i == -1:
1090 if i == -1:
1091 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1091 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1092 (count, strip, path))
1092 (count, strip, path))
1093 i += 1
1093 i += 1
1094 # consume '//' in the path
1094 # consume '//' in the path
1095 while i < pathlen - 1 and path[i] == '/':
1095 while i < pathlen - 1 and path[i] == '/':
1096 i += 1
1096 i += 1
1097 count -= 1
1097 count -= 1
1098 return path[:i].lstrip(), path[i:].rstrip()
1098 return path[:i].lstrip(), path[i:].rstrip()
1099
1099
1100 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip):
1100 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip):
1101 nulla = afile_orig == "/dev/null"
1101 nulla = afile_orig == "/dev/null"
1102 nullb = bfile_orig == "/dev/null"
1102 nullb = bfile_orig == "/dev/null"
1103 create = nulla and hunk.starta == 0 and hunk.lena == 0
1103 create = nulla and hunk.starta == 0 and hunk.lena == 0
1104 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1104 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1105 abase, afile = pathstrip(afile_orig, strip)
1105 abase, afile = pathstrip(afile_orig, strip)
1106 gooda = not nulla and backend.exists(afile)
1106 gooda = not nulla and backend.exists(afile)
1107 bbase, bfile = pathstrip(bfile_orig, strip)
1107 bbase, bfile = pathstrip(bfile_orig, strip)
1108 if afile == bfile:
1108 if afile == bfile:
1109 goodb = gooda
1109 goodb = gooda
1110 else:
1110 else:
1111 goodb = not nullb and backend.exists(bfile)
1111 goodb = not nullb and backend.exists(bfile)
1112 missing = not goodb and not gooda and not create
1112 missing = not goodb and not gooda and not create
1113
1113
1114 # some diff programs apparently produce patches where the afile is
1114 # some diff programs apparently produce patches where the afile is
1115 # not /dev/null, but afile starts with bfile
1115 # not /dev/null, but afile starts with bfile
1116 abasedir = afile[:afile.rfind('/') + 1]
1116 abasedir = afile[:afile.rfind('/') + 1]
1117 bbasedir = bfile[:bfile.rfind('/') + 1]
1117 bbasedir = bfile[:bfile.rfind('/') + 1]
1118 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1118 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1119 and hunk.starta == 0 and hunk.lena == 0):
1119 and hunk.starta == 0 and hunk.lena == 0):
1120 create = True
1120 create = True
1121 missing = False
1121 missing = False
1122
1122
1123 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1123 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1124 # diff is between a file and its backup. In this case, the original
1124 # diff is between a file and its backup. In this case, the original
1125 # file should be patched (see original mpatch code).
1125 # file should be patched (see original mpatch code).
1126 isbackup = (abase == bbase and bfile.startswith(afile))
1126 isbackup = (abase == bbase and bfile.startswith(afile))
1127 fname = None
1127 fname = None
1128 if not missing:
1128 if not missing:
1129 if gooda and goodb:
1129 if gooda and goodb:
1130 fname = isbackup and afile or bfile
1130 fname = isbackup and afile or bfile
1131 elif gooda:
1131 elif gooda:
1132 fname = afile
1132 fname = afile
1133
1133
1134 if not fname:
1134 if not fname:
1135 if not nullb:
1135 if not nullb:
1136 fname = isbackup and afile or bfile
1136 fname = isbackup and afile or bfile
1137 elif not nulla:
1137 elif not nulla:
1138 fname = afile
1138 fname = afile
1139 else:
1139 else:
1140 raise PatchError(_("undefined source and destination files"))
1140 raise PatchError(_("undefined source and destination files"))
1141
1141
1142 gp = patchmeta(fname)
1142 gp = patchmeta(fname)
1143 if create:
1143 if create:
1144 gp.op = 'ADD'
1144 gp.op = 'ADD'
1145 elif remove:
1145 elif remove:
1146 gp.op = 'DELETE'
1146 gp.op = 'DELETE'
1147 return gp
1147 return gp
1148
1148
1149 def scangitpatch(lr, firstline):
1149 def scangitpatch(lr, firstline):
1150 """
1150 """
1151 Git patches can emit:
1151 Git patches can emit:
1152 - rename a to b
1152 - rename a to b
1153 - change b
1153 - change b
1154 - copy a to c
1154 - copy a to c
1155 - change c
1155 - change c
1156
1156
1157 We cannot apply this sequence as-is, the renamed 'a' could not be
1157 We cannot apply this sequence as-is, the renamed 'a' could not be
1158 found for it would have been renamed already. And we cannot copy
1158 found for it would have been renamed already. And we cannot copy
1159 from 'b' instead because 'b' would have been changed already. So
1159 from 'b' instead because 'b' would have been changed already. So
1160 we scan the git patch for copy and rename commands so we can
1160 we scan the git patch for copy and rename commands so we can
1161 perform the copies ahead of time.
1161 perform the copies ahead of time.
1162 """
1162 """
1163 pos = 0
1163 pos = 0
1164 try:
1164 try:
1165 pos = lr.fp.tell()
1165 pos = lr.fp.tell()
1166 fp = lr.fp
1166 fp = lr.fp
1167 except IOError:
1167 except IOError:
1168 fp = cStringIO.StringIO(lr.fp.read())
1168 fp = cStringIO.StringIO(lr.fp.read())
1169 gitlr = linereader(fp)
1169 gitlr = linereader(fp)
1170 gitlr.push(firstline)
1170 gitlr.push(firstline)
1171 gitpatches = readgitpatch(gitlr)
1171 gitpatches = readgitpatch(gitlr)
1172 fp.seek(pos)
1172 fp.seek(pos)
1173 return gitpatches
1173 return gitpatches
1174
1174
1175 def iterhunks(fp):
1175 def iterhunks(fp):
1176 """Read a patch and yield the following events:
1176 """Read a patch and yield the following events:
1177 - ("file", afile, bfile, firsthunk): select a new target file.
1177 - ("file", afile, bfile, firsthunk): select a new target file.
1178 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1178 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1179 "file" event.
1179 "file" event.
1180 - ("git", gitchanges): current diff is in git format, gitchanges
1180 - ("git", gitchanges): current diff is in git format, gitchanges
1181 maps filenames to gitpatch records. Unique event.
1181 maps filenames to gitpatch records. Unique event.
1182 """
1182 """
1183 afile = ""
1183 afile = ""
1184 bfile = ""
1184 bfile = ""
1185 state = None
1185 state = None
1186 hunknum = 0
1186 hunknum = 0
1187 emitfile = newfile = False
1187 emitfile = newfile = False
1188 gitpatches = None
1188 gitpatches = None
1189
1189
1190 # our states
1190 # our states
1191 BFILE = 1
1191 BFILE = 1
1192 context = None
1192 context = None
1193 lr = linereader(fp)
1193 lr = linereader(fp)
1194
1194
1195 while True:
1195 while True:
1196 x = lr.readline()
1196 x = lr.readline()
1197 if not x:
1197 if not x:
1198 break
1198 break
1199 if state == BFILE and (
1199 if state == BFILE and (
1200 (not context and x[0] == '@')
1200 (not context and x[0] == '@')
1201 or (context is not False and x.startswith('***************'))
1201 or (context is not False and x.startswith('***************'))
1202 or x.startswith('GIT binary patch')):
1202 or x.startswith('GIT binary patch')):
1203 gp = None
1203 gp = None
1204 if (gitpatches and
1204 if (gitpatches and
1205 gitpatches[-1].ispatching(afile, bfile)):
1205 gitpatches[-1].ispatching(afile, bfile)):
1206 gp = gitpatches.pop()
1206 gp = gitpatches.pop()
1207 if x.startswith('GIT binary patch'):
1207 if x.startswith('GIT binary patch'):
1208 h = binhunk(lr, gp.path)
1208 h = binhunk(lr, gp.path)
1209 else:
1209 else:
1210 if context is None and x.startswith('***************'):
1210 if context is None and x.startswith('***************'):
1211 context = True
1211 context = True
1212 h = hunk(x, hunknum + 1, lr, context)
1212 h = hunk(x, hunknum + 1, lr, context)
1213 hunknum += 1
1213 hunknum += 1
1214 if emitfile:
1214 if emitfile:
1215 emitfile = False
1215 emitfile = False
1216 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1216 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1217 yield 'hunk', h
1217 yield 'hunk', h
1218 elif x.startswith('diff --git'):
1218 elif x.startswith('diff --git'):
1219 m = gitre.match(x.rstrip(' \r\n'))
1219 m = gitre.match(x.rstrip(' \r\n'))
1220 if not m:
1220 if not m:
1221 continue
1221 continue
1222 if gitpatches is None:
1222 if gitpatches is None:
1223 # scan whole input for git metadata
1223 # scan whole input for git metadata
1224 gitpatches = scangitpatch(lr, x)
1224 gitpatches = scangitpatch(lr, x)
1225 yield 'git', [g.copy() for g in gitpatches
1225 yield 'git', [g.copy() for g in gitpatches
1226 if g.op in ('COPY', 'RENAME')]
1226 if g.op in ('COPY', 'RENAME')]
1227 gitpatches.reverse()
1227 gitpatches.reverse()
1228 afile = 'a/' + m.group(1)
1228 afile = 'a/' + m.group(1)
1229 bfile = 'b/' + m.group(2)
1229 bfile = 'b/' + m.group(2)
1230 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1230 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1231 gp = gitpatches.pop()
1231 gp = gitpatches.pop()
1232 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1232 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1233 if not gitpatches:
1233 if not gitpatches:
1234 raise PatchError(_('failed to synchronize metadata for "%s"')
1234 raise PatchError(_('failed to synchronize metadata for "%s"')
1235 % afile[2:])
1235 % afile[2:])
1236 gp = gitpatches[-1]
1236 gp = gitpatches[-1]
1237 newfile = True
1237 newfile = True
1238 elif x.startswith('---'):
1238 elif x.startswith('---'):
1239 # check for a unified diff
1239 # check for a unified diff
1240 l2 = lr.readline()
1240 l2 = lr.readline()
1241 if not l2.startswith('+++'):
1241 if not l2.startswith('+++'):
1242 lr.push(l2)
1242 lr.push(l2)
1243 continue
1243 continue
1244 newfile = True
1244 newfile = True
1245 context = False
1245 context = False
1246 afile = parsefilename(x)
1246 afile = parsefilename(x)
1247 bfile = parsefilename(l2)
1247 bfile = parsefilename(l2)
1248 elif x.startswith('***'):
1248 elif x.startswith('***'):
1249 # check for a context diff
1249 # check for a context diff
1250 l2 = lr.readline()
1250 l2 = lr.readline()
1251 if not l2.startswith('---'):
1251 if not l2.startswith('---'):
1252 lr.push(l2)
1252 lr.push(l2)
1253 continue
1253 continue
1254 l3 = lr.readline()
1254 l3 = lr.readline()
1255 lr.push(l3)
1255 lr.push(l3)
1256 if not l3.startswith("***************"):
1256 if not l3.startswith("***************"):
1257 lr.push(l2)
1257 lr.push(l2)
1258 continue
1258 continue
1259 newfile = True
1259 newfile = True
1260 context = True
1260 context = True
1261 afile = parsefilename(x)
1261 afile = parsefilename(x)
1262 bfile = parsefilename(l2)
1262 bfile = parsefilename(l2)
1263
1263
1264 if newfile:
1264 if newfile:
1265 newfile = False
1265 newfile = False
1266 emitfile = True
1266 emitfile = True
1267 state = BFILE
1267 state = BFILE
1268 hunknum = 0
1268 hunknum = 0
1269
1269
1270 while gitpatches:
1270 while gitpatches:
1271 gp = gitpatches.pop()
1271 gp = gitpatches.pop()
1272 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1272 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1273
1273
1274 def applydiff(ui, fp, backend, store, strip=1, eolmode='strict'):
1274 def applydiff(ui, fp, backend, store, strip=1, eolmode='strict'):
1275 """Reads a patch from fp and tries to apply it.
1275 """Reads a patch from fp and tries to apply it.
1276
1276
1277 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1277 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1278 there was any fuzz.
1278 there was any fuzz.
1279
1279
1280 If 'eolmode' is 'strict', the patch content and patched file are
1280 If 'eolmode' is 'strict', the patch content and patched file are
1281 read in binary mode. Otherwise, line endings are ignored when
1281 read in binary mode. Otherwise, line endings are ignored when
1282 patching then normalized according to 'eolmode'.
1282 patching then normalized according to 'eolmode'.
1283 """
1283 """
1284 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1284 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1285 eolmode=eolmode)
1285 eolmode=eolmode)
1286
1286
1287 def _applydiff(ui, fp, patcher, backend, store, strip=1,
1287 def _applydiff(ui, fp, patcher, backend, store, strip=1,
1288 eolmode='strict'):
1288 eolmode='strict'):
1289
1289
1290 def pstrip(p):
1290 def pstrip(p):
1291 return pathstrip(p, strip - 1)[1]
1291 return pathstrip(p, strip - 1)[1]
1292
1292
1293 rejects = 0
1293 rejects = 0
1294 err = 0
1294 err = 0
1295 current_file = None
1295 current_file = None
1296
1296
1297 for state, values in iterhunks(fp):
1297 for state, values in iterhunks(fp):
1298 if state == 'hunk':
1298 if state == 'hunk':
1299 if not current_file:
1299 if not current_file:
1300 continue
1300 continue
1301 ret = current_file.apply(values)
1301 ret = current_file.apply(values)
1302 if ret > 0:
1302 if ret > 0:
1303 err = 1
1303 err = 1
1304 elif state == 'file':
1304 elif state == 'file':
1305 if current_file:
1305 if current_file:
1306 rejects += current_file.close()
1306 rejects += current_file.close()
1307 current_file = None
1307 current_file = None
1308 afile, bfile, first_hunk, gp = values
1308 afile, bfile, first_hunk, gp = values
1309 if gp:
1309 if gp:
1310 gp.path = pstrip(gp.path)
1310 gp.path = pstrip(gp.path)
1311 if gp.oldpath:
1311 if gp.oldpath:
1312 gp.oldpath = pstrip(gp.oldpath)
1312 gp.oldpath = pstrip(gp.oldpath)
1313 else:
1313 else:
1314 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
1314 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
1315 if gp.op == 'RENAME':
1315 if gp.op == 'RENAME':
1316 backend.unlink(gp.oldpath)
1316 backend.unlink(gp.oldpath)
1317 if not first_hunk:
1317 if not first_hunk:
1318 if gp.op == 'DELETE':
1318 if gp.op == 'DELETE':
1319 backend.unlink(gp.path)
1319 backend.unlink(gp.path)
1320 continue
1320 continue
1321 data, mode = None, None
1321 data, mode = None, None
1322 if gp.op in ('RENAME', 'COPY'):
1322 if gp.op in ('RENAME', 'COPY'):
1323 data, mode = store.getfile(gp.oldpath)[:2]
1323 data, mode = store.getfile(gp.oldpath)[:2]
1324 if gp.mode:
1324 if gp.mode:
1325 mode = gp.mode
1325 mode = gp.mode
1326 if gp.op == 'ADD':
1326 if gp.op == 'ADD':
1327 # Added files without content have no hunk and
1327 # Added files without content have no hunk and
1328 # must be created
1328 # must be created
1329 data = ''
1329 data = ''
1330 if data or mode:
1330 if data or mode:
1331 if (gp.op in ('ADD', 'RENAME', 'COPY')
1331 if (gp.op in ('ADD', 'RENAME', 'COPY')
1332 and backend.exists(gp.path)):
1332 and backend.exists(gp.path)):
1333 raise PatchError(_("cannot create %s: destination "
1333 raise PatchError(_("cannot create %s: destination "
1334 "already exists") % gp.path)
1334 "already exists") % gp.path)
1335 backend.setfile(gp.path, data, mode, gp.oldpath)
1335 backend.setfile(gp.path, data, mode, gp.oldpath)
1336 continue
1336 continue
1337 try:
1337 try:
1338 current_file = patcher(ui, gp, backend, store,
1338 current_file = patcher(ui, gp, backend, store,
1339 eolmode=eolmode)
1339 eolmode=eolmode)
1340 except PatchError, inst:
1340 except PatchError, inst:
1341 ui.warn(str(inst) + '\n')
1341 ui.warn(str(inst) + '\n')
1342 current_file = None
1342 current_file = None
1343 rejects += 1
1343 rejects += 1
1344 continue
1344 continue
1345 elif state == 'git':
1345 elif state == 'git':
1346 for gp in values:
1346 for gp in values:
1347 path = pstrip(gp.oldpath)
1347 path = pstrip(gp.oldpath)
1348 try:
1348 try:
1349 data, mode = backend.getfile(path)
1349 data, mode = backend.getfile(path)
1350 except IOError, e:
1350 except IOError, e:
1351 if e.errno != errno.ENOENT:
1351 if e.errno != errno.ENOENT:
1352 raise
1352 raise
1353 # The error ignored here will trigger a getfile()
1353 # The error ignored here will trigger a getfile()
1354 # error in a place more appropriate for error
1354 # error in a place more appropriate for error
1355 # handling, and will not interrupt the patching
1355 # handling, and will not interrupt the patching
1356 # process.
1356 # process.
1357 else:
1357 else:
1358 store.setfile(path, data, mode)
1358 store.setfile(path, data, mode)
1359 else:
1359 else:
1360 raise util.Abort(_('unsupported parser state: %s') % state)
1360 raise util.Abort(_('unsupported parser state: %s') % state)
1361
1361
1362 if current_file:
1362 if current_file:
1363 rejects += current_file.close()
1363 rejects += current_file.close()
1364
1364
1365 if rejects:
1365 if rejects:
1366 return -1
1366 return -1
1367 return err
1367 return err
1368
1368
1369 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1369 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1370 similarity):
1370 similarity):
1371 """use <patcher> to apply <patchname> to the working directory.
1371 """use <patcher> to apply <patchname> to the working directory.
1372 returns whether patch was applied with fuzz factor."""
1372 returns whether patch was applied with fuzz factor."""
1373
1373
1374 fuzz = False
1374 fuzz = False
1375 args = []
1375 args = []
1376 cwd = repo.root
1376 cwd = repo.root
1377 if cwd:
1377 if cwd:
1378 args.append('-d %s' % util.shellquote(cwd))
1378 args.append('-d %s' % util.shellquote(cwd))
1379 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1379 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1380 util.shellquote(patchname)))
1380 util.shellquote(patchname)))
1381 try:
1381 try:
1382 for line in fp:
1382 for line in fp:
1383 line = line.rstrip()
1383 line = line.rstrip()
1384 ui.note(line + '\n')
1384 ui.note(line + '\n')
1385 if line.startswith('patching file '):
1385 if line.startswith('patching file '):
1386 pf = util.parsepatchoutput(line)
1386 pf = util.parsepatchoutput(line)
1387 printed_file = False
1387 printed_file = False
1388 files.add(pf)
1388 files.add(pf)
1389 elif line.find('with fuzz') >= 0:
1389 elif line.find('with fuzz') >= 0:
1390 fuzz = True
1390 fuzz = True
1391 if not printed_file:
1391 if not printed_file:
1392 ui.warn(pf + '\n')
1392 ui.warn(pf + '\n')
1393 printed_file = True
1393 printed_file = True
1394 ui.warn(line + '\n')
1394 ui.warn(line + '\n')
1395 elif line.find('saving rejects to file') >= 0:
1395 elif line.find('saving rejects to file') >= 0:
1396 ui.warn(line + '\n')
1396 ui.warn(line + '\n')
1397 elif line.find('FAILED') >= 0:
1397 elif line.find('FAILED') >= 0:
1398 if not printed_file:
1398 if not printed_file:
1399 ui.warn(pf + '\n')
1399 ui.warn(pf + '\n')
1400 printed_file = True
1400 printed_file = True
1401 ui.warn(line + '\n')
1401 ui.warn(line + '\n')
1402 finally:
1402 finally:
1403 if files:
1403 if files:
1404 cfiles = list(files)
1404 cfiles = list(files)
1405 cwd = repo.getcwd()
1405 cwd = repo.getcwd()
1406 if cwd:
1406 if cwd:
1407 cfiles = [util.pathto(repo.root, cwd, f)
1407 cfiles = [util.pathto(repo.root, cwd, f)
1408 for f in cfiles]
1408 for f in cfiles]
1409 scmutil.addremove(repo, cfiles, similarity=similarity)
1409 scmutil.addremove(repo, cfiles, similarity=similarity)
1410 code = fp.close()
1410 code = fp.close()
1411 if code:
1411 if code:
1412 raise PatchError(_("patch command failed: %s") %
1412 raise PatchError(_("patch command failed: %s") %
1413 util.explainexit(code)[0])
1413 util.explainexit(code)[0])
1414 return fuzz
1414 return fuzz
1415
1415
1416 def patchbackend(ui, backend, patchobj, strip, files=None, eolmode='strict'):
1416 def patchbackend(ui, backend, patchobj, strip, files=None, eolmode='strict'):
1417 if files is None:
1417 if files is None:
1418 files = set()
1418 files = set()
1419 if eolmode is None:
1419 if eolmode is None:
1420 eolmode = ui.config('patch', 'eol', 'strict')
1420 eolmode = ui.config('patch', 'eol', 'strict')
1421 if eolmode.lower() not in eolmodes:
1421 if eolmode.lower() not in eolmodes:
1422 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1422 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1423 eolmode = eolmode.lower()
1423 eolmode = eolmode.lower()
1424
1424
1425 store = filestore()
1425 store = filestore()
1426 try:
1426 try:
1427 fp = open(patchobj, 'rb')
1427 fp = open(patchobj, 'rb')
1428 except TypeError:
1428 except TypeError:
1429 fp = patchobj
1429 fp = patchobj
1430 try:
1430 try:
1431 ret = applydiff(ui, fp, backend, store, strip=strip,
1431 ret = applydiff(ui, fp, backend, store, strip=strip,
1432 eolmode=eolmode)
1432 eolmode=eolmode)
1433 finally:
1433 finally:
1434 if fp != patchobj:
1434 if fp != patchobj:
1435 fp.close()
1435 fp.close()
1436 files.update(backend.close())
1436 files.update(backend.close())
1437 store.close()
1437 store.close()
1438 if ret < 0:
1438 if ret < 0:
1439 raise PatchError(_('patch failed to apply'))
1439 raise PatchError(_('patch failed to apply'))
1440 return ret > 0
1440 return ret > 0
1441
1441
1442 def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
1442 def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
1443 similarity=0):
1443 similarity=0):
1444 """use builtin patch to apply <patchobj> to the working directory.
1444 """use builtin patch to apply <patchobj> to the working directory.
1445 returns whether patch was applied with fuzz factor."""
1445 returns whether patch was applied with fuzz factor."""
1446 backend = workingbackend(ui, repo, similarity)
1446 backend = workingbackend(ui, repo, similarity)
1447 return patchbackend(ui, backend, patchobj, strip, files, eolmode)
1447 return patchbackend(ui, backend, patchobj, strip, files, eolmode)
1448
1448
1449 def patchrepo(ui, repo, ctx, store, patchobj, strip, files=None,
1449 def patchrepo(ui, repo, ctx, store, patchobj, strip, files=None,
1450 eolmode='strict'):
1450 eolmode='strict'):
1451 backend = repobackend(ui, repo, ctx, store)
1451 backend = repobackend(ui, repo, ctx, store)
1452 return patchbackend(ui, backend, patchobj, strip, files, eolmode)
1452 return patchbackend(ui, backend, patchobj, strip, files, eolmode)
1453
1453
1454 def makememctx(repo, parents, text, user, date, branch, files, store,
1454 def makememctx(repo, parents, text, user, date, branch, files, store,
1455 editor=None):
1455 editor=None):
1456 def getfilectx(repo, memctx, path):
1456 def getfilectx(repo, memctx, path):
1457 data, (islink, isexec), copied = store.getfile(path)
1457 data, (islink, isexec), copied = store.getfile(path)
1458 return context.memfilectx(path, data, islink=islink, isexec=isexec,
1458 return context.memfilectx(path, data, islink=islink, isexec=isexec,
1459 copied=copied)
1459 copied=copied)
1460 extra = {}
1460 extra = {}
1461 if branch:
1461 if branch:
1462 extra['branch'] = encoding.fromlocal(branch)
1462 extra['branch'] = encoding.fromlocal(branch)
1463 ctx = context.memctx(repo, parents, text, files, getfilectx, user,
1463 ctx = context.memctx(repo, parents, text, files, getfilectx, user,
1464 date, extra)
1464 date, extra)
1465 if editor:
1465 if editor:
1466 ctx._text = editor(repo, ctx, [])
1466 ctx._text = editor(repo, ctx, [])
1467 return ctx
1467 return ctx
1468
1468
1469 def patch(ui, repo, patchname, strip=1, files=None, eolmode='strict',
1469 def patch(ui, repo, patchname, strip=1, files=None, eolmode='strict',
1470 similarity=0):
1470 similarity=0):
1471 """Apply <patchname> to the working directory.
1471 """Apply <patchname> to the working directory.
1472
1472
1473 'eolmode' specifies how end of lines should be handled. It can be:
1473 'eolmode' specifies how end of lines should be handled. It can be:
1474 - 'strict': inputs are read in binary mode, EOLs are preserved
1474 - 'strict': inputs are read in binary mode, EOLs are preserved
1475 - 'crlf': EOLs are ignored when patching and reset to CRLF
1475 - 'crlf': EOLs are ignored when patching and reset to CRLF
1476 - 'lf': EOLs are ignored when patching and reset to LF
1476 - 'lf': EOLs are ignored when patching and reset to LF
1477 - None: get it from user settings, default to 'strict'
1477 - None: get it from user settings, default to 'strict'
1478 'eolmode' is ignored when using an external patcher program.
1478 'eolmode' is ignored when using an external patcher program.
1479
1479
1480 Returns whether patch was applied with fuzz factor.
1480 Returns whether patch was applied with fuzz factor.
1481 """
1481 """
1482 patcher = ui.config('ui', 'patch')
1482 patcher = ui.config('ui', 'patch')
1483 if files is None:
1483 if files is None:
1484 files = set()
1484 files = set()
1485 try:
1485 try:
1486 if patcher:
1486 if patcher:
1487 return _externalpatch(ui, repo, patcher, patchname, strip,
1487 return _externalpatch(ui, repo, patcher, patchname, strip,
1488 files, similarity)
1488 files, similarity)
1489 return internalpatch(ui, repo, patchname, strip, files, eolmode,
1489 return internalpatch(ui, repo, patchname, strip, files, eolmode,
1490 similarity)
1490 similarity)
1491 except PatchError, err:
1491 except PatchError, err:
1492 raise util.Abort(str(err))
1492 raise util.Abort(str(err))
1493
1493
1494 def changedfiles(ui, repo, patchpath, strip=1):
1494 def changedfiles(ui, repo, patchpath, strip=1):
1495 backend = fsbackend(ui, repo.root)
1495 backend = fsbackend(ui, repo.root)
1496 fp = open(patchpath, 'rb')
1496 fp = open(patchpath, 'rb')
1497 try:
1497 try:
1498 changed = set()
1498 changed = set()
1499 for state, values in iterhunks(fp):
1499 for state, values in iterhunks(fp):
1500 if state == 'file':
1500 if state == 'file':
1501 afile, bfile, first_hunk, gp = values
1501 afile, bfile, first_hunk, gp = values
1502 if gp:
1502 if gp:
1503 gp.path = pathstrip(gp.path, strip - 1)[1]
1503 gp.path = pathstrip(gp.path, strip - 1)[1]
1504 if gp.oldpath:
1504 if gp.oldpath:
1505 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1505 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1506 else:
1506 else:
1507 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
1507 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
1508 changed.add(gp.path)
1508 changed.add(gp.path)
1509 if gp.op == 'RENAME':
1509 if gp.op == 'RENAME':
1510 changed.add(gp.oldpath)
1510 changed.add(gp.oldpath)
1511 elif state not in ('hunk', 'git'):
1511 elif state not in ('hunk', 'git'):
1512 raise util.Abort(_('unsupported parser state: %s') % state)
1512 raise util.Abort(_('unsupported parser state: %s') % state)
1513 return changed
1513 return changed
1514 finally:
1514 finally:
1515 fp.close()
1515 fp.close()
1516
1516
1517 class GitDiffRequired(Exception):
1517 class GitDiffRequired(Exception):
1518 pass
1518 pass
1519
1519
1520 def diffopts(ui, opts=None, untrusted=False, section='diff'):
1520 def diffopts(ui, opts=None, untrusted=False, section='diff'):
1521 def get(key, name=None, getter=ui.configbool):
1521 def get(key, name=None, getter=ui.configbool):
1522 return ((opts and opts.get(key)) or
1522 return ((opts and opts.get(key)) or
1523 getter(section, name or key, None, untrusted=untrusted))
1523 getter(section, name or key, None, untrusted=untrusted))
1524 return mdiff.diffopts(
1524 return mdiff.diffopts(
1525 text=opts and opts.get('text'),
1525 text=opts and opts.get('text'),
1526 git=get('git'),
1526 git=get('git'),
1527 nodates=get('nodates'),
1527 nodates=get('nodates'),
1528 showfunc=get('show_function', 'showfunc'),
1528 showfunc=get('show_function', 'showfunc'),
1529 ignorews=get('ignore_all_space', 'ignorews'),
1529 ignorews=get('ignore_all_space', 'ignorews'),
1530 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1530 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1531 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1531 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1532 context=get('unified', getter=ui.config))
1532 context=get('unified', getter=ui.config))
1533
1533
1534 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1534 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1535 losedatafn=None, prefix=''):
1535 losedatafn=None, prefix=''):
1536 '''yields diff of changes to files between two nodes, or node and
1536 '''yields diff of changes to files between two nodes, or node and
1537 working directory.
1537 working directory.
1538
1538
1539 if node1 is None, use first dirstate parent instead.
1539 if node1 is None, use first dirstate parent instead.
1540 if node2 is None, compare node1 with working directory.
1540 if node2 is None, compare node1 with working directory.
1541
1541
1542 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1542 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1543 every time some change cannot be represented with the current
1543 every time some change cannot be represented with the current
1544 patch format. Return False to upgrade to git patch format, True to
1544 patch format. Return False to upgrade to git patch format, True to
1545 accept the loss or raise an exception to abort the diff. It is
1545 accept the loss or raise an exception to abort the diff. It is
1546 called with the name of current file being diffed as 'fn'. If set
1546 called with the name of current file being diffed as 'fn'. If set
1547 to None, patches will always be upgraded to git format when
1547 to None, patches will always be upgraded to git format when
1548 necessary.
1548 necessary.
1549
1549
1550 prefix is a filename prefix that is prepended to all filenames on
1550 prefix is a filename prefix that is prepended to all filenames on
1551 display (used for subrepos).
1551 display (used for subrepos).
1552 '''
1552 '''
1553
1553
1554 if opts is None:
1554 if opts is None:
1555 opts = mdiff.defaultopts
1555 opts = mdiff.defaultopts
1556
1556
1557 if not node1 and not node2:
1557 if not node1 and not node2:
1558 node1 = repo.dirstate.p1()
1558 node1 = repo.dirstate.p1()
1559
1559
1560 def lrugetfilectx():
1560 def lrugetfilectx():
1561 cache = {}
1561 cache = {}
1562 order = util.deque()
1562 order = util.deque()
1563 def getfilectx(f, ctx):
1563 def getfilectx(f, ctx):
1564 fctx = ctx.filectx(f, filelog=cache.get(f))
1564 fctx = ctx.filectx(f, filelog=cache.get(f))
1565 if f not in cache:
1565 if f not in cache:
1566 if len(cache) > 20:
1566 if len(cache) > 20:
1567 del cache[order.popleft()]
1567 del cache[order.popleft()]
1568 cache[f] = fctx.filelog()
1568 cache[f] = fctx.filelog()
1569 else:
1569 else:
1570 order.remove(f)
1570 order.remove(f)
1571 order.append(f)
1571 order.append(f)
1572 return fctx
1572 return fctx
1573 return getfilectx
1573 return getfilectx
1574 getfilectx = lrugetfilectx()
1574 getfilectx = lrugetfilectx()
1575
1575
1576 ctx1 = repo[node1]
1576 ctx1 = repo[node1]
1577 ctx2 = repo[node2]
1577 ctx2 = repo[node2]
1578
1578
1579 if not changes:
1579 if not changes:
1580 changes = repo.status(ctx1, ctx2, match=match)
1580 changes = repo.status(ctx1, ctx2, match=match)
1581 modified, added, removed = changes[:3]
1581 modified, added, removed = changes[:3]
1582
1582
1583 if not modified and not added and not removed:
1583 if not modified and not added and not removed:
1584 return []
1584 return []
1585
1585
1586 revs = None
1586 revs = None
1587 hexfunc = repo.ui.debugflag and hex or short
1587 hexfunc = repo.ui.debugflag and hex or short
1588 revs = [hexfunc(node) for node in [node1, node2] if node]
1588 revs = [hexfunc(node) for node in [node1, node2] if node]
1589
1589
1590 copy = {}
1590 copy = {}
1591 if opts.git or opts.upgrade:
1591 if opts.git or opts.upgrade:
1592 copy = copies.pathcopies(ctx1, ctx2)
1592 copy = copies.pathcopies(ctx1, ctx2)
1593
1593
1594 def difffn(opts, losedata):
1594 def difffn(opts, losedata):
1595 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1595 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1596 copy, getfilectx, opts, losedata, prefix)
1596 copy, getfilectx, opts, losedata, prefix)
1597 if opts.upgrade and not opts.git:
1597 if opts.upgrade and not opts.git:
1598 try:
1598 try:
1599 def losedata(fn):
1599 def losedata(fn):
1600 if not losedatafn or not losedatafn(fn=fn):
1600 if not losedatafn or not losedatafn(fn=fn):
1601 raise GitDiffRequired
1601 raise GitDiffRequired
1602 # Buffer the whole output until we are sure it can be generated
1602 # Buffer the whole output until we are sure it can be generated
1603 return list(difffn(opts.copy(git=False), losedata))
1603 return list(difffn(opts.copy(git=False), losedata))
1604 except GitDiffRequired:
1604 except GitDiffRequired:
1605 return difffn(opts.copy(git=True), None)
1605 return difffn(opts.copy(git=True), None)
1606 else:
1606 else:
1607 return difffn(opts, None)
1607 return difffn(opts, None)
1608
1608
1609 def difflabel(func, *args, **kw):
1609 def difflabel(func, *args, **kw):
1610 '''yields 2-tuples of (output, label) based on the output of func()'''
1610 '''yields 2-tuples of (output, label) based on the output of func()'''
1611 headprefixes = [('diff', 'diff.diffline'),
1611 headprefixes = [('diff', 'diff.diffline'),
1612 ('copy', 'diff.extended'),
1612 ('copy', 'diff.extended'),
1613 ('rename', 'diff.extended'),
1613 ('rename', 'diff.extended'),
1614 ('old', 'diff.extended'),
1614 ('old', 'diff.extended'),
1615 ('new', 'diff.extended'),
1615 ('new', 'diff.extended'),
1616 ('deleted', 'diff.extended'),
1616 ('deleted', 'diff.extended'),
1617 ('---', 'diff.file_a'),
1617 ('---', 'diff.file_a'),
1618 ('+++', 'diff.file_b')]
1618 ('+++', 'diff.file_b')]
1619 textprefixes = [('@', 'diff.hunk'),
1619 textprefixes = [('@', 'diff.hunk'),
1620 ('-', 'diff.deleted'),
1620 ('-', 'diff.deleted'),
1621 ('+', 'diff.inserted')]
1621 ('+', 'diff.inserted')]
1622 head = False
1622 head = False
1623 for chunk in func(*args, **kw):
1623 for chunk in func(*args, **kw):
1624 lines = chunk.split('\n')
1624 lines = chunk.split('\n')
1625 for i, line in enumerate(lines):
1625 for i, line in enumerate(lines):
1626 if i != 0:
1626 if i != 0:
1627 yield ('\n', '')
1627 yield ('\n', '')
1628 if head:
1628 if head:
1629 if line.startswith('@'):
1629 if line.startswith('@'):
1630 head = False
1630 head = False
1631 else:
1631 else:
1632 if line and line[0] not in ' +-@\\':
1632 if line and line[0] not in ' +-@\\':
1633 head = True
1633 head = True
1634 stripline = line
1634 stripline = line
1635 if not head and line and line[0] in '+-':
1635 if not head and line and line[0] in '+-':
1636 # highlight trailing whitespace, but only in changed lines
1636 # highlight trailing whitespace, but only in changed lines
1637 stripline = line.rstrip()
1637 stripline = line.rstrip()
1638 prefixes = textprefixes
1638 prefixes = textprefixes
1639 if head:
1639 if head:
1640 prefixes = headprefixes
1640 prefixes = headprefixes
1641 for prefix, label in prefixes:
1641 for prefix, label in prefixes:
1642 if stripline.startswith(prefix):
1642 if stripline.startswith(prefix):
1643 yield (stripline, label)
1643 yield (stripline, label)
1644 break
1644 break
1645 else:
1645 else:
1646 yield (line, '')
1646 yield (line, '')
1647 if line != stripline:
1647 if line != stripline:
1648 yield (line[len(stripline):], 'diff.trailingwhitespace')
1648 yield (line[len(stripline):], 'diff.trailingwhitespace')
1649
1649
1650 def diffui(*args, **kw):
1650 def diffui(*args, **kw):
1651 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1651 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1652 return difflabel(diff, *args, **kw)
1652 return difflabel(diff, *args, **kw)
1653
1653
1654 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1654 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1655 copy, getfilectx, opts, losedatafn, prefix):
1655 copy, getfilectx, opts, losedatafn, prefix):
1656
1656
1657 def join(f):
1657 def join(f):
1658 return os.path.join(prefix, f)
1658 return posixpath.join(prefix, f)
1659
1659
1660 def addmodehdr(header, omode, nmode):
1660 def addmodehdr(header, omode, nmode):
1661 if omode != nmode:
1661 if omode != nmode:
1662 header.append('old mode %s\n' % omode)
1662 header.append('old mode %s\n' % omode)
1663 header.append('new mode %s\n' % nmode)
1663 header.append('new mode %s\n' % nmode)
1664
1664
1665 def addindexmeta(meta, revs):
1665 def addindexmeta(meta, revs):
1666 if opts.git:
1666 if opts.git:
1667 i = len(revs)
1667 i = len(revs)
1668 if i==2:
1668 if i==2:
1669 meta.append('index %s..%s\n' % tuple(revs))
1669 meta.append('index %s..%s\n' % tuple(revs))
1670 elif i==3:
1670 elif i==3:
1671 meta.append('index %s,%s..%s\n' % tuple(revs))
1671 meta.append('index %s,%s..%s\n' % tuple(revs))
1672
1672
1673 def gitindex(text):
1673 def gitindex(text):
1674 if not text:
1674 if not text:
1675 return hex(nullid)
1675 return hex(nullid)
1676 l = len(text)
1676 l = len(text)
1677 s = util.sha1('blob %d\0' % l)
1677 s = util.sha1('blob %d\0' % l)
1678 s.update(text)
1678 s.update(text)
1679 return s.hexdigest()
1679 return s.hexdigest()
1680
1680
1681 def diffline(a, b, revs):
1681 def diffline(a, b, revs):
1682 if opts.git:
1682 if opts.git:
1683 line = 'diff --git a/%s b/%s\n' % (a, b)
1683 line = 'diff --git a/%s b/%s\n' % (a, b)
1684 elif not repo.ui.quiet:
1684 elif not repo.ui.quiet:
1685 if revs:
1685 if revs:
1686 revinfo = ' '.join(["-r %s" % rev for rev in revs])
1686 revinfo = ' '.join(["-r %s" % rev for rev in revs])
1687 line = 'diff %s %s\n' % (revinfo, a)
1687 line = 'diff %s %s\n' % (revinfo, a)
1688 else:
1688 else:
1689 line = 'diff %s\n' % a
1689 line = 'diff %s\n' % a
1690 else:
1690 else:
1691 line = ''
1691 line = ''
1692 return line
1692 return line
1693
1693
1694 date1 = util.datestr(ctx1.date())
1694 date1 = util.datestr(ctx1.date())
1695 man1 = ctx1.manifest()
1695 man1 = ctx1.manifest()
1696
1696
1697 gone = set()
1697 gone = set()
1698 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1698 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1699
1699
1700 copyto = dict([(v, k) for k, v in copy.items()])
1700 copyto = dict([(v, k) for k, v in copy.items()])
1701
1701
1702 if opts.git:
1702 if opts.git:
1703 revs = None
1703 revs = None
1704
1704
1705 for f in sorted(modified + added + removed):
1705 for f in sorted(modified + added + removed):
1706 to = None
1706 to = None
1707 tn = None
1707 tn = None
1708 dodiff = True
1708 dodiff = True
1709 header = []
1709 header = []
1710 if f in man1:
1710 if f in man1:
1711 to = getfilectx(f, ctx1).data()
1711 to = getfilectx(f, ctx1).data()
1712 if f not in removed:
1712 if f not in removed:
1713 tn = getfilectx(f, ctx2).data()
1713 tn = getfilectx(f, ctx2).data()
1714 a, b = f, f
1714 a, b = f, f
1715 if opts.git or losedatafn:
1715 if opts.git or losedatafn:
1716 if f in added:
1716 if f in added:
1717 mode = gitmode[ctx2.flags(f)]
1717 mode = gitmode[ctx2.flags(f)]
1718 if f in copy or f in copyto:
1718 if f in copy or f in copyto:
1719 if opts.git:
1719 if opts.git:
1720 if f in copy:
1720 if f in copy:
1721 a = copy[f]
1721 a = copy[f]
1722 else:
1722 else:
1723 a = copyto[f]
1723 a = copyto[f]
1724 omode = gitmode[man1.flags(a)]
1724 omode = gitmode[man1.flags(a)]
1725 addmodehdr(header, omode, mode)
1725 addmodehdr(header, omode, mode)
1726 if a in removed and a not in gone:
1726 if a in removed and a not in gone:
1727 op = 'rename'
1727 op = 'rename'
1728 gone.add(a)
1728 gone.add(a)
1729 else:
1729 else:
1730 op = 'copy'
1730 op = 'copy'
1731 header.append('%s from %s\n' % (op, join(a)))
1731 header.append('%s from %s\n' % (op, join(a)))
1732 header.append('%s to %s\n' % (op, join(f)))
1732 header.append('%s to %s\n' % (op, join(f)))
1733 to = getfilectx(a, ctx1).data()
1733 to = getfilectx(a, ctx1).data()
1734 else:
1734 else:
1735 losedatafn(f)
1735 losedatafn(f)
1736 else:
1736 else:
1737 if opts.git:
1737 if opts.git:
1738 header.append('new file mode %s\n' % mode)
1738 header.append('new file mode %s\n' % mode)
1739 elif ctx2.flags(f):
1739 elif ctx2.flags(f):
1740 losedatafn(f)
1740 losedatafn(f)
1741 # In theory, if tn was copied or renamed we should check
1741 # In theory, if tn was copied or renamed we should check
1742 # if the source is binary too but the copy record already
1742 # if the source is binary too but the copy record already
1743 # forces git mode.
1743 # forces git mode.
1744 if util.binary(tn):
1744 if util.binary(tn):
1745 if opts.git:
1745 if opts.git:
1746 dodiff = 'binary'
1746 dodiff = 'binary'
1747 else:
1747 else:
1748 losedatafn(f)
1748 losedatafn(f)
1749 if not opts.git and not tn:
1749 if not opts.git and not tn:
1750 # regular diffs cannot represent new empty file
1750 # regular diffs cannot represent new empty file
1751 losedatafn(f)
1751 losedatafn(f)
1752 elif f in removed:
1752 elif f in removed:
1753 if opts.git:
1753 if opts.git:
1754 # have we already reported a copy above?
1754 # have we already reported a copy above?
1755 if ((f in copy and copy[f] in added
1755 if ((f in copy and copy[f] in added
1756 and copyto[copy[f]] == f) or
1756 and copyto[copy[f]] == f) or
1757 (f in copyto and copyto[f] in added
1757 (f in copyto and copyto[f] in added
1758 and copy[copyto[f]] == f)):
1758 and copy[copyto[f]] == f)):
1759 dodiff = False
1759 dodiff = False
1760 else:
1760 else:
1761 header.append('deleted file mode %s\n' %
1761 header.append('deleted file mode %s\n' %
1762 gitmode[man1.flags(f)])
1762 gitmode[man1.flags(f)])
1763 elif not to or util.binary(to):
1763 elif not to or util.binary(to):
1764 # regular diffs cannot represent empty file deletion
1764 # regular diffs cannot represent empty file deletion
1765 losedatafn(f)
1765 losedatafn(f)
1766 else:
1766 else:
1767 oflag = man1.flags(f)
1767 oflag = man1.flags(f)
1768 nflag = ctx2.flags(f)
1768 nflag = ctx2.flags(f)
1769 binary = util.binary(to) or util.binary(tn)
1769 binary = util.binary(to) or util.binary(tn)
1770 if opts.git:
1770 if opts.git:
1771 addmodehdr(header, gitmode[oflag], gitmode[nflag])
1771 addmodehdr(header, gitmode[oflag], gitmode[nflag])
1772 if binary:
1772 if binary:
1773 dodiff = 'binary'
1773 dodiff = 'binary'
1774 elif binary or nflag != oflag:
1774 elif binary or nflag != oflag:
1775 losedatafn(f)
1775 losedatafn(f)
1776
1776
1777 if dodiff:
1777 if dodiff:
1778 if opts.git or revs:
1778 if opts.git or revs:
1779 header.insert(0, diffline(join(a), join(b), revs))
1779 header.insert(0, diffline(join(a), join(b), revs))
1780 if dodiff == 'binary':
1780 if dodiff == 'binary':
1781 text = mdiff.b85diff(to, tn)
1781 text = mdiff.b85diff(to, tn)
1782 if text:
1782 if text:
1783 addindexmeta(header, [gitindex(to), gitindex(tn)])
1783 addindexmeta(header, [gitindex(to), gitindex(tn)])
1784 else:
1784 else:
1785 text = mdiff.unidiff(to, date1,
1785 text = mdiff.unidiff(to, date1,
1786 # ctx2 date may be dynamic
1786 # ctx2 date may be dynamic
1787 tn, util.datestr(ctx2.date()),
1787 tn, util.datestr(ctx2.date()),
1788 join(a), join(b), opts=opts)
1788 join(a), join(b), opts=opts)
1789 if header and (text or len(header) > 1):
1789 if header and (text or len(header) > 1):
1790 yield ''.join(header)
1790 yield ''.join(header)
1791 if text:
1791 if text:
1792 yield text
1792 yield text
1793
1793
1794 def diffstatsum(stats):
1794 def diffstatsum(stats):
1795 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
1795 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
1796 for f, a, r, b in stats:
1796 for f, a, r, b in stats:
1797 maxfile = max(maxfile, encoding.colwidth(f))
1797 maxfile = max(maxfile, encoding.colwidth(f))
1798 maxtotal = max(maxtotal, a + r)
1798 maxtotal = max(maxtotal, a + r)
1799 addtotal += a
1799 addtotal += a
1800 removetotal += r
1800 removetotal += r
1801 binary = binary or b
1801 binary = binary or b
1802
1802
1803 return maxfile, maxtotal, addtotal, removetotal, binary
1803 return maxfile, maxtotal, addtotal, removetotal, binary
1804
1804
1805 def diffstatdata(lines):
1805 def diffstatdata(lines):
1806 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1806 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1807
1807
1808 results = []
1808 results = []
1809 filename, adds, removes, isbinary = None, 0, 0, False
1809 filename, adds, removes, isbinary = None, 0, 0, False
1810
1810
1811 def addresult():
1811 def addresult():
1812 if filename:
1812 if filename:
1813 results.append((filename, adds, removes, isbinary))
1813 results.append((filename, adds, removes, isbinary))
1814
1814
1815 for line in lines:
1815 for line in lines:
1816 if line.startswith('diff'):
1816 if line.startswith('diff'):
1817 addresult()
1817 addresult()
1818 # set numbers to 0 anyway when starting new file
1818 # set numbers to 0 anyway when starting new file
1819 adds, removes, isbinary = 0, 0, False
1819 adds, removes, isbinary = 0, 0, False
1820 if line.startswith('diff --git'):
1820 if line.startswith('diff --git'):
1821 filename = gitre.search(line).group(1)
1821 filename = gitre.search(line).group(1)
1822 elif line.startswith('diff -r'):
1822 elif line.startswith('diff -r'):
1823 # format: "diff -r ... -r ... filename"
1823 # format: "diff -r ... -r ... filename"
1824 filename = diffre.search(line).group(1)
1824 filename = diffre.search(line).group(1)
1825 elif line.startswith('+') and not line.startswith('+++ '):
1825 elif line.startswith('+') and not line.startswith('+++ '):
1826 adds += 1
1826 adds += 1
1827 elif line.startswith('-') and not line.startswith('--- '):
1827 elif line.startswith('-') and not line.startswith('--- '):
1828 removes += 1
1828 removes += 1
1829 elif (line.startswith('GIT binary patch') or
1829 elif (line.startswith('GIT binary patch') or
1830 line.startswith('Binary file')):
1830 line.startswith('Binary file')):
1831 isbinary = True
1831 isbinary = True
1832 addresult()
1832 addresult()
1833 return results
1833 return results
1834
1834
1835 def diffstat(lines, width=80, git=False):
1835 def diffstat(lines, width=80, git=False):
1836 output = []
1836 output = []
1837 stats = diffstatdata(lines)
1837 stats = diffstatdata(lines)
1838 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
1838 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
1839
1839
1840 countwidth = len(str(maxtotal))
1840 countwidth = len(str(maxtotal))
1841 if hasbinary and countwidth < 3:
1841 if hasbinary and countwidth < 3:
1842 countwidth = 3
1842 countwidth = 3
1843 graphwidth = width - countwidth - maxname - 6
1843 graphwidth = width - countwidth - maxname - 6
1844 if graphwidth < 10:
1844 if graphwidth < 10:
1845 graphwidth = 10
1845 graphwidth = 10
1846
1846
1847 def scale(i):
1847 def scale(i):
1848 if maxtotal <= graphwidth:
1848 if maxtotal <= graphwidth:
1849 return i
1849 return i
1850 # If diffstat runs out of room it doesn't print anything,
1850 # If diffstat runs out of room it doesn't print anything,
1851 # which isn't very useful, so always print at least one + or -
1851 # which isn't very useful, so always print at least one + or -
1852 # if there were at least some changes.
1852 # if there were at least some changes.
1853 return max(i * graphwidth // maxtotal, int(bool(i)))
1853 return max(i * graphwidth // maxtotal, int(bool(i)))
1854
1854
1855 for filename, adds, removes, isbinary in stats:
1855 for filename, adds, removes, isbinary in stats:
1856 if isbinary:
1856 if isbinary:
1857 count = 'Bin'
1857 count = 'Bin'
1858 else:
1858 else:
1859 count = adds + removes
1859 count = adds + removes
1860 pluses = '+' * scale(adds)
1860 pluses = '+' * scale(adds)
1861 minuses = '-' * scale(removes)
1861 minuses = '-' * scale(removes)
1862 output.append(' %s%s | %*s %s%s\n' %
1862 output.append(' %s%s | %*s %s%s\n' %
1863 (filename, ' ' * (maxname - encoding.colwidth(filename)),
1863 (filename, ' ' * (maxname - encoding.colwidth(filename)),
1864 countwidth, count, pluses, minuses))
1864 countwidth, count, pluses, minuses))
1865
1865
1866 if stats:
1866 if stats:
1867 output.append(_(' %d files changed, %d insertions(+), '
1867 output.append(_(' %d files changed, %d insertions(+), '
1868 '%d deletions(-)\n')
1868 '%d deletions(-)\n')
1869 % (len(stats), totaladds, totalremoves))
1869 % (len(stats), totaladds, totalremoves))
1870
1870
1871 return ''.join(output)
1871 return ''.join(output)
1872
1872
1873 def diffstatui(*args, **kw):
1873 def diffstatui(*args, **kw):
1874 '''like diffstat(), but yields 2-tuples of (output, label) for
1874 '''like diffstat(), but yields 2-tuples of (output, label) for
1875 ui.write()
1875 ui.write()
1876 '''
1876 '''
1877
1877
1878 for line in diffstat(*args, **kw).splitlines():
1878 for line in diffstat(*args, **kw).splitlines():
1879 if line and line[-1] in '+-':
1879 if line and line[-1] in '+-':
1880 name, graph = line.rsplit(' ', 1)
1880 name, graph = line.rsplit(' ', 1)
1881 yield (name + ' ', '')
1881 yield (name + ' ', '')
1882 m = re.search(r'\++', graph)
1882 m = re.search(r'\++', graph)
1883 if m:
1883 if m:
1884 yield (m.group(0), 'diffstat.inserted')
1884 yield (m.group(0), 'diffstat.inserted')
1885 m = re.search(r'-+', graph)
1885 m = re.search(r'-+', graph)
1886 if m:
1886 if m:
1887 yield (m.group(0), 'diffstat.deleted')
1887 yield (m.group(0), 'diffstat.deleted')
1888 else:
1888 else:
1889 yield (line, '')
1889 yield (line, '')
1890 yield ('\n', '')
1890 yield ('\n', '')
@@ -1,1281 +1,1281 b''
1 # subrepo.py - sub-repository handling for Mercurial
1 # subrepo.py - sub-repository handling for Mercurial
2 #
2 #
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import errno, os, re, xml.dom.minidom, shutil, posixpath
8 import errno, os, re, xml.dom.minidom, shutil, posixpath
9 import stat, subprocess, tarfile
9 import stat, subprocess, tarfile
10 from i18n import _
10 from i18n import _
11 import config, scmutil, util, node, error, cmdutil, bookmarks, match as matchmod
11 import config, scmutil, util, node, error, cmdutil, bookmarks, match as matchmod
12 hg = None
12 hg = None
13 propertycache = util.propertycache
13 propertycache = util.propertycache
14
14
15 nullstate = ('', '', 'empty')
15 nullstate = ('', '', 'empty')
16
16
17 def state(ctx, ui):
17 def state(ctx, ui):
18 """return a state dict, mapping subrepo paths configured in .hgsub
18 """return a state dict, mapping subrepo paths configured in .hgsub
19 to tuple: (source from .hgsub, revision from .hgsubstate, kind
19 to tuple: (source from .hgsub, revision from .hgsubstate, kind
20 (key in types dict))
20 (key in types dict))
21 """
21 """
22 p = config.config()
22 p = config.config()
23 def read(f, sections=None, remap=None):
23 def read(f, sections=None, remap=None):
24 if f in ctx:
24 if f in ctx:
25 try:
25 try:
26 data = ctx[f].data()
26 data = ctx[f].data()
27 except IOError, err:
27 except IOError, err:
28 if err.errno != errno.ENOENT:
28 if err.errno != errno.ENOENT:
29 raise
29 raise
30 # handle missing subrepo spec files as removed
30 # handle missing subrepo spec files as removed
31 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
31 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
32 return
32 return
33 p.parse(f, data, sections, remap, read)
33 p.parse(f, data, sections, remap, read)
34 else:
34 else:
35 raise util.Abort(_("subrepo spec file %s not found") % f)
35 raise util.Abort(_("subrepo spec file %s not found") % f)
36
36
37 if '.hgsub' in ctx:
37 if '.hgsub' in ctx:
38 read('.hgsub')
38 read('.hgsub')
39
39
40 for path, src in ui.configitems('subpaths'):
40 for path, src in ui.configitems('subpaths'):
41 p.set('subpaths', path, src, ui.configsource('subpaths', path))
41 p.set('subpaths', path, src, ui.configsource('subpaths', path))
42
42
43 rev = {}
43 rev = {}
44 if '.hgsubstate' in ctx:
44 if '.hgsubstate' in ctx:
45 try:
45 try:
46 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
46 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
47 l = l.lstrip()
47 l = l.lstrip()
48 if not l:
48 if not l:
49 continue
49 continue
50 try:
50 try:
51 revision, path = l.split(" ", 1)
51 revision, path = l.split(" ", 1)
52 except ValueError:
52 except ValueError:
53 raise util.Abort(_("invalid subrepository revision "
53 raise util.Abort(_("invalid subrepository revision "
54 "specifier in .hgsubstate line %d")
54 "specifier in .hgsubstate line %d")
55 % (i + 1))
55 % (i + 1))
56 rev[path] = revision
56 rev[path] = revision
57 except IOError, err:
57 except IOError, err:
58 if err.errno != errno.ENOENT:
58 if err.errno != errno.ENOENT:
59 raise
59 raise
60
60
61 def remap(src):
61 def remap(src):
62 for pattern, repl in p.items('subpaths'):
62 for pattern, repl in p.items('subpaths'):
63 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
63 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
64 # does a string decode.
64 # does a string decode.
65 repl = repl.encode('string-escape')
65 repl = repl.encode('string-escape')
66 # However, we still want to allow back references to go
66 # However, we still want to allow back references to go
67 # through unharmed, so we turn r'\\1' into r'\1'. Again,
67 # through unharmed, so we turn r'\\1' into r'\1'. Again,
68 # extra escapes are needed because re.sub string decodes.
68 # extra escapes are needed because re.sub string decodes.
69 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
69 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
70 try:
70 try:
71 src = re.sub(pattern, repl, src, 1)
71 src = re.sub(pattern, repl, src, 1)
72 except re.error, e:
72 except re.error, e:
73 raise util.Abort(_("bad subrepository pattern in %s: %s")
73 raise util.Abort(_("bad subrepository pattern in %s: %s")
74 % (p.source('subpaths', pattern), e))
74 % (p.source('subpaths', pattern), e))
75 return src
75 return src
76
76
77 state = {}
77 state = {}
78 for path, src in p[''].items():
78 for path, src in p[''].items():
79 kind = 'hg'
79 kind = 'hg'
80 if src.startswith('['):
80 if src.startswith('['):
81 if ']' not in src:
81 if ']' not in src:
82 raise util.Abort(_('missing ] in subrepo source'))
82 raise util.Abort(_('missing ] in subrepo source'))
83 kind, src = src.split(']', 1)
83 kind, src = src.split(']', 1)
84 kind = kind[1:]
84 kind = kind[1:]
85 src = src.lstrip() # strip any extra whitespace after ']'
85 src = src.lstrip() # strip any extra whitespace after ']'
86
86
87 if not util.url(src).isabs():
87 if not util.url(src).isabs():
88 parent = _abssource(ctx._repo, abort=False)
88 parent = _abssource(ctx._repo, abort=False)
89 if parent:
89 if parent:
90 parent = util.url(parent)
90 parent = util.url(parent)
91 parent.path = posixpath.join(parent.path or '', src)
91 parent.path = posixpath.join(parent.path or '', src)
92 parent.path = posixpath.normpath(parent.path)
92 parent.path = posixpath.normpath(parent.path)
93 joined = str(parent)
93 joined = str(parent)
94 # Remap the full joined path and use it if it changes,
94 # Remap the full joined path and use it if it changes,
95 # else remap the original source.
95 # else remap the original source.
96 remapped = remap(joined)
96 remapped = remap(joined)
97 if remapped == joined:
97 if remapped == joined:
98 src = remap(src)
98 src = remap(src)
99 else:
99 else:
100 src = remapped
100 src = remapped
101
101
102 src = remap(src)
102 src = remap(src)
103 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
103 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
104
104
105 return state
105 return state
106
106
107 def writestate(repo, state):
107 def writestate(repo, state):
108 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
108 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
109 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
109 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
110 repo.wwrite('.hgsubstate', ''.join(lines), '')
110 repo.wwrite('.hgsubstate', ''.join(lines), '')
111
111
112 def submerge(repo, wctx, mctx, actx, overwrite):
112 def submerge(repo, wctx, mctx, actx, overwrite):
113 """delegated from merge.applyupdates: merging of .hgsubstate file
113 """delegated from merge.applyupdates: merging of .hgsubstate file
114 in working context, merging context and ancestor context"""
114 in working context, merging context and ancestor context"""
115 if mctx == actx: # backwards?
115 if mctx == actx: # backwards?
116 actx = wctx.p1()
116 actx = wctx.p1()
117 s1 = wctx.substate
117 s1 = wctx.substate
118 s2 = mctx.substate
118 s2 = mctx.substate
119 sa = actx.substate
119 sa = actx.substate
120 sm = {}
120 sm = {}
121
121
122 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
122 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
123
123
124 def debug(s, msg, r=""):
124 def debug(s, msg, r=""):
125 if r:
125 if r:
126 r = "%s:%s:%s" % r
126 r = "%s:%s:%s" % r
127 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
127 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
128
128
129 for s, l in s1.items():
129 for s, l in s1.items():
130 a = sa.get(s, nullstate)
130 a = sa.get(s, nullstate)
131 ld = l # local state with possible dirty flag for compares
131 ld = l # local state with possible dirty flag for compares
132 if wctx.sub(s).dirty():
132 if wctx.sub(s).dirty():
133 ld = (l[0], l[1] + "+")
133 ld = (l[0], l[1] + "+")
134 if wctx == actx: # overwrite
134 if wctx == actx: # overwrite
135 a = ld
135 a = ld
136
136
137 if s in s2:
137 if s in s2:
138 r = s2[s]
138 r = s2[s]
139 if ld == r or r == a: # no change or local is newer
139 if ld == r or r == a: # no change or local is newer
140 sm[s] = l
140 sm[s] = l
141 continue
141 continue
142 elif ld == a: # other side changed
142 elif ld == a: # other side changed
143 debug(s, "other changed, get", r)
143 debug(s, "other changed, get", r)
144 wctx.sub(s).get(r, overwrite)
144 wctx.sub(s).get(r, overwrite)
145 sm[s] = r
145 sm[s] = r
146 elif ld[0] != r[0]: # sources differ
146 elif ld[0] != r[0]: # sources differ
147 if repo.ui.promptchoice(
147 if repo.ui.promptchoice(
148 _(' subrepository sources for %s differ\n'
148 _(' subrepository sources for %s differ\n'
149 'use (l)ocal source (%s) or (r)emote source (%s)?')
149 'use (l)ocal source (%s) or (r)emote source (%s)?')
150 % (s, l[0], r[0]),
150 % (s, l[0], r[0]),
151 (_('&Local'), _('&Remote')), 0):
151 (_('&Local'), _('&Remote')), 0):
152 debug(s, "prompt changed, get", r)
152 debug(s, "prompt changed, get", r)
153 wctx.sub(s).get(r, overwrite)
153 wctx.sub(s).get(r, overwrite)
154 sm[s] = r
154 sm[s] = r
155 elif ld[1] == a[1]: # local side is unchanged
155 elif ld[1] == a[1]: # local side is unchanged
156 debug(s, "other side changed, get", r)
156 debug(s, "other side changed, get", r)
157 wctx.sub(s).get(r, overwrite)
157 wctx.sub(s).get(r, overwrite)
158 sm[s] = r
158 sm[s] = r
159 else:
159 else:
160 debug(s, "both sides changed, merge with", r)
160 debug(s, "both sides changed, merge with", r)
161 wctx.sub(s).merge(r)
161 wctx.sub(s).merge(r)
162 sm[s] = l
162 sm[s] = l
163 elif ld == a: # remote removed, local unchanged
163 elif ld == a: # remote removed, local unchanged
164 debug(s, "remote removed, remove")
164 debug(s, "remote removed, remove")
165 wctx.sub(s).remove()
165 wctx.sub(s).remove()
166 elif a == nullstate: # not present in remote or ancestor
166 elif a == nullstate: # not present in remote or ancestor
167 debug(s, "local added, keep")
167 debug(s, "local added, keep")
168 sm[s] = l
168 sm[s] = l
169 continue
169 continue
170 else:
170 else:
171 if repo.ui.promptchoice(
171 if repo.ui.promptchoice(
172 _(' local changed subrepository %s which remote removed\n'
172 _(' local changed subrepository %s which remote removed\n'
173 'use (c)hanged version or (d)elete?') % s,
173 'use (c)hanged version or (d)elete?') % s,
174 (_('&Changed'), _('&Delete')), 0):
174 (_('&Changed'), _('&Delete')), 0):
175 debug(s, "prompt remove")
175 debug(s, "prompt remove")
176 wctx.sub(s).remove()
176 wctx.sub(s).remove()
177
177
178 for s, r in sorted(s2.items()):
178 for s, r in sorted(s2.items()):
179 if s in s1:
179 if s in s1:
180 continue
180 continue
181 elif s not in sa:
181 elif s not in sa:
182 debug(s, "remote added, get", r)
182 debug(s, "remote added, get", r)
183 mctx.sub(s).get(r)
183 mctx.sub(s).get(r)
184 sm[s] = r
184 sm[s] = r
185 elif r != sa[s]:
185 elif r != sa[s]:
186 if repo.ui.promptchoice(
186 if repo.ui.promptchoice(
187 _(' remote changed subrepository %s which local removed\n'
187 _(' remote changed subrepository %s which local removed\n'
188 'use (c)hanged version or (d)elete?') % s,
188 'use (c)hanged version or (d)elete?') % s,
189 (_('&Changed'), _('&Delete')), 0) == 0:
189 (_('&Changed'), _('&Delete')), 0) == 0:
190 debug(s, "prompt recreate", r)
190 debug(s, "prompt recreate", r)
191 wctx.sub(s).get(r)
191 wctx.sub(s).get(r)
192 sm[s] = r
192 sm[s] = r
193
193
194 # record merged .hgsubstate
194 # record merged .hgsubstate
195 writestate(repo, sm)
195 writestate(repo, sm)
196
196
197 def _updateprompt(ui, sub, dirty, local, remote):
197 def _updateprompt(ui, sub, dirty, local, remote):
198 if dirty:
198 if dirty:
199 msg = (_(' subrepository sources for %s differ\n'
199 msg = (_(' subrepository sources for %s differ\n'
200 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
200 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
201 % (subrelpath(sub), local, remote))
201 % (subrelpath(sub), local, remote))
202 else:
202 else:
203 msg = (_(' subrepository sources for %s differ (in checked out '
203 msg = (_(' subrepository sources for %s differ (in checked out '
204 'version)\n'
204 'version)\n'
205 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
205 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
206 % (subrelpath(sub), local, remote))
206 % (subrelpath(sub), local, remote))
207 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
207 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
208
208
209 def reporelpath(repo):
209 def reporelpath(repo):
210 """return path to this (sub)repo as seen from outermost repo"""
210 """return path to this (sub)repo as seen from outermost repo"""
211 parent = repo
211 parent = repo
212 while util.safehasattr(parent, '_subparent'):
212 while util.safehasattr(parent, '_subparent'):
213 parent = parent._subparent
213 parent = parent._subparent
214 p = parent.root.rstrip(os.sep)
214 p = parent.root.rstrip(os.sep)
215 return repo.root[len(p) + 1:]
215 return repo.root[len(p) + 1:]
216
216
217 def subrelpath(sub):
217 def subrelpath(sub):
218 """return path to this subrepo as seen from outermost repo"""
218 """return path to this subrepo as seen from outermost repo"""
219 if util.safehasattr(sub, '_relpath'):
219 if util.safehasattr(sub, '_relpath'):
220 return sub._relpath
220 return sub._relpath
221 if not util.safehasattr(sub, '_repo'):
221 if not util.safehasattr(sub, '_repo'):
222 return sub._path
222 return sub._path
223 return reporelpath(sub._repo)
223 return reporelpath(sub._repo)
224
224
225 def _abssource(repo, push=False, abort=True):
225 def _abssource(repo, push=False, abort=True):
226 """return pull/push path of repo - either based on parent repo .hgsub info
226 """return pull/push path of repo - either based on parent repo .hgsub info
227 or on the top repo config. Abort or return None if no source found."""
227 or on the top repo config. Abort or return None if no source found."""
228 if util.safehasattr(repo, '_subparent'):
228 if util.safehasattr(repo, '_subparent'):
229 source = util.url(repo._subsource)
229 source = util.url(repo._subsource)
230 if source.isabs():
230 if source.isabs():
231 return str(source)
231 return str(source)
232 source.path = posixpath.normpath(source.path)
232 source.path = posixpath.normpath(source.path)
233 parent = _abssource(repo._subparent, push, abort=False)
233 parent = _abssource(repo._subparent, push, abort=False)
234 if parent:
234 if parent:
235 parent = util.url(util.pconvert(parent))
235 parent = util.url(util.pconvert(parent))
236 parent.path = posixpath.join(parent.path or '', source.path)
236 parent.path = posixpath.join(parent.path or '', source.path)
237 parent.path = posixpath.normpath(parent.path)
237 parent.path = posixpath.normpath(parent.path)
238 return str(parent)
238 return str(parent)
239 else: # recursion reached top repo
239 else: # recursion reached top repo
240 if util.safehasattr(repo, '_subtoppath'):
240 if util.safehasattr(repo, '_subtoppath'):
241 return repo._subtoppath
241 return repo._subtoppath
242 if push and repo.ui.config('paths', 'default-push'):
242 if push and repo.ui.config('paths', 'default-push'):
243 return repo.ui.config('paths', 'default-push')
243 return repo.ui.config('paths', 'default-push')
244 if repo.ui.config('paths', 'default'):
244 if repo.ui.config('paths', 'default'):
245 return repo.ui.config('paths', 'default')
245 return repo.ui.config('paths', 'default')
246 if abort:
246 if abort:
247 raise util.Abort(_("default path for subrepository %s not found") %
247 raise util.Abort(_("default path for subrepository %s not found") %
248 reporelpath(repo))
248 reporelpath(repo))
249
249
250 def itersubrepos(ctx1, ctx2):
250 def itersubrepos(ctx1, ctx2):
251 """find subrepos in ctx1 or ctx2"""
251 """find subrepos in ctx1 or ctx2"""
252 # Create a (subpath, ctx) mapping where we prefer subpaths from
252 # Create a (subpath, ctx) mapping where we prefer subpaths from
253 # ctx1. The subpaths from ctx2 are important when the .hgsub file
253 # ctx1. The subpaths from ctx2 are important when the .hgsub file
254 # has been modified (in ctx2) but not yet committed (in ctx1).
254 # has been modified (in ctx2) but not yet committed (in ctx1).
255 subpaths = dict.fromkeys(ctx2.substate, ctx2)
255 subpaths = dict.fromkeys(ctx2.substate, ctx2)
256 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
256 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
257 for subpath, ctx in sorted(subpaths.iteritems()):
257 for subpath, ctx in sorted(subpaths.iteritems()):
258 yield subpath, ctx.sub(subpath)
258 yield subpath, ctx.sub(subpath)
259
259
260 def subrepo(ctx, path):
260 def subrepo(ctx, path):
261 """return instance of the right subrepo class for subrepo in path"""
261 """return instance of the right subrepo class for subrepo in path"""
262 # subrepo inherently violates our import layering rules
262 # subrepo inherently violates our import layering rules
263 # because it wants to make repo objects from deep inside the stack
263 # because it wants to make repo objects from deep inside the stack
264 # so we manually delay the circular imports to not break
264 # so we manually delay the circular imports to not break
265 # scripts that don't use our demand-loading
265 # scripts that don't use our demand-loading
266 global hg
266 global hg
267 import hg as h
267 import hg as h
268 hg = h
268 hg = h
269
269
270 scmutil.pathauditor(ctx._repo.root)(path)
270 scmutil.pathauditor(ctx._repo.root)(path)
271 state = ctx.substate[path]
271 state = ctx.substate[path]
272 if state[2] not in types:
272 if state[2] not in types:
273 raise util.Abort(_('unknown subrepo type %s') % state[2])
273 raise util.Abort(_('unknown subrepo type %s') % state[2])
274 return types[state[2]](ctx, path, state[:2])
274 return types[state[2]](ctx, path, state[:2])
275
275
276 # subrepo classes need to implement the following abstract class:
276 # subrepo classes need to implement the following abstract class:
277
277
278 class abstractsubrepo(object):
278 class abstractsubrepo(object):
279
279
280 def dirty(self, ignoreupdate=False):
280 def dirty(self, ignoreupdate=False):
281 """returns true if the dirstate of the subrepo is dirty or does not
281 """returns true if the dirstate of the subrepo is dirty or does not
282 match current stored state. If ignoreupdate is true, only check
282 match current stored state. If ignoreupdate is true, only check
283 whether the subrepo has uncommitted changes in its dirstate.
283 whether the subrepo has uncommitted changes in its dirstate.
284 """
284 """
285 raise NotImplementedError
285 raise NotImplementedError
286
286
287 def basestate(self):
287 def basestate(self):
288 """current working directory base state, disregarding .hgsubstate
288 """current working directory base state, disregarding .hgsubstate
289 state and working directory modifications"""
289 state and working directory modifications"""
290 raise NotImplementedError
290 raise NotImplementedError
291
291
292 def checknested(self, path):
292 def checknested(self, path):
293 """check if path is a subrepository within this repository"""
293 """check if path is a subrepository within this repository"""
294 return False
294 return False
295
295
296 def commit(self, text, user, date):
296 def commit(self, text, user, date):
297 """commit the current changes to the subrepo with the given
297 """commit the current changes to the subrepo with the given
298 log message. Use given user and date if possible. Return the
298 log message. Use given user and date if possible. Return the
299 new state of the subrepo.
299 new state of the subrepo.
300 """
300 """
301 raise NotImplementedError
301 raise NotImplementedError
302
302
303 def remove(self):
303 def remove(self):
304 """remove the subrepo
304 """remove the subrepo
305
305
306 (should verify the dirstate is not dirty first)
306 (should verify the dirstate is not dirty first)
307 """
307 """
308 raise NotImplementedError
308 raise NotImplementedError
309
309
310 def get(self, state, overwrite=False):
310 def get(self, state, overwrite=False):
311 """run whatever commands are needed to put the subrepo into
311 """run whatever commands are needed to put the subrepo into
312 this state
312 this state
313 """
313 """
314 raise NotImplementedError
314 raise NotImplementedError
315
315
316 def merge(self, state):
316 def merge(self, state):
317 """merge currently-saved state with the new state."""
317 """merge currently-saved state with the new state."""
318 raise NotImplementedError
318 raise NotImplementedError
319
319
320 def push(self, opts):
320 def push(self, opts):
321 """perform whatever action is analogous to 'hg push'
321 """perform whatever action is analogous to 'hg push'
322
322
323 This may be a no-op on some systems.
323 This may be a no-op on some systems.
324 """
324 """
325 raise NotImplementedError
325 raise NotImplementedError
326
326
327 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
327 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
328 return []
328 return []
329
329
330 def status(self, rev2, **opts):
330 def status(self, rev2, **opts):
331 return [], [], [], [], [], [], []
331 return [], [], [], [], [], [], []
332
332
333 def diff(self, diffopts, node2, match, prefix, **opts):
333 def diff(self, diffopts, node2, match, prefix, **opts):
334 pass
334 pass
335
335
336 def outgoing(self, ui, dest, opts):
336 def outgoing(self, ui, dest, opts):
337 return 1
337 return 1
338
338
339 def incoming(self, ui, source, opts):
339 def incoming(self, ui, source, opts):
340 return 1
340 return 1
341
341
342 def files(self):
342 def files(self):
343 """return filename iterator"""
343 """return filename iterator"""
344 raise NotImplementedError
344 raise NotImplementedError
345
345
346 def filedata(self, name):
346 def filedata(self, name):
347 """return file data"""
347 """return file data"""
348 raise NotImplementedError
348 raise NotImplementedError
349
349
350 def fileflags(self, name):
350 def fileflags(self, name):
351 """return file flags"""
351 """return file flags"""
352 return ''
352 return ''
353
353
354 def archive(self, ui, archiver, prefix, match=None):
354 def archive(self, ui, archiver, prefix, match=None):
355 if match is not None:
355 if match is not None:
356 files = [f for f in self.files() if match(f)]
356 files = [f for f in self.files() if match(f)]
357 else:
357 else:
358 files = self.files()
358 files = self.files()
359 total = len(files)
359 total = len(files)
360 relpath = subrelpath(self)
360 relpath = subrelpath(self)
361 ui.progress(_('archiving (%s)') % relpath, 0,
361 ui.progress(_('archiving (%s)') % relpath, 0,
362 unit=_('files'), total=total)
362 unit=_('files'), total=total)
363 for i, name in enumerate(files):
363 for i, name in enumerate(files):
364 flags = self.fileflags(name)
364 flags = self.fileflags(name)
365 mode = 'x' in flags and 0755 or 0644
365 mode = 'x' in flags and 0755 or 0644
366 symlink = 'l' in flags
366 symlink = 'l' in flags
367 archiver.addfile(os.path.join(prefix, self._path, name),
367 archiver.addfile(os.path.join(prefix, self._path, name),
368 mode, symlink, self.filedata(name))
368 mode, symlink, self.filedata(name))
369 ui.progress(_('archiving (%s)') % relpath, i + 1,
369 ui.progress(_('archiving (%s)') % relpath, i + 1,
370 unit=_('files'), total=total)
370 unit=_('files'), total=total)
371 ui.progress(_('archiving (%s)') % relpath, None)
371 ui.progress(_('archiving (%s)') % relpath, None)
372
372
373 def walk(self, match):
373 def walk(self, match):
374 '''
374 '''
375 walk recursively through the directory tree, finding all files
375 walk recursively through the directory tree, finding all files
376 matched by the match function
376 matched by the match function
377 '''
377 '''
378 pass
378 pass
379
379
380 def forget(self, ui, match, prefix):
380 def forget(self, ui, match, prefix):
381 return ([], [])
381 return ([], [])
382
382
383 def revert(self, ui, substate, *pats, **opts):
383 def revert(self, ui, substate, *pats, **opts):
384 ui.warn('%s: reverting %s subrepos is unsupported\n' \
384 ui.warn('%s: reverting %s subrepos is unsupported\n' \
385 % (substate[0], substate[2]))
385 % (substate[0], substate[2]))
386 return []
386 return []
387
387
388 class hgsubrepo(abstractsubrepo):
388 class hgsubrepo(abstractsubrepo):
389 def __init__(self, ctx, path, state):
389 def __init__(self, ctx, path, state):
390 self._path = path
390 self._path = path
391 self._state = state
391 self._state = state
392 r = ctx._repo
392 r = ctx._repo
393 root = r.wjoin(path)
393 root = r.wjoin(path)
394 create = False
394 create = False
395 if not os.path.exists(os.path.join(root, '.hg')):
395 if not os.path.exists(os.path.join(root, '.hg')):
396 create = True
396 create = True
397 util.makedirs(root)
397 util.makedirs(root)
398 self._repo = hg.repository(r.baseui, root, create=create)
398 self._repo = hg.repository(r.baseui, root, create=create)
399 for s, k in [('ui', 'commitsubrepos')]:
399 for s, k in [('ui', 'commitsubrepos')]:
400 v = r.ui.config(s, k)
400 v = r.ui.config(s, k)
401 if v:
401 if v:
402 self._repo.ui.setconfig(s, k, v)
402 self._repo.ui.setconfig(s, k, v)
403 self._initrepo(r, state[0], create)
403 self._initrepo(r, state[0], create)
404
404
405 def _initrepo(self, parentrepo, source, create):
405 def _initrepo(self, parentrepo, source, create):
406 self._repo._subparent = parentrepo
406 self._repo._subparent = parentrepo
407 self._repo._subsource = source
407 self._repo._subsource = source
408
408
409 if create:
409 if create:
410 fp = self._repo.opener("hgrc", "w", text=True)
410 fp = self._repo.opener("hgrc", "w", text=True)
411 fp.write('[paths]\n')
411 fp.write('[paths]\n')
412
412
413 def addpathconfig(key, value):
413 def addpathconfig(key, value):
414 if value:
414 if value:
415 fp.write('%s = %s\n' % (key, value))
415 fp.write('%s = %s\n' % (key, value))
416 self._repo.ui.setconfig('paths', key, value)
416 self._repo.ui.setconfig('paths', key, value)
417
417
418 defpath = _abssource(self._repo, abort=False)
418 defpath = _abssource(self._repo, abort=False)
419 defpushpath = _abssource(self._repo, True, abort=False)
419 defpushpath = _abssource(self._repo, True, abort=False)
420 addpathconfig('default', defpath)
420 addpathconfig('default', defpath)
421 if defpath != defpushpath:
421 if defpath != defpushpath:
422 addpathconfig('default-push', defpushpath)
422 addpathconfig('default-push', defpushpath)
423 fp.close()
423 fp.close()
424
424
425 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
425 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
426 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
426 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
427 os.path.join(prefix, self._path), explicitonly)
427 os.path.join(prefix, self._path), explicitonly)
428
428
429 def status(self, rev2, **opts):
429 def status(self, rev2, **opts):
430 try:
430 try:
431 rev1 = self._state[1]
431 rev1 = self._state[1]
432 ctx1 = self._repo[rev1]
432 ctx1 = self._repo[rev1]
433 ctx2 = self._repo[rev2]
433 ctx2 = self._repo[rev2]
434 return self._repo.status(ctx1, ctx2, **opts)
434 return self._repo.status(ctx1, ctx2, **opts)
435 except error.RepoLookupError, inst:
435 except error.RepoLookupError, inst:
436 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
436 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
437 % (inst, subrelpath(self)))
437 % (inst, subrelpath(self)))
438 return [], [], [], [], [], [], []
438 return [], [], [], [], [], [], []
439
439
440 def diff(self, diffopts, node2, match, prefix, **opts):
440 def diff(self, diffopts, node2, match, prefix, **opts):
441 try:
441 try:
442 node1 = node.bin(self._state[1])
442 node1 = node.bin(self._state[1])
443 # We currently expect node2 to come from substate and be
443 # We currently expect node2 to come from substate and be
444 # in hex format
444 # in hex format
445 if node2 is not None:
445 if node2 is not None:
446 node2 = node.bin(node2)
446 node2 = node.bin(node2)
447 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
447 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
448 node1, node2, match,
448 node1, node2, match,
449 prefix=os.path.join(prefix, self._path),
449 prefix=posixpath.join(prefix, self._path),
450 listsubrepos=True, **opts)
450 listsubrepos=True, **opts)
451 except error.RepoLookupError, inst:
451 except error.RepoLookupError, inst:
452 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
452 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
453 % (inst, subrelpath(self)))
453 % (inst, subrelpath(self)))
454
454
455 def archive(self, ui, archiver, prefix, match=None):
455 def archive(self, ui, archiver, prefix, match=None):
456 self._get(self._state + ('hg',))
456 self._get(self._state + ('hg',))
457 abstractsubrepo.archive(self, ui, archiver, prefix, match)
457 abstractsubrepo.archive(self, ui, archiver, prefix, match)
458
458
459 rev = self._state[1]
459 rev = self._state[1]
460 ctx = self._repo[rev]
460 ctx = self._repo[rev]
461 for subpath in ctx.substate:
461 for subpath in ctx.substate:
462 s = subrepo(ctx, subpath)
462 s = subrepo(ctx, subpath)
463 submatch = matchmod.narrowmatcher(subpath, match)
463 submatch = matchmod.narrowmatcher(subpath, match)
464 s.archive(ui, archiver, os.path.join(prefix, self._path), submatch)
464 s.archive(ui, archiver, os.path.join(prefix, self._path), submatch)
465
465
466 def dirty(self, ignoreupdate=False):
466 def dirty(self, ignoreupdate=False):
467 r = self._state[1]
467 r = self._state[1]
468 if r == '' and not ignoreupdate: # no state recorded
468 if r == '' and not ignoreupdate: # no state recorded
469 return True
469 return True
470 w = self._repo[None]
470 w = self._repo[None]
471 if r != w.p1().hex() and not ignoreupdate:
471 if r != w.p1().hex() and not ignoreupdate:
472 # different version checked out
472 # different version checked out
473 return True
473 return True
474 return w.dirty() # working directory changed
474 return w.dirty() # working directory changed
475
475
476 def basestate(self):
476 def basestate(self):
477 return self._repo['.'].hex()
477 return self._repo['.'].hex()
478
478
479 def checknested(self, path):
479 def checknested(self, path):
480 return self._repo._checknested(self._repo.wjoin(path))
480 return self._repo._checknested(self._repo.wjoin(path))
481
481
482 def commit(self, text, user, date):
482 def commit(self, text, user, date):
483 # don't bother committing in the subrepo if it's only been
483 # don't bother committing in the subrepo if it's only been
484 # updated
484 # updated
485 if not self.dirty(True):
485 if not self.dirty(True):
486 return self._repo['.'].hex()
486 return self._repo['.'].hex()
487 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
487 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
488 n = self._repo.commit(text, user, date)
488 n = self._repo.commit(text, user, date)
489 if not n:
489 if not n:
490 return self._repo['.'].hex() # different version checked out
490 return self._repo['.'].hex() # different version checked out
491 return node.hex(n)
491 return node.hex(n)
492
492
493 def remove(self):
493 def remove(self):
494 # we can't fully delete the repository as it may contain
494 # we can't fully delete the repository as it may contain
495 # local-only history
495 # local-only history
496 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
496 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
497 hg.clean(self._repo, node.nullid, False)
497 hg.clean(self._repo, node.nullid, False)
498
498
499 def _get(self, state):
499 def _get(self, state):
500 source, revision, kind = state
500 source, revision, kind = state
501 if revision not in self._repo:
501 if revision not in self._repo:
502 self._repo._subsource = source
502 self._repo._subsource = source
503 srcurl = _abssource(self._repo)
503 srcurl = _abssource(self._repo)
504 other = hg.peer(self._repo, {}, srcurl)
504 other = hg.peer(self._repo, {}, srcurl)
505 if len(self._repo) == 0:
505 if len(self._repo) == 0:
506 self._repo.ui.status(_('cloning subrepo %s from %s\n')
506 self._repo.ui.status(_('cloning subrepo %s from %s\n')
507 % (subrelpath(self), srcurl))
507 % (subrelpath(self), srcurl))
508 parentrepo = self._repo._subparent
508 parentrepo = self._repo._subparent
509 shutil.rmtree(self._repo.path)
509 shutil.rmtree(self._repo.path)
510 other, cloned = hg.clone(self._repo._subparent.baseui, {},
510 other, cloned = hg.clone(self._repo._subparent.baseui, {},
511 other, self._repo.root,
511 other, self._repo.root,
512 update=False)
512 update=False)
513 self._repo = cloned.local()
513 self._repo = cloned.local()
514 self._initrepo(parentrepo, source, create=True)
514 self._initrepo(parentrepo, source, create=True)
515 else:
515 else:
516 self._repo.ui.status(_('pulling subrepo %s from %s\n')
516 self._repo.ui.status(_('pulling subrepo %s from %s\n')
517 % (subrelpath(self), srcurl))
517 % (subrelpath(self), srcurl))
518 self._repo.pull(other)
518 self._repo.pull(other)
519 bookmarks.updatefromremote(self._repo.ui, self._repo, other,
519 bookmarks.updatefromremote(self._repo.ui, self._repo, other,
520 srcurl)
520 srcurl)
521
521
522 def get(self, state, overwrite=False):
522 def get(self, state, overwrite=False):
523 self._get(state)
523 self._get(state)
524 source, revision, kind = state
524 source, revision, kind = state
525 self._repo.ui.debug("getting subrepo %s\n" % self._path)
525 self._repo.ui.debug("getting subrepo %s\n" % self._path)
526 hg.updaterepo(self._repo, revision, overwrite)
526 hg.updaterepo(self._repo, revision, overwrite)
527
527
528 def merge(self, state):
528 def merge(self, state):
529 self._get(state)
529 self._get(state)
530 cur = self._repo['.']
530 cur = self._repo['.']
531 dst = self._repo[state[1]]
531 dst = self._repo[state[1]]
532 anc = dst.ancestor(cur)
532 anc = dst.ancestor(cur)
533
533
534 def mergefunc():
534 def mergefunc():
535 if anc == cur and dst.branch() == cur.branch():
535 if anc == cur and dst.branch() == cur.branch():
536 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
536 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
537 hg.update(self._repo, state[1])
537 hg.update(self._repo, state[1])
538 elif anc == dst:
538 elif anc == dst:
539 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
539 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
540 else:
540 else:
541 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
541 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
542 hg.merge(self._repo, state[1], remind=False)
542 hg.merge(self._repo, state[1], remind=False)
543
543
544 wctx = self._repo[None]
544 wctx = self._repo[None]
545 if self.dirty():
545 if self.dirty():
546 if anc != dst:
546 if anc != dst:
547 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
547 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
548 mergefunc()
548 mergefunc()
549 else:
549 else:
550 mergefunc()
550 mergefunc()
551 else:
551 else:
552 mergefunc()
552 mergefunc()
553
553
554 def push(self, opts):
554 def push(self, opts):
555 force = opts.get('force')
555 force = opts.get('force')
556 newbranch = opts.get('new_branch')
556 newbranch = opts.get('new_branch')
557 ssh = opts.get('ssh')
557 ssh = opts.get('ssh')
558
558
559 # push subrepos depth-first for coherent ordering
559 # push subrepos depth-first for coherent ordering
560 c = self._repo['']
560 c = self._repo['']
561 subs = c.substate # only repos that are committed
561 subs = c.substate # only repos that are committed
562 for s in sorted(subs):
562 for s in sorted(subs):
563 if c.sub(s).push(opts) == 0:
563 if c.sub(s).push(opts) == 0:
564 return False
564 return False
565
565
566 dsturl = _abssource(self._repo, True)
566 dsturl = _abssource(self._repo, True)
567 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
567 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
568 (subrelpath(self), dsturl))
568 (subrelpath(self), dsturl))
569 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
569 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
570 return self._repo.push(other, force, newbranch=newbranch)
570 return self._repo.push(other, force, newbranch=newbranch)
571
571
572 def outgoing(self, ui, dest, opts):
572 def outgoing(self, ui, dest, opts):
573 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
573 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
574
574
575 def incoming(self, ui, source, opts):
575 def incoming(self, ui, source, opts):
576 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
576 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
577
577
578 def files(self):
578 def files(self):
579 rev = self._state[1]
579 rev = self._state[1]
580 ctx = self._repo[rev]
580 ctx = self._repo[rev]
581 return ctx.manifest()
581 return ctx.manifest()
582
582
583 def filedata(self, name):
583 def filedata(self, name):
584 rev = self._state[1]
584 rev = self._state[1]
585 return self._repo[rev][name].data()
585 return self._repo[rev][name].data()
586
586
587 def fileflags(self, name):
587 def fileflags(self, name):
588 rev = self._state[1]
588 rev = self._state[1]
589 ctx = self._repo[rev]
589 ctx = self._repo[rev]
590 return ctx.flags(name)
590 return ctx.flags(name)
591
591
592 def walk(self, match):
592 def walk(self, match):
593 ctx = self._repo[None]
593 ctx = self._repo[None]
594 return ctx.walk(match)
594 return ctx.walk(match)
595
595
596 def forget(self, ui, match, prefix):
596 def forget(self, ui, match, prefix):
597 return cmdutil.forget(ui, self._repo, match,
597 return cmdutil.forget(ui, self._repo, match,
598 os.path.join(prefix, self._path), True)
598 os.path.join(prefix, self._path), True)
599
599
600 def revert(self, ui, substate, *pats, **opts):
600 def revert(self, ui, substate, *pats, **opts):
601 # reverting a subrepo is a 2 step process:
601 # reverting a subrepo is a 2 step process:
602 # 1. if the no_backup is not set, revert all modified
602 # 1. if the no_backup is not set, revert all modified
603 # files inside the subrepo
603 # files inside the subrepo
604 # 2. update the subrepo to the revision specified in
604 # 2. update the subrepo to the revision specified in
605 # the corresponding substate dictionary
605 # the corresponding substate dictionary
606 ui.status(_('reverting subrepo %s\n') % substate[0])
606 ui.status(_('reverting subrepo %s\n') % substate[0])
607 if not opts.get('no_backup'):
607 if not opts.get('no_backup'):
608 # Revert all files on the subrepo, creating backups
608 # Revert all files on the subrepo, creating backups
609 # Note that this will not recursively revert subrepos
609 # Note that this will not recursively revert subrepos
610 # We could do it if there was a set:subrepos() predicate
610 # We could do it if there was a set:subrepos() predicate
611 opts = opts.copy()
611 opts = opts.copy()
612 opts['date'] = None
612 opts['date'] = None
613 opts['rev'] = substate[1]
613 opts['rev'] = substate[1]
614
614
615 pats = []
615 pats = []
616 if not opts['all']:
616 if not opts['all']:
617 pats = ['set:modified()']
617 pats = ['set:modified()']
618 self.filerevert(ui, *pats, **opts)
618 self.filerevert(ui, *pats, **opts)
619
619
620 # Update the repo to the revision specified in the given substate
620 # Update the repo to the revision specified in the given substate
621 self.get(substate, overwrite=True)
621 self.get(substate, overwrite=True)
622
622
623 def filerevert(self, ui, *pats, **opts):
623 def filerevert(self, ui, *pats, **opts):
624 ctx = self._repo[opts['rev']]
624 ctx = self._repo[opts['rev']]
625 parents = self._repo.dirstate.parents()
625 parents = self._repo.dirstate.parents()
626 if opts['all']:
626 if opts['all']:
627 pats = ['set:modified()']
627 pats = ['set:modified()']
628 else:
628 else:
629 pats = []
629 pats = []
630 cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
630 cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
631
631
632 class svnsubrepo(abstractsubrepo):
632 class svnsubrepo(abstractsubrepo):
633 def __init__(self, ctx, path, state):
633 def __init__(self, ctx, path, state):
634 self._path = path
634 self._path = path
635 self._state = state
635 self._state = state
636 self._ctx = ctx
636 self._ctx = ctx
637 self._ui = ctx._repo.ui
637 self._ui = ctx._repo.ui
638 self._exe = util.findexe('svn')
638 self._exe = util.findexe('svn')
639 if not self._exe:
639 if not self._exe:
640 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
640 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
641 % self._path)
641 % self._path)
642
642
643 def _svncommand(self, commands, filename='', failok=False):
643 def _svncommand(self, commands, filename='', failok=False):
644 cmd = [self._exe]
644 cmd = [self._exe]
645 extrakw = {}
645 extrakw = {}
646 if not self._ui.interactive():
646 if not self._ui.interactive():
647 # Making stdin be a pipe should prevent svn from behaving
647 # Making stdin be a pipe should prevent svn from behaving
648 # interactively even if we can't pass --non-interactive.
648 # interactively even if we can't pass --non-interactive.
649 extrakw['stdin'] = subprocess.PIPE
649 extrakw['stdin'] = subprocess.PIPE
650 # Starting in svn 1.5 --non-interactive is a global flag
650 # Starting in svn 1.5 --non-interactive is a global flag
651 # instead of being per-command, but we need to support 1.4 so
651 # instead of being per-command, but we need to support 1.4 so
652 # we have to be intelligent about what commands take
652 # we have to be intelligent about what commands take
653 # --non-interactive.
653 # --non-interactive.
654 if commands[0] in ('update', 'checkout', 'commit'):
654 if commands[0] in ('update', 'checkout', 'commit'):
655 cmd.append('--non-interactive')
655 cmd.append('--non-interactive')
656 cmd.extend(commands)
656 cmd.extend(commands)
657 if filename is not None:
657 if filename is not None:
658 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
658 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
659 cmd.append(path)
659 cmd.append(path)
660 env = dict(os.environ)
660 env = dict(os.environ)
661 # Avoid localized output, preserve current locale for everything else.
661 # Avoid localized output, preserve current locale for everything else.
662 lc_all = env.get('LC_ALL')
662 lc_all = env.get('LC_ALL')
663 if lc_all:
663 if lc_all:
664 env['LANG'] = lc_all
664 env['LANG'] = lc_all
665 del env['LC_ALL']
665 del env['LC_ALL']
666 env['LC_MESSAGES'] = 'C'
666 env['LC_MESSAGES'] = 'C'
667 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
667 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
668 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
668 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
669 universal_newlines=True, env=env, **extrakw)
669 universal_newlines=True, env=env, **extrakw)
670 stdout, stderr = p.communicate()
670 stdout, stderr = p.communicate()
671 stderr = stderr.strip()
671 stderr = stderr.strip()
672 if not failok:
672 if not failok:
673 if p.returncode:
673 if p.returncode:
674 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
674 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
675 if stderr:
675 if stderr:
676 self._ui.warn(stderr + '\n')
676 self._ui.warn(stderr + '\n')
677 return stdout, stderr
677 return stdout, stderr
678
678
679 @propertycache
679 @propertycache
680 def _svnversion(self):
680 def _svnversion(self):
681 output, err = self._svncommand(['--version', '--quiet'], filename=None)
681 output, err = self._svncommand(['--version', '--quiet'], filename=None)
682 m = re.search(r'^(\d+)\.(\d+)', output)
682 m = re.search(r'^(\d+)\.(\d+)', output)
683 if not m:
683 if not m:
684 raise util.Abort(_('cannot retrieve svn tool version'))
684 raise util.Abort(_('cannot retrieve svn tool version'))
685 return (int(m.group(1)), int(m.group(2)))
685 return (int(m.group(1)), int(m.group(2)))
686
686
687 def _wcrevs(self):
687 def _wcrevs(self):
688 # Get the working directory revision as well as the last
688 # Get the working directory revision as well as the last
689 # commit revision so we can compare the subrepo state with
689 # commit revision so we can compare the subrepo state with
690 # both. We used to store the working directory one.
690 # both. We used to store the working directory one.
691 output, err = self._svncommand(['info', '--xml'])
691 output, err = self._svncommand(['info', '--xml'])
692 doc = xml.dom.minidom.parseString(output)
692 doc = xml.dom.minidom.parseString(output)
693 entries = doc.getElementsByTagName('entry')
693 entries = doc.getElementsByTagName('entry')
694 lastrev, rev = '0', '0'
694 lastrev, rev = '0', '0'
695 if entries:
695 if entries:
696 rev = str(entries[0].getAttribute('revision')) or '0'
696 rev = str(entries[0].getAttribute('revision')) or '0'
697 commits = entries[0].getElementsByTagName('commit')
697 commits = entries[0].getElementsByTagName('commit')
698 if commits:
698 if commits:
699 lastrev = str(commits[0].getAttribute('revision')) or '0'
699 lastrev = str(commits[0].getAttribute('revision')) or '0'
700 return (lastrev, rev)
700 return (lastrev, rev)
701
701
702 def _wcrev(self):
702 def _wcrev(self):
703 return self._wcrevs()[0]
703 return self._wcrevs()[0]
704
704
705 def _wcchanged(self):
705 def _wcchanged(self):
706 """Return (changes, extchanges, missing) where changes is True
706 """Return (changes, extchanges, missing) where changes is True
707 if the working directory was changed, extchanges is
707 if the working directory was changed, extchanges is
708 True if any of these changes concern an external entry and missing
708 True if any of these changes concern an external entry and missing
709 is True if any change is a missing entry.
709 is True if any change is a missing entry.
710 """
710 """
711 output, err = self._svncommand(['status', '--xml'])
711 output, err = self._svncommand(['status', '--xml'])
712 externals, changes, missing = [], [], []
712 externals, changes, missing = [], [], []
713 doc = xml.dom.minidom.parseString(output)
713 doc = xml.dom.minidom.parseString(output)
714 for e in doc.getElementsByTagName('entry'):
714 for e in doc.getElementsByTagName('entry'):
715 s = e.getElementsByTagName('wc-status')
715 s = e.getElementsByTagName('wc-status')
716 if not s:
716 if not s:
717 continue
717 continue
718 item = s[0].getAttribute('item')
718 item = s[0].getAttribute('item')
719 props = s[0].getAttribute('props')
719 props = s[0].getAttribute('props')
720 path = e.getAttribute('path')
720 path = e.getAttribute('path')
721 if item == 'external':
721 if item == 'external':
722 externals.append(path)
722 externals.append(path)
723 elif item == 'missing':
723 elif item == 'missing':
724 missing.append(path)
724 missing.append(path)
725 if (item not in ('', 'normal', 'unversioned', 'external')
725 if (item not in ('', 'normal', 'unversioned', 'external')
726 or props not in ('', 'none', 'normal')):
726 or props not in ('', 'none', 'normal')):
727 changes.append(path)
727 changes.append(path)
728 for path in changes:
728 for path in changes:
729 for ext in externals:
729 for ext in externals:
730 if path == ext or path.startswith(ext + os.sep):
730 if path == ext or path.startswith(ext + os.sep):
731 return True, True, bool(missing)
731 return True, True, bool(missing)
732 return bool(changes), False, bool(missing)
732 return bool(changes), False, bool(missing)
733
733
734 def dirty(self, ignoreupdate=False):
734 def dirty(self, ignoreupdate=False):
735 if not self._wcchanged()[0]:
735 if not self._wcchanged()[0]:
736 if self._state[1] in self._wcrevs() or ignoreupdate:
736 if self._state[1] in self._wcrevs() or ignoreupdate:
737 return False
737 return False
738 return True
738 return True
739
739
740 def basestate(self):
740 def basestate(self):
741 lastrev, rev = self._wcrevs()
741 lastrev, rev = self._wcrevs()
742 if lastrev != rev:
742 if lastrev != rev:
743 # Last committed rev is not the same than rev. We would
743 # Last committed rev is not the same than rev. We would
744 # like to take lastrev but we do not know if the subrepo
744 # like to take lastrev but we do not know if the subrepo
745 # URL exists at lastrev. Test it and fallback to rev it
745 # URL exists at lastrev. Test it and fallback to rev it
746 # is not there.
746 # is not there.
747 try:
747 try:
748 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
748 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
749 return lastrev
749 return lastrev
750 except error.Abort:
750 except error.Abort:
751 pass
751 pass
752 return rev
752 return rev
753
753
754 def commit(self, text, user, date):
754 def commit(self, text, user, date):
755 # user and date are out of our hands since svn is centralized
755 # user and date are out of our hands since svn is centralized
756 changed, extchanged, missing = self._wcchanged()
756 changed, extchanged, missing = self._wcchanged()
757 if not changed:
757 if not changed:
758 return self.basestate()
758 return self.basestate()
759 if extchanged:
759 if extchanged:
760 # Do not try to commit externals
760 # Do not try to commit externals
761 raise util.Abort(_('cannot commit svn externals'))
761 raise util.Abort(_('cannot commit svn externals'))
762 if missing:
762 if missing:
763 # svn can commit with missing entries but aborting like hg
763 # svn can commit with missing entries but aborting like hg
764 # seems a better approach.
764 # seems a better approach.
765 raise util.Abort(_('cannot commit missing svn entries'))
765 raise util.Abort(_('cannot commit missing svn entries'))
766 commitinfo, err = self._svncommand(['commit', '-m', text])
766 commitinfo, err = self._svncommand(['commit', '-m', text])
767 self._ui.status(commitinfo)
767 self._ui.status(commitinfo)
768 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
768 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
769 if not newrev:
769 if not newrev:
770 if not commitinfo.strip():
770 if not commitinfo.strip():
771 # Sometimes, our definition of "changed" differs from
771 # Sometimes, our definition of "changed" differs from
772 # svn one. For instance, svn ignores missing files
772 # svn one. For instance, svn ignores missing files
773 # when committing. If there are only missing files, no
773 # when committing. If there are only missing files, no
774 # commit is made, no output and no error code.
774 # commit is made, no output and no error code.
775 raise util.Abort(_('failed to commit svn changes'))
775 raise util.Abort(_('failed to commit svn changes'))
776 raise util.Abort(commitinfo.splitlines()[-1])
776 raise util.Abort(commitinfo.splitlines()[-1])
777 newrev = newrev.groups()[0]
777 newrev = newrev.groups()[0]
778 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
778 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
779 return newrev
779 return newrev
780
780
781 def remove(self):
781 def remove(self):
782 if self.dirty():
782 if self.dirty():
783 self._ui.warn(_('not removing repo %s because '
783 self._ui.warn(_('not removing repo %s because '
784 'it has changes.\n' % self._path))
784 'it has changes.\n' % self._path))
785 return
785 return
786 self._ui.note(_('removing subrepo %s\n') % self._path)
786 self._ui.note(_('removing subrepo %s\n') % self._path)
787
787
788 def onerror(function, path, excinfo):
788 def onerror(function, path, excinfo):
789 if function is not os.remove:
789 if function is not os.remove:
790 raise
790 raise
791 # read-only files cannot be unlinked under Windows
791 # read-only files cannot be unlinked under Windows
792 s = os.stat(path)
792 s = os.stat(path)
793 if (s.st_mode & stat.S_IWRITE) != 0:
793 if (s.st_mode & stat.S_IWRITE) != 0:
794 raise
794 raise
795 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
795 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
796 os.remove(path)
796 os.remove(path)
797
797
798 path = self._ctx._repo.wjoin(self._path)
798 path = self._ctx._repo.wjoin(self._path)
799 shutil.rmtree(path, onerror=onerror)
799 shutil.rmtree(path, onerror=onerror)
800 try:
800 try:
801 os.removedirs(os.path.dirname(path))
801 os.removedirs(os.path.dirname(path))
802 except OSError:
802 except OSError:
803 pass
803 pass
804
804
805 def get(self, state, overwrite=False):
805 def get(self, state, overwrite=False):
806 if overwrite:
806 if overwrite:
807 self._svncommand(['revert', '--recursive'])
807 self._svncommand(['revert', '--recursive'])
808 args = ['checkout']
808 args = ['checkout']
809 if self._svnversion >= (1, 5):
809 if self._svnversion >= (1, 5):
810 args.append('--force')
810 args.append('--force')
811 # The revision must be specified at the end of the URL to properly
811 # The revision must be specified at the end of the URL to properly
812 # update to a directory which has since been deleted and recreated.
812 # update to a directory which has since been deleted and recreated.
813 args.append('%s@%s' % (state[0], state[1]))
813 args.append('%s@%s' % (state[0], state[1]))
814 status, err = self._svncommand(args, failok=True)
814 status, err = self._svncommand(args, failok=True)
815 if not re.search('Checked out revision [0-9]+.', status):
815 if not re.search('Checked out revision [0-9]+.', status):
816 if ('is already a working copy for a different URL' in err
816 if ('is already a working copy for a different URL' in err
817 and (self._wcchanged()[:2] == (False, False))):
817 and (self._wcchanged()[:2] == (False, False))):
818 # obstructed but clean working copy, so just blow it away.
818 # obstructed but clean working copy, so just blow it away.
819 self.remove()
819 self.remove()
820 self.get(state, overwrite=False)
820 self.get(state, overwrite=False)
821 return
821 return
822 raise util.Abort((status or err).splitlines()[-1])
822 raise util.Abort((status or err).splitlines()[-1])
823 self._ui.status(status)
823 self._ui.status(status)
824
824
825 def merge(self, state):
825 def merge(self, state):
826 old = self._state[1]
826 old = self._state[1]
827 new = state[1]
827 new = state[1]
828 wcrev = self._wcrev()
828 wcrev = self._wcrev()
829 if new != wcrev:
829 if new != wcrev:
830 dirty = old == wcrev or self._wcchanged()[0]
830 dirty = old == wcrev or self._wcchanged()[0]
831 if _updateprompt(self._ui, self, dirty, wcrev, new):
831 if _updateprompt(self._ui, self, dirty, wcrev, new):
832 self.get(state, False)
832 self.get(state, False)
833
833
834 def push(self, opts):
834 def push(self, opts):
835 # push is a no-op for SVN
835 # push is a no-op for SVN
836 return True
836 return True
837
837
838 def files(self):
838 def files(self):
839 output = self._svncommand(['list', '--recursive', '--xml'])[0]
839 output = self._svncommand(['list', '--recursive', '--xml'])[0]
840 doc = xml.dom.minidom.parseString(output)
840 doc = xml.dom.minidom.parseString(output)
841 paths = []
841 paths = []
842 for e in doc.getElementsByTagName('entry'):
842 for e in doc.getElementsByTagName('entry'):
843 kind = str(e.getAttribute('kind'))
843 kind = str(e.getAttribute('kind'))
844 if kind != 'file':
844 if kind != 'file':
845 continue
845 continue
846 name = ''.join(c.data for c
846 name = ''.join(c.data for c
847 in e.getElementsByTagName('name')[0].childNodes
847 in e.getElementsByTagName('name')[0].childNodes
848 if c.nodeType == c.TEXT_NODE)
848 if c.nodeType == c.TEXT_NODE)
849 paths.append(name.encode('utf-8'))
849 paths.append(name.encode('utf-8'))
850 return paths
850 return paths
851
851
852 def filedata(self, name):
852 def filedata(self, name):
853 return self._svncommand(['cat'], name)[0]
853 return self._svncommand(['cat'], name)[0]
854
854
855
855
856 class gitsubrepo(abstractsubrepo):
856 class gitsubrepo(abstractsubrepo):
857 def __init__(self, ctx, path, state):
857 def __init__(self, ctx, path, state):
858 self._state = state
858 self._state = state
859 self._ctx = ctx
859 self._ctx = ctx
860 self._path = path
860 self._path = path
861 self._relpath = os.path.join(reporelpath(ctx._repo), path)
861 self._relpath = os.path.join(reporelpath(ctx._repo), path)
862 self._abspath = ctx._repo.wjoin(path)
862 self._abspath = ctx._repo.wjoin(path)
863 self._subparent = ctx._repo
863 self._subparent = ctx._repo
864 self._ui = ctx._repo.ui
864 self._ui = ctx._repo.ui
865 self._ensuregit()
865 self._ensuregit()
866
866
867 def _ensuregit(self):
867 def _ensuregit(self):
868 try:
868 try:
869 self._gitexecutable = 'git'
869 self._gitexecutable = 'git'
870 out, err = self._gitnodir(['--version'])
870 out, err = self._gitnodir(['--version'])
871 except OSError, e:
871 except OSError, e:
872 if e.errno != 2 or os.name != 'nt':
872 if e.errno != 2 or os.name != 'nt':
873 raise
873 raise
874 self._gitexecutable = 'git.cmd'
874 self._gitexecutable = 'git.cmd'
875 out, err = self._gitnodir(['--version'])
875 out, err = self._gitnodir(['--version'])
876 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
876 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
877 if not m:
877 if not m:
878 self._ui.warn(_('cannot retrieve git version'))
878 self._ui.warn(_('cannot retrieve git version'))
879 return
879 return
880 version = (int(m.group(1)), m.group(2), m.group(3))
880 version = (int(m.group(1)), m.group(2), m.group(3))
881 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
881 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
882 # despite the docstring comment. For now, error on 1.4.0, warn on
882 # despite the docstring comment. For now, error on 1.4.0, warn on
883 # 1.5.0 but attempt to continue.
883 # 1.5.0 but attempt to continue.
884 if version < (1, 5, 0):
884 if version < (1, 5, 0):
885 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
885 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
886 elif version < (1, 6, 0):
886 elif version < (1, 6, 0):
887 self._ui.warn(_('git subrepo requires at least 1.6.0 or later'))
887 self._ui.warn(_('git subrepo requires at least 1.6.0 or later'))
888
888
889 def _gitcommand(self, commands, env=None, stream=False):
889 def _gitcommand(self, commands, env=None, stream=False):
890 return self._gitdir(commands, env=env, stream=stream)[0]
890 return self._gitdir(commands, env=env, stream=stream)[0]
891
891
892 def _gitdir(self, commands, env=None, stream=False):
892 def _gitdir(self, commands, env=None, stream=False):
893 return self._gitnodir(commands, env=env, stream=stream,
893 return self._gitnodir(commands, env=env, stream=stream,
894 cwd=self._abspath)
894 cwd=self._abspath)
895
895
896 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
896 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
897 """Calls the git command
897 """Calls the git command
898
898
899 The methods tries to call the git command. versions prior to 1.6.0
899 The methods tries to call the git command. versions prior to 1.6.0
900 are not supported and very probably fail.
900 are not supported and very probably fail.
901 """
901 """
902 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
902 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
903 # unless ui.quiet is set, print git's stderr,
903 # unless ui.quiet is set, print git's stderr,
904 # which is mostly progress and useful info
904 # which is mostly progress and useful info
905 errpipe = None
905 errpipe = None
906 if self._ui.quiet:
906 if self._ui.quiet:
907 errpipe = open(os.devnull, 'w')
907 errpipe = open(os.devnull, 'w')
908 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
908 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
909 cwd=cwd, env=env, close_fds=util.closefds,
909 cwd=cwd, env=env, close_fds=util.closefds,
910 stdout=subprocess.PIPE, stderr=errpipe)
910 stdout=subprocess.PIPE, stderr=errpipe)
911 if stream:
911 if stream:
912 return p.stdout, None
912 return p.stdout, None
913
913
914 retdata = p.stdout.read().strip()
914 retdata = p.stdout.read().strip()
915 # wait for the child to exit to avoid race condition.
915 # wait for the child to exit to avoid race condition.
916 p.wait()
916 p.wait()
917
917
918 if p.returncode != 0 and p.returncode != 1:
918 if p.returncode != 0 and p.returncode != 1:
919 # there are certain error codes that are ok
919 # there are certain error codes that are ok
920 command = commands[0]
920 command = commands[0]
921 if command in ('cat-file', 'symbolic-ref'):
921 if command in ('cat-file', 'symbolic-ref'):
922 return retdata, p.returncode
922 return retdata, p.returncode
923 # for all others, abort
923 # for all others, abort
924 raise util.Abort('git %s error %d in %s' %
924 raise util.Abort('git %s error %d in %s' %
925 (command, p.returncode, self._relpath))
925 (command, p.returncode, self._relpath))
926
926
927 return retdata, p.returncode
927 return retdata, p.returncode
928
928
929 def _gitmissing(self):
929 def _gitmissing(self):
930 return not os.path.exists(os.path.join(self._abspath, '.git'))
930 return not os.path.exists(os.path.join(self._abspath, '.git'))
931
931
932 def _gitstate(self):
932 def _gitstate(self):
933 return self._gitcommand(['rev-parse', 'HEAD'])
933 return self._gitcommand(['rev-parse', 'HEAD'])
934
934
935 def _gitcurrentbranch(self):
935 def _gitcurrentbranch(self):
936 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
936 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
937 if err:
937 if err:
938 current = None
938 current = None
939 return current
939 return current
940
940
941 def _gitremote(self, remote):
941 def _gitremote(self, remote):
942 out = self._gitcommand(['remote', 'show', '-n', remote])
942 out = self._gitcommand(['remote', 'show', '-n', remote])
943 line = out.split('\n')[1]
943 line = out.split('\n')[1]
944 i = line.index('URL: ') + len('URL: ')
944 i = line.index('URL: ') + len('URL: ')
945 return line[i:]
945 return line[i:]
946
946
947 def _githavelocally(self, revision):
947 def _githavelocally(self, revision):
948 out, code = self._gitdir(['cat-file', '-e', revision])
948 out, code = self._gitdir(['cat-file', '-e', revision])
949 return code == 0
949 return code == 0
950
950
951 def _gitisancestor(self, r1, r2):
951 def _gitisancestor(self, r1, r2):
952 base = self._gitcommand(['merge-base', r1, r2])
952 base = self._gitcommand(['merge-base', r1, r2])
953 return base == r1
953 return base == r1
954
954
955 def _gitisbare(self):
955 def _gitisbare(self):
956 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
956 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
957
957
958 def _gitupdatestat(self):
958 def _gitupdatestat(self):
959 """This must be run before git diff-index.
959 """This must be run before git diff-index.
960 diff-index only looks at changes to file stat;
960 diff-index only looks at changes to file stat;
961 this command looks at file contents and updates the stat."""
961 this command looks at file contents and updates the stat."""
962 self._gitcommand(['update-index', '-q', '--refresh'])
962 self._gitcommand(['update-index', '-q', '--refresh'])
963
963
964 def _gitbranchmap(self):
964 def _gitbranchmap(self):
965 '''returns 2 things:
965 '''returns 2 things:
966 a map from git branch to revision
966 a map from git branch to revision
967 a map from revision to branches'''
967 a map from revision to branches'''
968 branch2rev = {}
968 branch2rev = {}
969 rev2branch = {}
969 rev2branch = {}
970
970
971 out = self._gitcommand(['for-each-ref', '--format',
971 out = self._gitcommand(['for-each-ref', '--format',
972 '%(objectname) %(refname)'])
972 '%(objectname) %(refname)'])
973 for line in out.split('\n'):
973 for line in out.split('\n'):
974 revision, ref = line.split(' ')
974 revision, ref = line.split(' ')
975 if (not ref.startswith('refs/heads/') and
975 if (not ref.startswith('refs/heads/') and
976 not ref.startswith('refs/remotes/')):
976 not ref.startswith('refs/remotes/')):
977 continue
977 continue
978 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
978 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
979 continue # ignore remote/HEAD redirects
979 continue # ignore remote/HEAD redirects
980 branch2rev[ref] = revision
980 branch2rev[ref] = revision
981 rev2branch.setdefault(revision, []).append(ref)
981 rev2branch.setdefault(revision, []).append(ref)
982 return branch2rev, rev2branch
982 return branch2rev, rev2branch
983
983
984 def _gittracking(self, branches):
984 def _gittracking(self, branches):
985 'return map of remote branch to local tracking branch'
985 'return map of remote branch to local tracking branch'
986 # assumes no more than one local tracking branch for each remote
986 # assumes no more than one local tracking branch for each remote
987 tracking = {}
987 tracking = {}
988 for b in branches:
988 for b in branches:
989 if b.startswith('refs/remotes/'):
989 if b.startswith('refs/remotes/'):
990 continue
990 continue
991 bname = b.split('/', 2)[2]
991 bname = b.split('/', 2)[2]
992 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
992 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
993 if remote:
993 if remote:
994 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
994 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
995 tracking['refs/remotes/%s/%s' %
995 tracking['refs/remotes/%s/%s' %
996 (remote, ref.split('/', 2)[2])] = b
996 (remote, ref.split('/', 2)[2])] = b
997 return tracking
997 return tracking
998
998
999 def _abssource(self, source):
999 def _abssource(self, source):
1000 if '://' not in source:
1000 if '://' not in source:
1001 # recognize the scp syntax as an absolute source
1001 # recognize the scp syntax as an absolute source
1002 colon = source.find(':')
1002 colon = source.find(':')
1003 if colon != -1 and '/' not in source[:colon]:
1003 if colon != -1 and '/' not in source[:colon]:
1004 return source
1004 return source
1005 self._subsource = source
1005 self._subsource = source
1006 return _abssource(self)
1006 return _abssource(self)
1007
1007
1008 def _fetch(self, source, revision):
1008 def _fetch(self, source, revision):
1009 if self._gitmissing():
1009 if self._gitmissing():
1010 source = self._abssource(source)
1010 source = self._abssource(source)
1011 self._ui.status(_('cloning subrepo %s from %s\n') %
1011 self._ui.status(_('cloning subrepo %s from %s\n') %
1012 (self._relpath, source))
1012 (self._relpath, source))
1013 self._gitnodir(['clone', source, self._abspath])
1013 self._gitnodir(['clone', source, self._abspath])
1014 if self._githavelocally(revision):
1014 if self._githavelocally(revision):
1015 return
1015 return
1016 self._ui.status(_('pulling subrepo %s from %s\n') %
1016 self._ui.status(_('pulling subrepo %s from %s\n') %
1017 (self._relpath, self._gitremote('origin')))
1017 (self._relpath, self._gitremote('origin')))
1018 # try only origin: the originally cloned repo
1018 # try only origin: the originally cloned repo
1019 self._gitcommand(['fetch'])
1019 self._gitcommand(['fetch'])
1020 if not self._githavelocally(revision):
1020 if not self._githavelocally(revision):
1021 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1021 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1022 (revision, self._relpath))
1022 (revision, self._relpath))
1023
1023
1024 def dirty(self, ignoreupdate=False):
1024 def dirty(self, ignoreupdate=False):
1025 if self._gitmissing():
1025 if self._gitmissing():
1026 return self._state[1] != ''
1026 return self._state[1] != ''
1027 if self._gitisbare():
1027 if self._gitisbare():
1028 return True
1028 return True
1029 if not ignoreupdate and self._state[1] != self._gitstate():
1029 if not ignoreupdate and self._state[1] != self._gitstate():
1030 # different version checked out
1030 # different version checked out
1031 return True
1031 return True
1032 # check for staged changes or modified files; ignore untracked files
1032 # check for staged changes or modified files; ignore untracked files
1033 self._gitupdatestat()
1033 self._gitupdatestat()
1034 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1034 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1035 return code == 1
1035 return code == 1
1036
1036
1037 def basestate(self):
1037 def basestate(self):
1038 return self._gitstate()
1038 return self._gitstate()
1039
1039
1040 def get(self, state, overwrite=False):
1040 def get(self, state, overwrite=False):
1041 source, revision, kind = state
1041 source, revision, kind = state
1042 if not revision:
1042 if not revision:
1043 self.remove()
1043 self.remove()
1044 return
1044 return
1045 self._fetch(source, revision)
1045 self._fetch(source, revision)
1046 # if the repo was set to be bare, unbare it
1046 # if the repo was set to be bare, unbare it
1047 if self._gitisbare():
1047 if self._gitisbare():
1048 self._gitcommand(['config', 'core.bare', 'false'])
1048 self._gitcommand(['config', 'core.bare', 'false'])
1049 if self._gitstate() == revision:
1049 if self._gitstate() == revision:
1050 self._gitcommand(['reset', '--hard', 'HEAD'])
1050 self._gitcommand(['reset', '--hard', 'HEAD'])
1051 return
1051 return
1052 elif self._gitstate() == revision:
1052 elif self._gitstate() == revision:
1053 if overwrite:
1053 if overwrite:
1054 # first reset the index to unmark new files for commit, because
1054 # first reset the index to unmark new files for commit, because
1055 # reset --hard will otherwise throw away files added for commit,
1055 # reset --hard will otherwise throw away files added for commit,
1056 # not just unmark them.
1056 # not just unmark them.
1057 self._gitcommand(['reset', 'HEAD'])
1057 self._gitcommand(['reset', 'HEAD'])
1058 self._gitcommand(['reset', '--hard', 'HEAD'])
1058 self._gitcommand(['reset', '--hard', 'HEAD'])
1059 return
1059 return
1060 branch2rev, rev2branch = self._gitbranchmap()
1060 branch2rev, rev2branch = self._gitbranchmap()
1061
1061
1062 def checkout(args):
1062 def checkout(args):
1063 cmd = ['checkout']
1063 cmd = ['checkout']
1064 if overwrite:
1064 if overwrite:
1065 # first reset the index to unmark new files for commit, because
1065 # first reset the index to unmark new files for commit, because
1066 # the -f option will otherwise throw away files added for
1066 # the -f option will otherwise throw away files added for
1067 # commit, not just unmark them.
1067 # commit, not just unmark them.
1068 self._gitcommand(['reset', 'HEAD'])
1068 self._gitcommand(['reset', 'HEAD'])
1069 cmd.append('-f')
1069 cmd.append('-f')
1070 self._gitcommand(cmd + args)
1070 self._gitcommand(cmd + args)
1071
1071
1072 def rawcheckout():
1072 def rawcheckout():
1073 # no branch to checkout, check it out with no branch
1073 # no branch to checkout, check it out with no branch
1074 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1074 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1075 self._relpath)
1075 self._relpath)
1076 self._ui.warn(_('check out a git branch if you intend '
1076 self._ui.warn(_('check out a git branch if you intend '
1077 'to make changes\n'))
1077 'to make changes\n'))
1078 checkout(['-q', revision])
1078 checkout(['-q', revision])
1079
1079
1080 if revision not in rev2branch:
1080 if revision not in rev2branch:
1081 rawcheckout()
1081 rawcheckout()
1082 return
1082 return
1083 branches = rev2branch[revision]
1083 branches = rev2branch[revision]
1084 firstlocalbranch = None
1084 firstlocalbranch = None
1085 for b in branches:
1085 for b in branches:
1086 if b == 'refs/heads/master':
1086 if b == 'refs/heads/master':
1087 # master trumps all other branches
1087 # master trumps all other branches
1088 checkout(['refs/heads/master'])
1088 checkout(['refs/heads/master'])
1089 return
1089 return
1090 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1090 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1091 firstlocalbranch = b
1091 firstlocalbranch = b
1092 if firstlocalbranch:
1092 if firstlocalbranch:
1093 checkout([firstlocalbranch])
1093 checkout([firstlocalbranch])
1094 return
1094 return
1095
1095
1096 tracking = self._gittracking(branch2rev.keys())
1096 tracking = self._gittracking(branch2rev.keys())
1097 # choose a remote branch already tracked if possible
1097 # choose a remote branch already tracked if possible
1098 remote = branches[0]
1098 remote = branches[0]
1099 if remote not in tracking:
1099 if remote not in tracking:
1100 for b in branches:
1100 for b in branches:
1101 if b in tracking:
1101 if b in tracking:
1102 remote = b
1102 remote = b
1103 break
1103 break
1104
1104
1105 if remote not in tracking:
1105 if remote not in tracking:
1106 # create a new local tracking branch
1106 # create a new local tracking branch
1107 local = remote.split('/', 2)[2]
1107 local = remote.split('/', 2)[2]
1108 checkout(['-b', local, remote])
1108 checkout(['-b', local, remote])
1109 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1109 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1110 # When updating to a tracked remote branch,
1110 # When updating to a tracked remote branch,
1111 # if the local tracking branch is downstream of it,
1111 # if the local tracking branch is downstream of it,
1112 # a normal `git pull` would have performed a "fast-forward merge"
1112 # a normal `git pull` would have performed a "fast-forward merge"
1113 # which is equivalent to updating the local branch to the remote.
1113 # which is equivalent to updating the local branch to the remote.
1114 # Since we are only looking at branching at update, we need to
1114 # Since we are only looking at branching at update, we need to
1115 # detect this situation and perform this action lazily.
1115 # detect this situation and perform this action lazily.
1116 if tracking[remote] != self._gitcurrentbranch():
1116 if tracking[remote] != self._gitcurrentbranch():
1117 checkout([tracking[remote]])
1117 checkout([tracking[remote]])
1118 self._gitcommand(['merge', '--ff', remote])
1118 self._gitcommand(['merge', '--ff', remote])
1119 else:
1119 else:
1120 # a real merge would be required, just checkout the revision
1120 # a real merge would be required, just checkout the revision
1121 rawcheckout()
1121 rawcheckout()
1122
1122
1123 def commit(self, text, user, date):
1123 def commit(self, text, user, date):
1124 if self._gitmissing():
1124 if self._gitmissing():
1125 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1125 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1126 cmd = ['commit', '-a', '-m', text]
1126 cmd = ['commit', '-a', '-m', text]
1127 env = os.environ.copy()
1127 env = os.environ.copy()
1128 if user:
1128 if user:
1129 cmd += ['--author', user]
1129 cmd += ['--author', user]
1130 if date:
1130 if date:
1131 # git's date parser silently ignores when seconds < 1e9
1131 # git's date parser silently ignores when seconds < 1e9
1132 # convert to ISO8601
1132 # convert to ISO8601
1133 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1133 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1134 '%Y-%m-%dT%H:%M:%S %1%2')
1134 '%Y-%m-%dT%H:%M:%S %1%2')
1135 self._gitcommand(cmd, env=env)
1135 self._gitcommand(cmd, env=env)
1136 # make sure commit works otherwise HEAD might not exist under certain
1136 # make sure commit works otherwise HEAD might not exist under certain
1137 # circumstances
1137 # circumstances
1138 return self._gitstate()
1138 return self._gitstate()
1139
1139
1140 def merge(self, state):
1140 def merge(self, state):
1141 source, revision, kind = state
1141 source, revision, kind = state
1142 self._fetch(source, revision)
1142 self._fetch(source, revision)
1143 base = self._gitcommand(['merge-base', revision, self._state[1]])
1143 base = self._gitcommand(['merge-base', revision, self._state[1]])
1144 self._gitupdatestat()
1144 self._gitupdatestat()
1145 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1145 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1146
1146
1147 def mergefunc():
1147 def mergefunc():
1148 if base == revision:
1148 if base == revision:
1149 self.get(state) # fast forward merge
1149 self.get(state) # fast forward merge
1150 elif base != self._state[1]:
1150 elif base != self._state[1]:
1151 self._gitcommand(['merge', '--no-commit', revision])
1151 self._gitcommand(['merge', '--no-commit', revision])
1152
1152
1153 if self.dirty():
1153 if self.dirty():
1154 if self._gitstate() != revision:
1154 if self._gitstate() != revision:
1155 dirty = self._gitstate() == self._state[1] or code != 0
1155 dirty = self._gitstate() == self._state[1] or code != 0
1156 if _updateprompt(self._ui, self, dirty,
1156 if _updateprompt(self._ui, self, dirty,
1157 self._state[1][:7], revision[:7]):
1157 self._state[1][:7], revision[:7]):
1158 mergefunc()
1158 mergefunc()
1159 else:
1159 else:
1160 mergefunc()
1160 mergefunc()
1161
1161
1162 def push(self, opts):
1162 def push(self, opts):
1163 force = opts.get('force')
1163 force = opts.get('force')
1164
1164
1165 if not self._state[1]:
1165 if not self._state[1]:
1166 return True
1166 return True
1167 if self._gitmissing():
1167 if self._gitmissing():
1168 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1168 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1169 # if a branch in origin contains the revision, nothing to do
1169 # if a branch in origin contains the revision, nothing to do
1170 branch2rev, rev2branch = self._gitbranchmap()
1170 branch2rev, rev2branch = self._gitbranchmap()
1171 if self._state[1] in rev2branch:
1171 if self._state[1] in rev2branch:
1172 for b in rev2branch[self._state[1]]:
1172 for b in rev2branch[self._state[1]]:
1173 if b.startswith('refs/remotes/origin/'):
1173 if b.startswith('refs/remotes/origin/'):
1174 return True
1174 return True
1175 for b, revision in branch2rev.iteritems():
1175 for b, revision in branch2rev.iteritems():
1176 if b.startswith('refs/remotes/origin/'):
1176 if b.startswith('refs/remotes/origin/'):
1177 if self._gitisancestor(self._state[1], revision):
1177 if self._gitisancestor(self._state[1], revision):
1178 return True
1178 return True
1179 # otherwise, try to push the currently checked out branch
1179 # otherwise, try to push the currently checked out branch
1180 cmd = ['push']
1180 cmd = ['push']
1181 if force:
1181 if force:
1182 cmd.append('--force')
1182 cmd.append('--force')
1183
1183
1184 current = self._gitcurrentbranch()
1184 current = self._gitcurrentbranch()
1185 if current:
1185 if current:
1186 # determine if the current branch is even useful
1186 # determine if the current branch is even useful
1187 if not self._gitisancestor(self._state[1], current):
1187 if not self._gitisancestor(self._state[1], current):
1188 self._ui.warn(_('unrelated git branch checked out '
1188 self._ui.warn(_('unrelated git branch checked out '
1189 'in subrepo %s\n') % self._relpath)
1189 'in subrepo %s\n') % self._relpath)
1190 return False
1190 return False
1191 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1191 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1192 (current.split('/', 2)[2], self._relpath))
1192 (current.split('/', 2)[2], self._relpath))
1193 self._gitcommand(cmd + ['origin', current])
1193 self._gitcommand(cmd + ['origin', current])
1194 return True
1194 return True
1195 else:
1195 else:
1196 self._ui.warn(_('no branch checked out in subrepo %s\n'
1196 self._ui.warn(_('no branch checked out in subrepo %s\n'
1197 'cannot push revision %s\n') %
1197 'cannot push revision %s\n') %
1198 (self._relpath, self._state[1]))
1198 (self._relpath, self._state[1]))
1199 return False
1199 return False
1200
1200
1201 def remove(self):
1201 def remove(self):
1202 if self._gitmissing():
1202 if self._gitmissing():
1203 return
1203 return
1204 if self.dirty():
1204 if self.dirty():
1205 self._ui.warn(_('not removing repo %s because '
1205 self._ui.warn(_('not removing repo %s because '
1206 'it has changes.\n') % self._relpath)
1206 'it has changes.\n') % self._relpath)
1207 return
1207 return
1208 # we can't fully delete the repository as it may contain
1208 # we can't fully delete the repository as it may contain
1209 # local-only history
1209 # local-only history
1210 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1210 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1211 self._gitcommand(['config', 'core.bare', 'true'])
1211 self._gitcommand(['config', 'core.bare', 'true'])
1212 for f in os.listdir(self._abspath):
1212 for f in os.listdir(self._abspath):
1213 if f == '.git':
1213 if f == '.git':
1214 continue
1214 continue
1215 path = os.path.join(self._abspath, f)
1215 path = os.path.join(self._abspath, f)
1216 if os.path.isdir(path) and not os.path.islink(path):
1216 if os.path.isdir(path) and not os.path.islink(path):
1217 shutil.rmtree(path)
1217 shutil.rmtree(path)
1218 else:
1218 else:
1219 os.remove(path)
1219 os.remove(path)
1220
1220
1221 def archive(self, ui, archiver, prefix, match=None):
1221 def archive(self, ui, archiver, prefix, match=None):
1222 source, revision = self._state
1222 source, revision = self._state
1223 if not revision:
1223 if not revision:
1224 return
1224 return
1225 self._fetch(source, revision)
1225 self._fetch(source, revision)
1226
1226
1227 # Parse git's native archive command.
1227 # Parse git's native archive command.
1228 # This should be much faster than manually traversing the trees
1228 # This should be much faster than manually traversing the trees
1229 # and objects with many subprocess calls.
1229 # and objects with many subprocess calls.
1230 tarstream = self._gitcommand(['archive', revision], stream=True)
1230 tarstream = self._gitcommand(['archive', revision], stream=True)
1231 tar = tarfile.open(fileobj=tarstream, mode='r|')
1231 tar = tarfile.open(fileobj=tarstream, mode='r|')
1232 relpath = subrelpath(self)
1232 relpath = subrelpath(self)
1233 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1233 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1234 for i, info in enumerate(tar):
1234 for i, info in enumerate(tar):
1235 if info.isdir():
1235 if info.isdir():
1236 continue
1236 continue
1237 if match and not match(info.name):
1237 if match and not match(info.name):
1238 continue
1238 continue
1239 if info.issym():
1239 if info.issym():
1240 data = info.linkname
1240 data = info.linkname
1241 else:
1241 else:
1242 data = tar.extractfile(info).read()
1242 data = tar.extractfile(info).read()
1243 archiver.addfile(os.path.join(prefix, self._path, info.name),
1243 archiver.addfile(os.path.join(prefix, self._path, info.name),
1244 info.mode, info.issym(), data)
1244 info.mode, info.issym(), data)
1245 ui.progress(_('archiving (%s)') % relpath, i + 1,
1245 ui.progress(_('archiving (%s)') % relpath, i + 1,
1246 unit=_('files'))
1246 unit=_('files'))
1247 ui.progress(_('archiving (%s)') % relpath, None)
1247 ui.progress(_('archiving (%s)') % relpath, None)
1248
1248
1249
1249
1250 def status(self, rev2, **opts):
1250 def status(self, rev2, **opts):
1251 rev1 = self._state[1]
1251 rev1 = self._state[1]
1252 if self._gitmissing() or not rev1:
1252 if self._gitmissing() or not rev1:
1253 # if the repo is missing, return no results
1253 # if the repo is missing, return no results
1254 return [], [], [], [], [], [], []
1254 return [], [], [], [], [], [], []
1255 modified, added, removed = [], [], []
1255 modified, added, removed = [], [], []
1256 self._gitupdatestat()
1256 self._gitupdatestat()
1257 if rev2:
1257 if rev2:
1258 command = ['diff-tree', rev1, rev2]
1258 command = ['diff-tree', rev1, rev2]
1259 else:
1259 else:
1260 command = ['diff-index', rev1]
1260 command = ['diff-index', rev1]
1261 out = self._gitcommand(command)
1261 out = self._gitcommand(command)
1262 for line in out.split('\n'):
1262 for line in out.split('\n'):
1263 tab = line.find('\t')
1263 tab = line.find('\t')
1264 if tab == -1:
1264 if tab == -1:
1265 continue
1265 continue
1266 status, f = line[tab - 1], line[tab + 1:]
1266 status, f = line[tab - 1], line[tab + 1:]
1267 if status == 'M':
1267 if status == 'M':
1268 modified.append(f)
1268 modified.append(f)
1269 elif status == 'A':
1269 elif status == 'A':
1270 added.append(f)
1270 added.append(f)
1271 elif status == 'D':
1271 elif status == 'D':
1272 removed.append(f)
1272 removed.append(f)
1273
1273
1274 deleted = unknown = ignored = clean = []
1274 deleted = unknown = ignored = clean = []
1275 return modified, added, removed, deleted, unknown, ignored, clean
1275 return modified, added, removed, deleted, unknown, ignored, clean
1276
1276
1277 types = {
1277 types = {
1278 'hg': hgsubrepo,
1278 'hg': hgsubrepo,
1279 'svn': svnsubrepo,
1279 'svn': svnsubrepo,
1280 'git': gitsubrepo,
1280 'git': gitsubrepo,
1281 }
1281 }
General Comments 0
You need to be logged in to leave comments. Login now