##// END OF EJS Templates
py3: slice over bytes to prevent getting ascii values...
Pulkit Goyal -
r38389:c7eb9bce default
parent child Browse files
Show More
@@ -1,766 +1,767 b''
1 # Patch transplanting extension for Mercurial
1 # Patch transplanting extension for Mercurial
2 #
2 #
3 # Copyright 2006, 2007 Brendan Cully <brendan@kublai.com>
3 # Copyright 2006, 2007 Brendan Cully <brendan@kublai.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 '''command to transplant changesets from another branch
8 '''command to transplant changesets from another branch
9
9
10 This extension allows you to transplant changes to another parent revision,
10 This extension allows you to transplant changes to another parent revision,
11 possibly in another repository. The transplant is done using 'diff' patches.
11 possibly in another repository. The transplant is done using 'diff' patches.
12
12
13 Transplanted patches are recorded in .hg/transplant/transplants, as a
13 Transplanted patches are recorded in .hg/transplant/transplants, as a
14 map from a changeset hash to its hash in the source repository.
14 map from a changeset hash to its hash in the source repository.
15 '''
15 '''
16 from __future__ import absolute_import
16 from __future__ import absolute_import
17
17
18 import os
18 import os
19
19
20 from mercurial.i18n import _
20 from mercurial.i18n import _
21 from mercurial import (
21 from mercurial import (
22 bundlerepo,
22 bundlerepo,
23 cmdutil,
23 cmdutil,
24 error,
24 error,
25 exchange,
25 exchange,
26 hg,
26 hg,
27 logcmdutil,
27 logcmdutil,
28 match,
28 match,
29 merge,
29 merge,
30 node as nodemod,
30 node as nodemod,
31 patch,
31 patch,
32 pycompat,
32 pycompat,
33 registrar,
33 registrar,
34 revlog,
34 revlog,
35 revset,
35 revset,
36 scmutil,
36 scmutil,
37 smartset,
37 smartset,
38 util,
38 util,
39 vfs as vfsmod,
39 vfs as vfsmod,
40 )
40 )
41 from mercurial.utils import (
41 from mercurial.utils import (
42 procutil,
42 procutil,
43 stringutil,
43 stringutil,
44 )
44 )
45
45
46 class TransplantError(error.Abort):
46 class TransplantError(error.Abort):
47 pass
47 pass
48
48
49 cmdtable = {}
49 cmdtable = {}
50 command = registrar.command(cmdtable)
50 command = registrar.command(cmdtable)
51 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
51 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
52 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
52 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
53 # be specifying the version(s) of Mercurial they are tested with, or
53 # be specifying the version(s) of Mercurial they are tested with, or
54 # leave the attribute unspecified.
54 # leave the attribute unspecified.
55 testedwith = 'ships-with-hg-core'
55 testedwith = 'ships-with-hg-core'
56
56
57 configtable = {}
57 configtable = {}
58 configitem = registrar.configitem(configtable)
58 configitem = registrar.configitem(configtable)
59
59
60 configitem('transplant', 'filter',
60 configitem('transplant', 'filter',
61 default=None,
61 default=None,
62 )
62 )
63 configitem('transplant', 'log',
63 configitem('transplant', 'log',
64 default=None,
64 default=None,
65 )
65 )
66
66
67 class transplantentry(object):
67 class transplantentry(object):
68 def __init__(self, lnode, rnode):
68 def __init__(self, lnode, rnode):
69 self.lnode = lnode
69 self.lnode = lnode
70 self.rnode = rnode
70 self.rnode = rnode
71
71
72 class transplants(object):
72 class transplants(object):
73 def __init__(self, path=None, transplantfile=None, opener=None):
73 def __init__(self, path=None, transplantfile=None, opener=None):
74 self.path = path
74 self.path = path
75 self.transplantfile = transplantfile
75 self.transplantfile = transplantfile
76 self.opener = opener
76 self.opener = opener
77
77
78 if not opener:
78 if not opener:
79 self.opener = vfsmod.vfs(self.path)
79 self.opener = vfsmod.vfs(self.path)
80 self.transplants = {}
80 self.transplants = {}
81 self.dirty = False
81 self.dirty = False
82 self.read()
82 self.read()
83
83
84 def read(self):
84 def read(self):
85 abspath = os.path.join(self.path, self.transplantfile)
85 abspath = os.path.join(self.path, self.transplantfile)
86 if self.transplantfile and os.path.exists(abspath):
86 if self.transplantfile and os.path.exists(abspath):
87 for line in self.opener.read(self.transplantfile).splitlines():
87 for line in self.opener.read(self.transplantfile).splitlines():
88 lnode, rnode = map(revlog.bin, line.split(':'))
88 lnode, rnode = map(revlog.bin, line.split(':'))
89 list = self.transplants.setdefault(rnode, [])
89 list = self.transplants.setdefault(rnode, [])
90 list.append(transplantentry(lnode, rnode))
90 list.append(transplantentry(lnode, rnode))
91
91
92 def write(self):
92 def write(self):
93 if self.dirty and self.transplantfile:
93 if self.dirty and self.transplantfile:
94 if not os.path.isdir(self.path):
94 if not os.path.isdir(self.path):
95 os.mkdir(self.path)
95 os.mkdir(self.path)
96 fp = self.opener(self.transplantfile, 'w')
96 fp = self.opener(self.transplantfile, 'w')
97 for list in self.transplants.itervalues():
97 for list in self.transplants.itervalues():
98 for t in list:
98 for t in list:
99 l, r = map(nodemod.hex, (t.lnode, t.rnode))
99 l, r = map(nodemod.hex, (t.lnode, t.rnode))
100 fp.write(l + ':' + r + '\n')
100 fp.write(l + ':' + r + '\n')
101 fp.close()
101 fp.close()
102 self.dirty = False
102 self.dirty = False
103
103
104 def get(self, rnode):
104 def get(self, rnode):
105 return self.transplants.get(rnode) or []
105 return self.transplants.get(rnode) or []
106
106
107 def set(self, lnode, rnode):
107 def set(self, lnode, rnode):
108 list = self.transplants.setdefault(rnode, [])
108 list = self.transplants.setdefault(rnode, [])
109 list.append(transplantentry(lnode, rnode))
109 list.append(transplantentry(lnode, rnode))
110 self.dirty = True
110 self.dirty = True
111
111
112 def remove(self, transplant):
112 def remove(self, transplant):
113 list = self.transplants.get(transplant.rnode)
113 list = self.transplants.get(transplant.rnode)
114 if list:
114 if list:
115 del list[list.index(transplant)]
115 del list[list.index(transplant)]
116 self.dirty = True
116 self.dirty = True
117
117
118 class transplanter(object):
118 class transplanter(object):
119 def __init__(self, ui, repo, opts):
119 def __init__(self, ui, repo, opts):
120 self.ui = ui
120 self.ui = ui
121 self.path = repo.vfs.join('transplant')
121 self.path = repo.vfs.join('transplant')
122 self.opener = vfsmod.vfs(self.path)
122 self.opener = vfsmod.vfs(self.path)
123 self.transplants = transplants(self.path, 'transplants',
123 self.transplants = transplants(self.path, 'transplants',
124 opener=self.opener)
124 opener=self.opener)
125 def getcommiteditor():
125 def getcommiteditor():
126 editform = cmdutil.mergeeditform(repo[None], 'transplant')
126 editform = cmdutil.mergeeditform(repo[None], 'transplant')
127 return cmdutil.getcommiteditor(editform=editform,
127 return cmdutil.getcommiteditor(editform=editform,
128 **pycompat.strkwargs(opts))
128 **pycompat.strkwargs(opts))
129 self.getcommiteditor = getcommiteditor
129 self.getcommiteditor = getcommiteditor
130
130
131 def applied(self, repo, node, parent):
131 def applied(self, repo, node, parent):
132 '''returns True if a node is already an ancestor of parent
132 '''returns True if a node is already an ancestor of parent
133 or is parent or has already been transplanted'''
133 or is parent or has already been transplanted'''
134 if hasnode(repo, parent):
134 if hasnode(repo, parent):
135 parentrev = repo.changelog.rev(parent)
135 parentrev = repo.changelog.rev(parent)
136 if hasnode(repo, node):
136 if hasnode(repo, node):
137 rev = repo.changelog.rev(node)
137 rev = repo.changelog.rev(node)
138 reachable = repo.changelog.ancestors([parentrev], rev,
138 reachable = repo.changelog.ancestors([parentrev], rev,
139 inclusive=True)
139 inclusive=True)
140 if rev in reachable:
140 if rev in reachable:
141 return True
141 return True
142 for t in self.transplants.get(node):
142 for t in self.transplants.get(node):
143 # it might have been stripped
143 # it might have been stripped
144 if not hasnode(repo, t.lnode):
144 if not hasnode(repo, t.lnode):
145 self.transplants.remove(t)
145 self.transplants.remove(t)
146 return False
146 return False
147 lnoderev = repo.changelog.rev(t.lnode)
147 lnoderev = repo.changelog.rev(t.lnode)
148 if lnoderev in repo.changelog.ancestors([parentrev], lnoderev,
148 if lnoderev in repo.changelog.ancestors([parentrev], lnoderev,
149 inclusive=True):
149 inclusive=True):
150 return True
150 return True
151 return False
151 return False
152
152
153 def apply(self, repo, source, revmap, merges, opts=None):
153 def apply(self, repo, source, revmap, merges, opts=None):
154 '''apply the revisions in revmap one by one in revision order'''
154 '''apply the revisions in revmap one by one in revision order'''
155 if opts is None:
155 if opts is None:
156 opts = {}
156 opts = {}
157 revs = sorted(revmap)
157 revs = sorted(revmap)
158 p1, p2 = repo.dirstate.parents()
158 p1, p2 = repo.dirstate.parents()
159 pulls = []
159 pulls = []
160 diffopts = patch.difffeatureopts(self.ui, opts)
160 diffopts = patch.difffeatureopts(self.ui, opts)
161 diffopts.git = True
161 diffopts.git = True
162
162
163 lock = tr = None
163 lock = tr = None
164 try:
164 try:
165 lock = repo.lock()
165 lock = repo.lock()
166 tr = repo.transaction('transplant')
166 tr = repo.transaction('transplant')
167 for rev in revs:
167 for rev in revs:
168 node = revmap[rev]
168 node = revmap[rev]
169 revstr = '%d:%s' % (rev, nodemod.short(node))
169 revstr = '%d:%s' % (rev, nodemod.short(node))
170
170
171 if self.applied(repo, node, p1):
171 if self.applied(repo, node, p1):
172 self.ui.warn(_('skipping already applied revision %s\n') %
172 self.ui.warn(_('skipping already applied revision %s\n') %
173 revstr)
173 revstr)
174 continue
174 continue
175
175
176 parents = source.changelog.parents(node)
176 parents = source.changelog.parents(node)
177 if not (opts.get('filter') or opts.get('log')):
177 if not (opts.get('filter') or opts.get('log')):
178 # If the changeset parent is the same as the
178 # If the changeset parent is the same as the
179 # wdir's parent, just pull it.
179 # wdir's parent, just pull it.
180 if parents[0] == p1:
180 if parents[0] == p1:
181 pulls.append(node)
181 pulls.append(node)
182 p1 = node
182 p1 = node
183 continue
183 continue
184 if pulls:
184 if pulls:
185 if source != repo:
185 if source != repo:
186 exchange.pull(repo, source.peer(), heads=pulls)
186 exchange.pull(repo, source.peer(), heads=pulls)
187 merge.update(repo, pulls[-1], False, False)
187 merge.update(repo, pulls[-1], False, False)
188 p1, p2 = repo.dirstate.parents()
188 p1, p2 = repo.dirstate.parents()
189 pulls = []
189 pulls = []
190
190
191 domerge = False
191 domerge = False
192 if node in merges:
192 if node in merges:
193 # pulling all the merge revs at once would mean we
193 # pulling all the merge revs at once would mean we
194 # couldn't transplant after the latest even if
194 # couldn't transplant after the latest even if
195 # transplants before them fail.
195 # transplants before them fail.
196 domerge = True
196 domerge = True
197 if not hasnode(repo, node):
197 if not hasnode(repo, node):
198 exchange.pull(repo, source.peer(), heads=[node])
198 exchange.pull(repo, source.peer(), heads=[node])
199
199
200 skipmerge = False
200 skipmerge = False
201 if parents[1] != revlog.nullid:
201 if parents[1] != revlog.nullid:
202 if not opts.get('parent'):
202 if not opts.get('parent'):
203 self.ui.note(_('skipping merge changeset %d:%s\n')
203 self.ui.note(_('skipping merge changeset %d:%s\n')
204 % (rev, nodemod.short(node)))
204 % (rev, nodemod.short(node)))
205 skipmerge = True
205 skipmerge = True
206 else:
206 else:
207 parent = source.lookup(opts['parent'])
207 parent = source.lookup(opts['parent'])
208 if parent not in parents:
208 if parent not in parents:
209 raise error.Abort(_('%s is not a parent of %s') %
209 raise error.Abort(_('%s is not a parent of %s') %
210 (nodemod.short(parent),
210 (nodemod.short(parent),
211 nodemod.short(node)))
211 nodemod.short(node)))
212 else:
212 else:
213 parent = parents[0]
213 parent = parents[0]
214
214
215 if skipmerge:
215 if skipmerge:
216 patchfile = None
216 patchfile = None
217 else:
217 else:
218 fd, patchfile = pycompat.mkstemp(prefix='hg-transplant-')
218 fd, patchfile = pycompat.mkstemp(prefix='hg-transplant-')
219 fp = os.fdopen(fd, r'wb')
219 fp = os.fdopen(fd, r'wb')
220 gen = patch.diff(source, parent, node, opts=diffopts)
220 gen = patch.diff(source, parent, node, opts=diffopts)
221 for chunk in gen:
221 for chunk in gen:
222 fp.write(chunk)
222 fp.write(chunk)
223 fp.close()
223 fp.close()
224
224
225 del revmap[rev]
225 del revmap[rev]
226 if patchfile or domerge:
226 if patchfile or domerge:
227 try:
227 try:
228 try:
228 try:
229 n = self.applyone(repo, node,
229 n = self.applyone(repo, node,
230 source.changelog.read(node),
230 source.changelog.read(node),
231 patchfile, merge=domerge,
231 patchfile, merge=domerge,
232 log=opts.get('log'),
232 log=opts.get('log'),
233 filter=opts.get('filter'))
233 filter=opts.get('filter'))
234 except TransplantError:
234 except TransplantError:
235 # Do not rollback, it is up to the user to
235 # Do not rollback, it is up to the user to
236 # fix the merge or cancel everything
236 # fix the merge or cancel everything
237 tr.close()
237 tr.close()
238 raise
238 raise
239 if n and domerge:
239 if n and domerge:
240 self.ui.status(_('%s merged at %s\n') % (revstr,
240 self.ui.status(_('%s merged at %s\n') % (revstr,
241 nodemod.short(n)))
241 nodemod.short(n)))
242 elif n:
242 elif n:
243 self.ui.status(_('%s transplanted to %s\n')
243 self.ui.status(_('%s transplanted to %s\n')
244 % (nodemod.short(node),
244 % (nodemod.short(node),
245 nodemod.short(n)))
245 nodemod.short(n)))
246 finally:
246 finally:
247 if patchfile:
247 if patchfile:
248 os.unlink(patchfile)
248 os.unlink(patchfile)
249 tr.close()
249 tr.close()
250 if pulls:
250 if pulls:
251 exchange.pull(repo, source.peer(), heads=pulls)
251 exchange.pull(repo, source.peer(), heads=pulls)
252 merge.update(repo, pulls[-1], False, False)
252 merge.update(repo, pulls[-1], False, False)
253 finally:
253 finally:
254 self.saveseries(revmap, merges)
254 self.saveseries(revmap, merges)
255 self.transplants.write()
255 self.transplants.write()
256 if tr:
256 if tr:
257 tr.release()
257 tr.release()
258 if lock:
258 if lock:
259 lock.release()
259 lock.release()
260
260
261 def filter(self, filter, node, changelog, patchfile):
261 def filter(self, filter, node, changelog, patchfile):
262 '''arbitrarily rewrite changeset before applying it'''
262 '''arbitrarily rewrite changeset before applying it'''
263
263
264 self.ui.status(_('filtering %s\n') % patchfile)
264 self.ui.status(_('filtering %s\n') % patchfile)
265 user, date, msg = (changelog[1], changelog[2], changelog[4])
265 user, date, msg = (changelog[1], changelog[2], changelog[4])
266 fd, headerfile = pycompat.mkstemp(prefix='hg-transplant-')
266 fd, headerfile = pycompat.mkstemp(prefix='hg-transplant-')
267 fp = os.fdopen(fd, r'wb')
267 fp = os.fdopen(fd, r'wb')
268 fp.write("# HG changeset patch\n")
268 fp.write("# HG changeset patch\n")
269 fp.write("# User %s\n" % user)
269 fp.write("# User %s\n" % user)
270 fp.write("# Date %d %d\n" % date)
270 fp.write("# Date %d %d\n" % date)
271 fp.write(msg + '\n')
271 fp.write(msg + '\n')
272 fp.close()
272 fp.close()
273
273
274 try:
274 try:
275 self.ui.system('%s %s %s' % (filter,
275 self.ui.system('%s %s %s' % (filter,
276 procutil.shellquote(headerfile),
276 procutil.shellquote(headerfile),
277 procutil.shellquote(patchfile)),
277 procutil.shellquote(patchfile)),
278 environ={'HGUSER': changelog[1],
278 environ={'HGUSER': changelog[1],
279 'HGREVISION': nodemod.hex(node),
279 'HGREVISION': nodemod.hex(node),
280 },
280 },
281 onerr=error.Abort, errprefix=_('filter failed'),
281 onerr=error.Abort, errprefix=_('filter failed'),
282 blockedtag='transplant_filter')
282 blockedtag='transplant_filter')
283 user, date, msg = self.parselog(open(headerfile, 'rb'))[1:4]
283 user, date, msg = self.parselog(open(headerfile, 'rb'))[1:4]
284 finally:
284 finally:
285 os.unlink(headerfile)
285 os.unlink(headerfile)
286
286
287 return (user, date, msg)
287 return (user, date, msg)
288
288
289 def applyone(self, repo, node, cl, patchfile, merge=False, log=False,
289 def applyone(self, repo, node, cl, patchfile, merge=False, log=False,
290 filter=None):
290 filter=None):
291 '''apply the patch in patchfile to the repository as a transplant'''
291 '''apply the patch in patchfile to the repository as a transplant'''
292 (manifest, user, (time, timezone), files, message) = cl[:5]
292 (manifest, user, (time, timezone), files, message) = cl[:5]
293 date = "%d %d" % (time, timezone)
293 date = "%d %d" % (time, timezone)
294 extra = {'transplant_source': node}
294 extra = {'transplant_source': node}
295 if filter:
295 if filter:
296 (user, date, message) = self.filter(filter, node, cl, patchfile)
296 (user, date, message) = self.filter(filter, node, cl, patchfile)
297
297
298 if log:
298 if log:
299 # we don't translate messages inserted into commits
299 # we don't translate messages inserted into commits
300 message += '\n(transplanted from %s)' % nodemod.hex(node)
300 message += '\n(transplanted from %s)' % nodemod.hex(node)
301
301
302 self.ui.status(_('applying %s\n') % nodemod.short(node))
302 self.ui.status(_('applying %s\n') % nodemod.short(node))
303 self.ui.note('%s %s\n%s\n' % (user, date, message))
303 self.ui.note('%s %s\n%s\n' % (user, date, message))
304
304
305 if not patchfile and not merge:
305 if not patchfile and not merge:
306 raise error.Abort(_('can only omit patchfile if merging'))
306 raise error.Abort(_('can only omit patchfile if merging'))
307 if patchfile:
307 if patchfile:
308 try:
308 try:
309 files = set()
309 files = set()
310 patch.patch(self.ui, repo, patchfile, files=files, eolmode=None)
310 patch.patch(self.ui, repo, patchfile, files=files, eolmode=None)
311 files = list(files)
311 files = list(files)
312 except Exception as inst:
312 except Exception as inst:
313 seriespath = os.path.join(self.path, 'series')
313 seriespath = os.path.join(self.path, 'series')
314 if os.path.exists(seriespath):
314 if os.path.exists(seriespath):
315 os.unlink(seriespath)
315 os.unlink(seriespath)
316 p1 = repo.dirstate.p1()
316 p1 = repo.dirstate.p1()
317 p2 = node
317 p2 = node
318 self.log(user, date, message, p1, p2, merge=merge)
318 self.log(user, date, message, p1, p2, merge=merge)
319 self.ui.write(stringutil.forcebytestr(inst) + '\n')
319 self.ui.write(stringutil.forcebytestr(inst) + '\n')
320 raise TransplantError(_('fix up the working directory and run '
320 raise TransplantError(_('fix up the working directory and run '
321 'hg transplant --continue'))
321 'hg transplant --continue'))
322 else:
322 else:
323 files = None
323 files = None
324 if merge:
324 if merge:
325 p1, p2 = repo.dirstate.parents()
325 p1, p2 = repo.dirstate.parents()
326 repo.setparents(p1, node)
326 repo.setparents(p1, node)
327 m = match.always(repo.root, '')
327 m = match.always(repo.root, '')
328 else:
328 else:
329 m = match.exact(repo.root, '', files)
329 m = match.exact(repo.root, '', files)
330
330
331 n = repo.commit(message, user, date, extra=extra, match=m,
331 n = repo.commit(message, user, date, extra=extra, match=m,
332 editor=self.getcommiteditor())
332 editor=self.getcommiteditor())
333 if not n:
333 if not n:
334 self.ui.warn(_('skipping emptied changeset %s\n') %
334 self.ui.warn(_('skipping emptied changeset %s\n') %
335 nodemod.short(node))
335 nodemod.short(node))
336 return None
336 return None
337 if not merge:
337 if not merge:
338 self.transplants.set(n, node)
338 self.transplants.set(n, node)
339
339
340 return n
340 return n
341
341
342 def canresume(self):
342 def canresume(self):
343 return os.path.exists(os.path.join(self.path, 'journal'))
343 return os.path.exists(os.path.join(self.path, 'journal'))
344
344
345 def resume(self, repo, source, opts):
345 def resume(self, repo, source, opts):
346 '''recover last transaction and apply remaining changesets'''
346 '''recover last transaction and apply remaining changesets'''
347 if os.path.exists(os.path.join(self.path, 'journal')):
347 if os.path.exists(os.path.join(self.path, 'journal')):
348 n, node = self.recover(repo, source, opts)
348 n, node = self.recover(repo, source, opts)
349 if n:
349 if n:
350 self.ui.status(_('%s transplanted as %s\n') %
350 self.ui.status(_('%s transplanted as %s\n') %
351 (nodemod.short(node),
351 (nodemod.short(node),
352 nodemod.short(n)))
352 nodemod.short(n)))
353 else:
353 else:
354 self.ui.status(_('%s skipped due to empty diff\n')
354 self.ui.status(_('%s skipped due to empty diff\n')
355 % (nodemod.short(node),))
355 % (nodemod.short(node),))
356 seriespath = os.path.join(self.path, 'series')
356 seriespath = os.path.join(self.path, 'series')
357 if not os.path.exists(seriespath):
357 if not os.path.exists(seriespath):
358 self.transplants.write()
358 self.transplants.write()
359 return
359 return
360 nodes, merges = self.readseries()
360 nodes, merges = self.readseries()
361 revmap = {}
361 revmap = {}
362 for n in nodes:
362 for n in nodes:
363 revmap[source.changelog.rev(n)] = n
363 revmap[source.changelog.rev(n)] = n
364 os.unlink(seriespath)
364 os.unlink(seriespath)
365
365
366 self.apply(repo, source, revmap, merges, opts)
366 self.apply(repo, source, revmap, merges, opts)
367
367
368 def recover(self, repo, source, opts):
368 def recover(self, repo, source, opts):
369 '''commit working directory using journal metadata'''
369 '''commit working directory using journal metadata'''
370 node, user, date, message, parents = self.readlog()
370 node, user, date, message, parents = self.readlog()
371 merge = False
371 merge = False
372
372
373 if not user or not date or not message or not parents[0]:
373 if not user or not date or not message or not parents[0]:
374 raise error.Abort(_('transplant log file is corrupt'))
374 raise error.Abort(_('transplant log file is corrupt'))
375
375
376 parent = parents[0]
376 parent = parents[0]
377 if len(parents) > 1:
377 if len(parents) > 1:
378 if opts.get('parent'):
378 if opts.get('parent'):
379 parent = source.lookup(opts['parent'])
379 parent = source.lookup(opts['parent'])
380 if parent not in parents:
380 if parent not in parents:
381 raise error.Abort(_('%s is not a parent of %s') %
381 raise error.Abort(_('%s is not a parent of %s') %
382 (nodemod.short(parent),
382 (nodemod.short(parent),
383 nodemod.short(node)))
383 nodemod.short(node)))
384 else:
384 else:
385 merge = True
385 merge = True
386
386
387 extra = {'transplant_source': node}
387 extra = {'transplant_source': node}
388 try:
388 try:
389 p1, p2 = repo.dirstate.parents()
389 p1, p2 = repo.dirstate.parents()
390 if p1 != parent:
390 if p1 != parent:
391 raise error.Abort(_('working directory not at transplant '
391 raise error.Abort(_('working directory not at transplant '
392 'parent %s') % nodemod.hex(parent))
392 'parent %s') % nodemod.hex(parent))
393 if merge:
393 if merge:
394 repo.setparents(p1, parents[1])
394 repo.setparents(p1, parents[1])
395 modified, added, removed, deleted = repo.status()[:4]
395 modified, added, removed, deleted = repo.status()[:4]
396 if merge or modified or added or removed or deleted:
396 if merge or modified or added or removed or deleted:
397 n = repo.commit(message, user, date, extra=extra,
397 n = repo.commit(message, user, date, extra=extra,
398 editor=self.getcommiteditor())
398 editor=self.getcommiteditor())
399 if not n:
399 if not n:
400 raise error.Abort(_('commit failed'))
400 raise error.Abort(_('commit failed'))
401 if not merge:
401 if not merge:
402 self.transplants.set(n, node)
402 self.transplants.set(n, node)
403 else:
403 else:
404 n = None
404 n = None
405 self.unlog()
405 self.unlog()
406
406
407 return n, node
407 return n, node
408 finally:
408 finally:
409 # TODO: get rid of this meaningless try/finally enclosing.
409 # TODO: get rid of this meaningless try/finally enclosing.
410 # this is kept only to reduce changes in a patch.
410 # this is kept only to reduce changes in a patch.
411 pass
411 pass
412
412
413 def readseries(self):
413 def readseries(self):
414 nodes = []
414 nodes = []
415 merges = []
415 merges = []
416 cur = nodes
416 cur = nodes
417 for line in self.opener.read('series').splitlines():
417 for line in self.opener.read('series').splitlines():
418 if line.startswith('# Merges'):
418 if line.startswith('# Merges'):
419 cur = merges
419 cur = merges
420 continue
420 continue
421 cur.append(revlog.bin(line))
421 cur.append(revlog.bin(line))
422
422
423 return (nodes, merges)
423 return (nodes, merges)
424
424
425 def saveseries(self, revmap, merges):
425 def saveseries(self, revmap, merges):
426 if not revmap:
426 if not revmap:
427 return
427 return
428
428
429 if not os.path.isdir(self.path):
429 if not os.path.isdir(self.path):
430 os.mkdir(self.path)
430 os.mkdir(self.path)
431 series = self.opener('series', 'w')
431 series = self.opener('series', 'w')
432 for rev in sorted(revmap):
432 for rev in sorted(revmap):
433 series.write(nodemod.hex(revmap[rev]) + '\n')
433 series.write(nodemod.hex(revmap[rev]) + '\n')
434 if merges:
434 if merges:
435 series.write('# Merges\n')
435 series.write('# Merges\n')
436 for m in merges:
436 for m in merges:
437 series.write(nodemod.hex(m) + '\n')
437 series.write(nodemod.hex(m) + '\n')
438 series.close()
438 series.close()
439
439
440 def parselog(self, fp):
440 def parselog(self, fp):
441 parents = []
441 parents = []
442 message = []
442 message = []
443 node = revlog.nullid
443 node = revlog.nullid
444 inmsg = False
444 inmsg = False
445 user = None
445 user = None
446 date = None
446 date = None
447 for line in fp.read().splitlines():
447 for line in fp.read().splitlines():
448 if inmsg:
448 if inmsg:
449 message.append(line)
449 message.append(line)
450 elif line.startswith('# User '):
450 elif line.startswith('# User '):
451 user = line[7:]
451 user = line[7:]
452 elif line.startswith('# Date '):
452 elif line.startswith('# Date '):
453 date = line[7:]
453 date = line[7:]
454 elif line.startswith('# Node ID '):
454 elif line.startswith('# Node ID '):
455 node = revlog.bin(line[10:])
455 node = revlog.bin(line[10:])
456 elif line.startswith('# Parent '):
456 elif line.startswith('# Parent '):
457 parents.append(revlog.bin(line[9:]))
457 parents.append(revlog.bin(line[9:]))
458 elif not line.startswith('# '):
458 elif not line.startswith('# '):
459 inmsg = True
459 inmsg = True
460 message.append(line)
460 message.append(line)
461 if None in (user, date):
461 if None in (user, date):
462 raise error.Abort(_("filter corrupted changeset (no user or date)"))
462 raise error.Abort(_("filter corrupted changeset (no user or date)"))
463 return (node, user, date, '\n'.join(message), parents)
463 return (node, user, date, '\n'.join(message), parents)
464
464
465 def log(self, user, date, message, p1, p2, merge=False):
465 def log(self, user, date, message, p1, p2, merge=False):
466 '''journal changelog metadata for later recover'''
466 '''journal changelog metadata for later recover'''
467
467
468 if not os.path.isdir(self.path):
468 if not os.path.isdir(self.path):
469 os.mkdir(self.path)
469 os.mkdir(self.path)
470 fp = self.opener('journal', 'w')
470 fp = self.opener('journal', 'w')
471 fp.write('# User %s\n' % user)
471 fp.write('# User %s\n' % user)
472 fp.write('# Date %s\n' % date)
472 fp.write('# Date %s\n' % date)
473 fp.write('# Node ID %s\n' % nodemod.hex(p2))
473 fp.write('# Node ID %s\n' % nodemod.hex(p2))
474 fp.write('# Parent ' + nodemod.hex(p1) + '\n')
474 fp.write('# Parent ' + nodemod.hex(p1) + '\n')
475 if merge:
475 if merge:
476 fp.write('# Parent ' + nodemod.hex(p2) + '\n')
476 fp.write('# Parent ' + nodemod.hex(p2) + '\n')
477 fp.write(message.rstrip() + '\n')
477 fp.write(message.rstrip() + '\n')
478 fp.close()
478 fp.close()
479
479
480 def readlog(self):
480 def readlog(self):
481 return self.parselog(self.opener('journal'))
481 return self.parselog(self.opener('journal'))
482
482
483 def unlog(self):
483 def unlog(self):
484 '''remove changelog journal'''
484 '''remove changelog journal'''
485 absdst = os.path.join(self.path, 'journal')
485 absdst = os.path.join(self.path, 'journal')
486 if os.path.exists(absdst):
486 if os.path.exists(absdst):
487 os.unlink(absdst)
487 os.unlink(absdst)
488
488
489 def transplantfilter(self, repo, source, root):
489 def transplantfilter(self, repo, source, root):
490 def matchfn(node):
490 def matchfn(node):
491 if self.applied(repo, node, root):
491 if self.applied(repo, node, root):
492 return False
492 return False
493 if source.changelog.parents(node)[1] != revlog.nullid:
493 if source.changelog.parents(node)[1] != revlog.nullid:
494 return False
494 return False
495 extra = source.changelog.read(node)[5]
495 extra = source.changelog.read(node)[5]
496 cnode = extra.get('transplant_source')
496 cnode = extra.get('transplant_source')
497 if cnode and self.applied(repo, cnode, root):
497 if cnode and self.applied(repo, cnode, root):
498 return False
498 return False
499 return True
499 return True
500
500
501 return matchfn
501 return matchfn
502
502
503 def hasnode(repo, node):
503 def hasnode(repo, node):
504 try:
504 try:
505 return repo.changelog.rev(node) is not None
505 return repo.changelog.rev(node) is not None
506 except error.RevlogError:
506 except error.RevlogError:
507 return False
507 return False
508
508
509 def browserevs(ui, repo, nodes, opts):
509 def browserevs(ui, repo, nodes, opts):
510 '''interactively transplant changesets'''
510 '''interactively transplant changesets'''
511 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
511 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
512 transplants = []
512 transplants = []
513 merges = []
513 merges = []
514 prompt = _('apply changeset? [ynmpcq?]:'
514 prompt = _('apply changeset? [ynmpcq?]:'
515 '$$ &yes, transplant this changeset'
515 '$$ &yes, transplant this changeset'
516 '$$ &no, skip this changeset'
516 '$$ &no, skip this changeset'
517 '$$ &merge at this changeset'
517 '$$ &merge at this changeset'
518 '$$ show &patch'
518 '$$ show &patch'
519 '$$ &commit selected changesets'
519 '$$ &commit selected changesets'
520 '$$ &quit and cancel transplant'
520 '$$ &quit and cancel transplant'
521 '$$ &? (show this help)')
521 '$$ &? (show this help)')
522 for node in nodes:
522 for node in nodes:
523 displayer.show(repo[node])
523 displayer.show(repo[node])
524 action = None
524 action = None
525 while not action:
525 while not action:
526 action = 'ynmpcq?'[ui.promptchoice(prompt)]
526 choice = ui.promptchoice(prompt)
527 action = 'ynmpcq?'[choice:choice + 1]
527 if action == '?':
528 if action == '?':
528 for c, t in ui.extractchoices(prompt)[1]:
529 for c, t in ui.extractchoices(prompt)[1]:
529 ui.write('%s: %s\n' % (c, t))
530 ui.write('%s: %s\n' % (c, t))
530 action = None
531 action = None
531 elif action == 'p':
532 elif action == 'p':
532 parent = repo.changelog.parents(node)[0]
533 parent = repo.changelog.parents(node)[0]
533 for chunk in patch.diff(repo, parent, node):
534 for chunk in patch.diff(repo, parent, node):
534 ui.write(chunk)
535 ui.write(chunk)
535 action = None
536 action = None
536 if action == 'y':
537 if action == 'y':
537 transplants.append(node)
538 transplants.append(node)
538 elif action == 'm':
539 elif action == 'm':
539 merges.append(node)
540 merges.append(node)
540 elif action == 'c':
541 elif action == 'c':
541 break
542 break
542 elif action == 'q':
543 elif action == 'q':
543 transplants = ()
544 transplants = ()
544 merges = ()
545 merges = ()
545 break
546 break
546 displayer.close()
547 displayer.close()
547 return (transplants, merges)
548 return (transplants, merges)
548
549
549 @command('transplant',
550 @command('transplant',
550 [('s', 'source', '', _('transplant changesets from REPO'), _('REPO')),
551 [('s', 'source', '', _('transplant changesets from REPO'), _('REPO')),
551 ('b', 'branch', [], _('use this source changeset as head'), _('REV')),
552 ('b', 'branch', [], _('use this source changeset as head'), _('REV')),
552 ('a', 'all', None, _('pull all changesets up to the --branch revisions')),
553 ('a', 'all', None, _('pull all changesets up to the --branch revisions')),
553 ('p', 'prune', [], _('skip over REV'), _('REV')),
554 ('p', 'prune', [], _('skip over REV'), _('REV')),
554 ('m', 'merge', [], _('merge at REV'), _('REV')),
555 ('m', 'merge', [], _('merge at REV'), _('REV')),
555 ('', 'parent', '',
556 ('', 'parent', '',
556 _('parent to choose when transplanting merge'), _('REV')),
557 _('parent to choose when transplanting merge'), _('REV')),
557 ('e', 'edit', False, _('invoke editor on commit messages')),
558 ('e', 'edit', False, _('invoke editor on commit messages')),
558 ('', 'log', None, _('append transplant info to log message')),
559 ('', 'log', None, _('append transplant info to log message')),
559 ('c', 'continue', None, _('continue last transplant session '
560 ('c', 'continue', None, _('continue last transplant session '
560 'after fixing conflicts')),
561 'after fixing conflicts')),
561 ('', 'filter', '',
562 ('', 'filter', '',
562 _('filter changesets through command'), _('CMD'))],
563 _('filter changesets through command'), _('CMD'))],
563 _('hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] '
564 _('hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] '
564 '[-m REV] [REV]...'))
565 '[-m REV] [REV]...'))
565 def transplant(ui, repo, *revs, **opts):
566 def transplant(ui, repo, *revs, **opts):
566 '''transplant changesets from another branch
567 '''transplant changesets from another branch
567
568
568 Selected changesets will be applied on top of the current working
569 Selected changesets will be applied on top of the current working
569 directory with the log of the original changeset. The changesets
570 directory with the log of the original changeset. The changesets
570 are copied and will thus appear twice in the history with different
571 are copied and will thus appear twice in the history with different
571 identities.
572 identities.
572
573
573 Consider using the graft command if everything is inside the same
574 Consider using the graft command if everything is inside the same
574 repository - it will use merges and will usually give a better result.
575 repository - it will use merges and will usually give a better result.
575 Use the rebase extension if the changesets are unpublished and you want
576 Use the rebase extension if the changesets are unpublished and you want
576 to move them instead of copying them.
577 to move them instead of copying them.
577
578
578 If --log is specified, log messages will have a comment appended
579 If --log is specified, log messages will have a comment appended
579 of the form::
580 of the form::
580
581
581 (transplanted from CHANGESETHASH)
582 (transplanted from CHANGESETHASH)
582
583
583 You can rewrite the changelog message with the --filter option.
584 You can rewrite the changelog message with the --filter option.
584 Its argument will be invoked with the current changelog message as
585 Its argument will be invoked with the current changelog message as
585 $1 and the patch as $2.
586 $1 and the patch as $2.
586
587
587 --source/-s specifies another repository to use for selecting changesets,
588 --source/-s specifies another repository to use for selecting changesets,
588 just as if it temporarily had been pulled.
589 just as if it temporarily had been pulled.
589 If --branch/-b is specified, these revisions will be used as
590 If --branch/-b is specified, these revisions will be used as
590 heads when deciding which changesets to transplant, just as if only
591 heads when deciding which changesets to transplant, just as if only
591 these revisions had been pulled.
592 these revisions had been pulled.
592 If --all/-a is specified, all the revisions up to the heads specified
593 If --all/-a is specified, all the revisions up to the heads specified
593 with --branch will be transplanted.
594 with --branch will be transplanted.
594
595
595 Example:
596 Example:
596
597
597 - transplant all changes up to REV on top of your current revision::
598 - transplant all changes up to REV on top of your current revision::
598
599
599 hg transplant --branch REV --all
600 hg transplant --branch REV --all
600
601
601 You can optionally mark selected transplanted changesets as merge
602 You can optionally mark selected transplanted changesets as merge
602 changesets. You will not be prompted to transplant any ancestors
603 changesets. You will not be prompted to transplant any ancestors
603 of a merged transplant, and you can merge descendants of them
604 of a merged transplant, and you can merge descendants of them
604 normally instead of transplanting them.
605 normally instead of transplanting them.
605
606
606 Merge changesets may be transplanted directly by specifying the
607 Merge changesets may be transplanted directly by specifying the
607 proper parent changeset by calling :hg:`transplant --parent`.
608 proper parent changeset by calling :hg:`transplant --parent`.
608
609
609 If no merges or revisions are provided, :hg:`transplant` will
610 If no merges or revisions are provided, :hg:`transplant` will
610 start an interactive changeset browser.
611 start an interactive changeset browser.
611
612
612 If a changeset application fails, you can fix the merge by hand
613 If a changeset application fails, you can fix the merge by hand
613 and then resume where you left off by calling :hg:`transplant
614 and then resume where you left off by calling :hg:`transplant
614 --continue/-c`.
615 --continue/-c`.
615 '''
616 '''
616 with repo.wlock():
617 with repo.wlock():
617 return _dotransplant(ui, repo, *revs, **opts)
618 return _dotransplant(ui, repo, *revs, **opts)
618
619
619 def _dotransplant(ui, repo, *revs, **opts):
620 def _dotransplant(ui, repo, *revs, **opts):
620 def incwalk(repo, csets, match=util.always):
621 def incwalk(repo, csets, match=util.always):
621 for node in csets:
622 for node in csets:
622 if match(node):
623 if match(node):
623 yield node
624 yield node
624
625
625 def transplantwalk(repo, dest, heads, match=util.always):
626 def transplantwalk(repo, dest, heads, match=util.always):
626 '''Yield all nodes that are ancestors of a head but not ancestors
627 '''Yield all nodes that are ancestors of a head but not ancestors
627 of dest.
628 of dest.
628 If no heads are specified, the heads of repo will be used.'''
629 If no heads are specified, the heads of repo will be used.'''
629 if not heads:
630 if not heads:
630 heads = repo.heads()
631 heads = repo.heads()
631 ancestors = []
632 ancestors = []
632 ctx = repo[dest]
633 ctx = repo[dest]
633 for head in heads:
634 for head in heads:
634 ancestors.append(ctx.ancestor(repo[head]).node())
635 ancestors.append(ctx.ancestor(repo[head]).node())
635 for node in repo.changelog.nodesbetween(ancestors, heads)[0]:
636 for node in repo.changelog.nodesbetween(ancestors, heads)[0]:
636 if match(node):
637 if match(node):
637 yield node
638 yield node
638
639
639 def checkopts(opts, revs):
640 def checkopts(opts, revs):
640 if opts.get('continue'):
641 if opts.get('continue'):
641 if opts.get('branch') or opts.get('all') or opts.get('merge'):
642 if opts.get('branch') or opts.get('all') or opts.get('merge'):
642 raise error.Abort(_('--continue is incompatible with '
643 raise error.Abort(_('--continue is incompatible with '
643 '--branch, --all and --merge'))
644 '--branch, --all and --merge'))
644 return
645 return
645 if not (opts.get('source') or revs or
646 if not (opts.get('source') or revs or
646 opts.get('merge') or opts.get('branch')):
647 opts.get('merge') or opts.get('branch')):
647 raise error.Abort(_('no source URL, branch revision, or revision '
648 raise error.Abort(_('no source URL, branch revision, or revision '
648 'list provided'))
649 'list provided'))
649 if opts.get('all'):
650 if opts.get('all'):
650 if not opts.get('branch'):
651 if not opts.get('branch'):
651 raise error.Abort(_('--all requires a branch revision'))
652 raise error.Abort(_('--all requires a branch revision'))
652 if revs:
653 if revs:
653 raise error.Abort(_('--all is incompatible with a '
654 raise error.Abort(_('--all is incompatible with a '
654 'revision list'))
655 'revision list'))
655
656
656 opts = pycompat.byteskwargs(opts)
657 opts = pycompat.byteskwargs(opts)
657 checkopts(opts, revs)
658 checkopts(opts, revs)
658
659
659 if not opts.get('log'):
660 if not opts.get('log'):
660 # deprecated config: transplant.log
661 # deprecated config: transplant.log
661 opts['log'] = ui.config('transplant', 'log')
662 opts['log'] = ui.config('transplant', 'log')
662 if not opts.get('filter'):
663 if not opts.get('filter'):
663 # deprecated config: transplant.filter
664 # deprecated config: transplant.filter
664 opts['filter'] = ui.config('transplant', 'filter')
665 opts['filter'] = ui.config('transplant', 'filter')
665
666
666 tp = transplanter(ui, repo, opts)
667 tp = transplanter(ui, repo, opts)
667
668
668 p1, p2 = repo.dirstate.parents()
669 p1, p2 = repo.dirstate.parents()
669 if len(repo) > 0 and p1 == revlog.nullid:
670 if len(repo) > 0 and p1 == revlog.nullid:
670 raise error.Abort(_('no revision checked out'))
671 raise error.Abort(_('no revision checked out'))
671 if opts.get('continue'):
672 if opts.get('continue'):
672 if not tp.canresume():
673 if not tp.canresume():
673 raise error.Abort(_('no transplant to continue'))
674 raise error.Abort(_('no transplant to continue'))
674 else:
675 else:
675 cmdutil.checkunfinished(repo)
676 cmdutil.checkunfinished(repo)
676 if p2 != revlog.nullid:
677 if p2 != revlog.nullid:
677 raise error.Abort(_('outstanding uncommitted merges'))
678 raise error.Abort(_('outstanding uncommitted merges'))
678 m, a, r, d = repo.status()[:4]
679 m, a, r, d = repo.status()[:4]
679 if m or a or r or d:
680 if m or a or r or d:
680 raise error.Abort(_('outstanding local changes'))
681 raise error.Abort(_('outstanding local changes'))
681
682
682 sourcerepo = opts.get('source')
683 sourcerepo = opts.get('source')
683 if sourcerepo:
684 if sourcerepo:
684 peer = hg.peer(repo, opts, ui.expandpath(sourcerepo))
685 peer = hg.peer(repo, opts, ui.expandpath(sourcerepo))
685 heads = pycompat.maplist(peer.lookup, opts.get('branch', ()))
686 heads = pycompat.maplist(peer.lookup, opts.get('branch', ()))
686 target = set(heads)
687 target = set(heads)
687 for r in revs:
688 for r in revs:
688 try:
689 try:
689 target.add(peer.lookup(r))
690 target.add(peer.lookup(r))
690 except error.RepoError:
691 except error.RepoError:
691 pass
692 pass
692 source, csets, cleanupfn = bundlerepo.getremotechanges(ui, repo, peer,
693 source, csets, cleanupfn = bundlerepo.getremotechanges(ui, repo, peer,
693 onlyheads=sorted(target), force=True)
694 onlyheads=sorted(target), force=True)
694 else:
695 else:
695 source = repo
696 source = repo
696 heads = pycompat.maplist(source.lookup, opts.get('branch', ()))
697 heads = pycompat.maplist(source.lookup, opts.get('branch', ()))
697 cleanupfn = None
698 cleanupfn = None
698
699
699 try:
700 try:
700 if opts.get('continue'):
701 if opts.get('continue'):
701 tp.resume(repo, source, opts)
702 tp.resume(repo, source, opts)
702 return
703 return
703
704
704 tf = tp.transplantfilter(repo, source, p1)
705 tf = tp.transplantfilter(repo, source, p1)
705 if opts.get('prune'):
706 if opts.get('prune'):
706 prune = set(source[r].node()
707 prune = set(source[r].node()
707 for r in scmutil.revrange(source, opts.get('prune')))
708 for r in scmutil.revrange(source, opts.get('prune')))
708 matchfn = lambda x: tf(x) and x not in prune
709 matchfn = lambda x: tf(x) and x not in prune
709 else:
710 else:
710 matchfn = tf
711 matchfn = tf
711 merges = pycompat.maplist(source.lookup, opts.get('merge', ()))
712 merges = pycompat.maplist(source.lookup, opts.get('merge', ()))
712 revmap = {}
713 revmap = {}
713 if revs:
714 if revs:
714 for r in scmutil.revrange(source, revs):
715 for r in scmutil.revrange(source, revs):
715 revmap[int(r)] = source[r].node()
716 revmap[int(r)] = source[r].node()
716 elif opts.get('all') or not merges:
717 elif opts.get('all') or not merges:
717 if source != repo:
718 if source != repo:
718 alltransplants = incwalk(source, csets, match=matchfn)
719 alltransplants = incwalk(source, csets, match=matchfn)
719 else:
720 else:
720 alltransplants = transplantwalk(source, p1, heads,
721 alltransplants = transplantwalk(source, p1, heads,
721 match=matchfn)
722 match=matchfn)
722 if opts.get('all'):
723 if opts.get('all'):
723 revs = alltransplants
724 revs = alltransplants
724 else:
725 else:
725 revs, newmerges = browserevs(ui, source, alltransplants, opts)
726 revs, newmerges = browserevs(ui, source, alltransplants, opts)
726 merges.extend(newmerges)
727 merges.extend(newmerges)
727 for r in revs:
728 for r in revs:
728 revmap[source.changelog.rev(r)] = r
729 revmap[source.changelog.rev(r)] = r
729 for r in merges:
730 for r in merges:
730 revmap[source.changelog.rev(r)] = r
731 revmap[source.changelog.rev(r)] = r
731
732
732 tp.apply(repo, source, revmap, merges, opts)
733 tp.apply(repo, source, revmap, merges, opts)
733 finally:
734 finally:
734 if cleanupfn:
735 if cleanupfn:
735 cleanupfn()
736 cleanupfn()
736
737
737 revsetpredicate = registrar.revsetpredicate()
738 revsetpredicate = registrar.revsetpredicate()
738
739
739 @revsetpredicate('transplanted([set])')
740 @revsetpredicate('transplanted([set])')
740 def revsettransplanted(repo, subset, x):
741 def revsettransplanted(repo, subset, x):
741 """Transplanted changesets in set, or all transplanted changesets.
742 """Transplanted changesets in set, or all transplanted changesets.
742 """
743 """
743 if x:
744 if x:
744 s = revset.getset(repo, subset, x)
745 s = revset.getset(repo, subset, x)
745 else:
746 else:
746 s = subset
747 s = subset
747 return smartset.baseset([r for r in s if
748 return smartset.baseset([r for r in s if
748 repo[r].extra().get('transplant_source')])
749 repo[r].extra().get('transplant_source')])
749
750
750 templatekeyword = registrar.templatekeyword()
751 templatekeyword = registrar.templatekeyword()
751
752
752 @templatekeyword('transplanted', requires={'ctx'})
753 @templatekeyword('transplanted', requires={'ctx'})
753 def kwtransplanted(context, mapping):
754 def kwtransplanted(context, mapping):
754 """String. The node identifier of the transplanted
755 """String. The node identifier of the transplanted
755 changeset if any."""
756 changeset if any."""
756 ctx = context.resource(mapping, 'ctx')
757 ctx = context.resource(mapping, 'ctx')
757 n = ctx.extra().get('transplant_source')
758 n = ctx.extra().get('transplant_source')
758 return n and nodemod.hex(n) or ''
759 return n and nodemod.hex(n) or ''
759
760
760 def extsetup(ui):
761 def extsetup(ui):
761 cmdutil.unfinishedstates.append(
762 cmdutil.unfinishedstates.append(
762 ['transplant/journal', True, False, _('transplant in progress'),
763 ['transplant/journal', True, False, _('transplant in progress'),
763 _("use 'hg transplant --continue' or 'hg update' to abort")])
764 _("use 'hg transplant --continue' or 'hg update' to abort")])
764
765
765 # tell hggettext to extract docstrings from these functions:
766 # tell hggettext to extract docstrings from these functions:
766 i18nfunctions = [revsettransplanted, kwtransplanted]
767 i18nfunctions = [revsettransplanted, kwtransplanted]
@@ -1,1815 +1,1815 b''
1 # subrepo.py - sub-repository classes and factory
1 # subrepo.py - sub-repository classes and factory
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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import copy
10 import copy
11 import errno
11 import errno
12 import hashlib
12 import hashlib
13 import os
13 import os
14 import posixpath
14 import posixpath
15 import re
15 import re
16 import stat
16 import stat
17 import subprocess
17 import subprocess
18 import sys
18 import sys
19 import tarfile
19 import tarfile
20 import xml.dom.minidom
20 import xml.dom.minidom
21
21
22 from .i18n import _
22 from .i18n import _
23 from . import (
23 from . import (
24 cmdutil,
24 cmdutil,
25 encoding,
25 encoding,
26 error,
26 error,
27 exchange,
27 exchange,
28 logcmdutil,
28 logcmdutil,
29 match as matchmod,
29 match as matchmod,
30 node,
30 node,
31 pathutil,
31 pathutil,
32 phases,
32 phases,
33 pycompat,
33 pycompat,
34 scmutil,
34 scmutil,
35 subrepoutil,
35 subrepoutil,
36 util,
36 util,
37 vfs as vfsmod,
37 vfs as vfsmod,
38 )
38 )
39 from .utils import (
39 from .utils import (
40 dateutil,
40 dateutil,
41 procutil,
41 procutil,
42 stringutil,
42 stringutil,
43 )
43 )
44
44
45 hg = None
45 hg = None
46 reporelpath = subrepoutil.reporelpath
46 reporelpath = subrepoutil.reporelpath
47 subrelpath = subrepoutil.subrelpath
47 subrelpath = subrepoutil.subrelpath
48 _abssource = subrepoutil._abssource
48 _abssource = subrepoutil._abssource
49 propertycache = util.propertycache
49 propertycache = util.propertycache
50
50
51 def _expandedabspath(path):
51 def _expandedabspath(path):
52 '''
52 '''
53 get a path or url and if it is a path expand it and return an absolute path
53 get a path or url and if it is a path expand it and return an absolute path
54 '''
54 '''
55 expandedpath = util.urllocalpath(util.expandpath(path))
55 expandedpath = util.urllocalpath(util.expandpath(path))
56 u = util.url(expandedpath)
56 u = util.url(expandedpath)
57 if not u.scheme:
57 if not u.scheme:
58 path = util.normpath(os.path.abspath(u.path))
58 path = util.normpath(os.path.abspath(u.path))
59 return path
59 return path
60
60
61 def _getstorehashcachename(remotepath):
61 def _getstorehashcachename(remotepath):
62 '''get a unique filename for the store hash cache of a remote repository'''
62 '''get a unique filename for the store hash cache of a remote repository'''
63 return node.hex(hashlib.sha1(_expandedabspath(remotepath)).digest())[0:12]
63 return node.hex(hashlib.sha1(_expandedabspath(remotepath)).digest())[0:12]
64
64
65 class SubrepoAbort(error.Abort):
65 class SubrepoAbort(error.Abort):
66 """Exception class used to avoid handling a subrepo error more than once"""
66 """Exception class used to avoid handling a subrepo error more than once"""
67 def __init__(self, *args, **kw):
67 def __init__(self, *args, **kw):
68 self.subrepo = kw.pop(r'subrepo', None)
68 self.subrepo = kw.pop(r'subrepo', None)
69 self.cause = kw.pop(r'cause', None)
69 self.cause = kw.pop(r'cause', None)
70 error.Abort.__init__(self, *args, **kw)
70 error.Abort.__init__(self, *args, **kw)
71
71
72 def annotatesubrepoerror(func):
72 def annotatesubrepoerror(func):
73 def decoratedmethod(self, *args, **kargs):
73 def decoratedmethod(self, *args, **kargs):
74 try:
74 try:
75 res = func(self, *args, **kargs)
75 res = func(self, *args, **kargs)
76 except SubrepoAbort as ex:
76 except SubrepoAbort as ex:
77 # This exception has already been handled
77 # This exception has already been handled
78 raise ex
78 raise ex
79 except error.Abort as ex:
79 except error.Abort as ex:
80 subrepo = subrelpath(self)
80 subrepo = subrelpath(self)
81 errormsg = (stringutil.forcebytestr(ex) + ' '
81 errormsg = (stringutil.forcebytestr(ex) + ' '
82 + _('(in subrepository "%s")') % subrepo)
82 + _('(in subrepository "%s")') % subrepo)
83 # avoid handling this exception by raising a SubrepoAbort exception
83 # avoid handling this exception by raising a SubrepoAbort exception
84 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
84 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
85 cause=sys.exc_info())
85 cause=sys.exc_info())
86 return res
86 return res
87 return decoratedmethod
87 return decoratedmethod
88
88
89 def _updateprompt(ui, sub, dirty, local, remote):
89 def _updateprompt(ui, sub, dirty, local, remote):
90 if dirty:
90 if dirty:
91 msg = (_(' subrepository sources for %s differ\n'
91 msg = (_(' subrepository sources for %s differ\n'
92 'use (l)ocal source (%s) or (r)emote source (%s)?'
92 'use (l)ocal source (%s) or (r)emote source (%s)?'
93 '$$ &Local $$ &Remote')
93 '$$ &Local $$ &Remote')
94 % (subrelpath(sub), local, remote))
94 % (subrelpath(sub), local, remote))
95 else:
95 else:
96 msg = (_(' subrepository sources for %s differ (in checked out '
96 msg = (_(' subrepository sources for %s differ (in checked out '
97 'version)\n'
97 'version)\n'
98 'use (l)ocal source (%s) or (r)emote source (%s)?'
98 'use (l)ocal source (%s) or (r)emote source (%s)?'
99 '$$ &Local $$ &Remote')
99 '$$ &Local $$ &Remote')
100 % (subrelpath(sub), local, remote))
100 % (subrelpath(sub), local, remote))
101 return ui.promptchoice(msg, 0)
101 return ui.promptchoice(msg, 0)
102
102
103 def _sanitize(ui, vfs, ignore):
103 def _sanitize(ui, vfs, ignore):
104 for dirname, dirs, names in vfs.walk():
104 for dirname, dirs, names in vfs.walk():
105 for i, d in enumerate(dirs):
105 for i, d in enumerate(dirs):
106 if d.lower() == ignore:
106 if d.lower() == ignore:
107 del dirs[i]
107 del dirs[i]
108 break
108 break
109 if vfs.basename(dirname).lower() != '.hg':
109 if vfs.basename(dirname).lower() != '.hg':
110 continue
110 continue
111 for f in names:
111 for f in names:
112 if f.lower() == 'hgrc':
112 if f.lower() == 'hgrc':
113 ui.warn(_("warning: removing potentially hostile 'hgrc' "
113 ui.warn(_("warning: removing potentially hostile 'hgrc' "
114 "in '%s'\n") % vfs.join(dirname))
114 "in '%s'\n") % vfs.join(dirname))
115 vfs.unlink(vfs.reljoin(dirname, f))
115 vfs.unlink(vfs.reljoin(dirname, f))
116
116
117 def _auditsubrepopath(repo, path):
117 def _auditsubrepopath(repo, path):
118 # auditor doesn't check if the path itself is a symlink
118 # auditor doesn't check if the path itself is a symlink
119 pathutil.pathauditor(repo.root)(path)
119 pathutil.pathauditor(repo.root)(path)
120 if repo.wvfs.islink(path):
120 if repo.wvfs.islink(path):
121 raise error.Abort(_("subrepo '%s' traverses symbolic link") % path)
121 raise error.Abort(_("subrepo '%s' traverses symbolic link") % path)
122
122
123 SUBREPO_ALLOWED_DEFAULTS = {
123 SUBREPO_ALLOWED_DEFAULTS = {
124 'hg': True,
124 'hg': True,
125 'git': False,
125 'git': False,
126 'svn': False,
126 'svn': False,
127 }
127 }
128
128
129 def _checktype(ui, kind):
129 def _checktype(ui, kind):
130 # subrepos.allowed is a master kill switch. If disabled, subrepos are
130 # subrepos.allowed is a master kill switch. If disabled, subrepos are
131 # disabled period.
131 # disabled period.
132 if not ui.configbool('subrepos', 'allowed', True):
132 if not ui.configbool('subrepos', 'allowed', True):
133 raise error.Abort(_('subrepos not enabled'),
133 raise error.Abort(_('subrepos not enabled'),
134 hint=_("see 'hg help config.subrepos' for details"))
134 hint=_("see 'hg help config.subrepos' for details"))
135
135
136 default = SUBREPO_ALLOWED_DEFAULTS.get(kind, False)
136 default = SUBREPO_ALLOWED_DEFAULTS.get(kind, False)
137 if not ui.configbool('subrepos', '%s:allowed' % kind, default):
137 if not ui.configbool('subrepos', '%s:allowed' % kind, default):
138 raise error.Abort(_('%s subrepos not allowed') % kind,
138 raise error.Abort(_('%s subrepos not allowed') % kind,
139 hint=_("see 'hg help config.subrepos' for details"))
139 hint=_("see 'hg help config.subrepos' for details"))
140
140
141 if kind not in types:
141 if kind not in types:
142 raise error.Abort(_('unknown subrepo type %s') % kind)
142 raise error.Abort(_('unknown subrepo type %s') % kind)
143
143
144 def subrepo(ctx, path, allowwdir=False, allowcreate=True):
144 def subrepo(ctx, path, allowwdir=False, allowcreate=True):
145 """return instance of the right subrepo class for subrepo in path"""
145 """return instance of the right subrepo class for subrepo in path"""
146 # subrepo inherently violates our import layering rules
146 # subrepo inherently violates our import layering rules
147 # because it wants to make repo objects from deep inside the stack
147 # because it wants to make repo objects from deep inside the stack
148 # so we manually delay the circular imports to not break
148 # so we manually delay the circular imports to not break
149 # scripts that don't use our demand-loading
149 # scripts that don't use our demand-loading
150 global hg
150 global hg
151 from . import hg as h
151 from . import hg as h
152 hg = h
152 hg = h
153
153
154 repo = ctx.repo()
154 repo = ctx.repo()
155 _auditsubrepopath(repo, path)
155 _auditsubrepopath(repo, path)
156 state = ctx.substate[path]
156 state = ctx.substate[path]
157 _checktype(repo.ui, state[2])
157 _checktype(repo.ui, state[2])
158 if allowwdir:
158 if allowwdir:
159 state = (state[0], ctx.subrev(path), state[2])
159 state = (state[0], ctx.subrev(path), state[2])
160 return types[state[2]](ctx, path, state[:2], allowcreate)
160 return types[state[2]](ctx, path, state[:2], allowcreate)
161
161
162 def nullsubrepo(ctx, path, pctx):
162 def nullsubrepo(ctx, path, pctx):
163 """return an empty subrepo in pctx for the extant subrepo in ctx"""
163 """return an empty subrepo in pctx for the extant subrepo in ctx"""
164 # subrepo inherently violates our import layering rules
164 # subrepo inherently violates our import layering rules
165 # because it wants to make repo objects from deep inside the stack
165 # because it wants to make repo objects from deep inside the stack
166 # so we manually delay the circular imports to not break
166 # so we manually delay the circular imports to not break
167 # scripts that don't use our demand-loading
167 # scripts that don't use our demand-loading
168 global hg
168 global hg
169 from . import hg as h
169 from . import hg as h
170 hg = h
170 hg = h
171
171
172 repo = ctx.repo()
172 repo = ctx.repo()
173 _auditsubrepopath(repo, path)
173 _auditsubrepopath(repo, path)
174 state = ctx.substate[path]
174 state = ctx.substate[path]
175 _checktype(repo.ui, state[2])
175 _checktype(repo.ui, state[2])
176 subrev = ''
176 subrev = ''
177 if state[2] == 'hg':
177 if state[2] == 'hg':
178 subrev = "0" * 40
178 subrev = "0" * 40
179 return types[state[2]](pctx, path, (state[0], subrev), True)
179 return types[state[2]](pctx, path, (state[0], subrev), True)
180
180
181 # subrepo classes need to implement the following abstract class:
181 # subrepo classes need to implement the following abstract class:
182
182
183 class abstractsubrepo(object):
183 class abstractsubrepo(object):
184
184
185 def __init__(self, ctx, path):
185 def __init__(self, ctx, path):
186 """Initialize abstractsubrepo part
186 """Initialize abstractsubrepo part
187
187
188 ``ctx`` is the context referring this subrepository in the
188 ``ctx`` is the context referring this subrepository in the
189 parent repository.
189 parent repository.
190
190
191 ``path`` is the path to this subrepository as seen from
191 ``path`` is the path to this subrepository as seen from
192 innermost repository.
192 innermost repository.
193 """
193 """
194 self.ui = ctx.repo().ui
194 self.ui = ctx.repo().ui
195 self._ctx = ctx
195 self._ctx = ctx
196 self._path = path
196 self._path = path
197
197
198 def addwebdirpath(self, serverpath, webconf):
198 def addwebdirpath(self, serverpath, webconf):
199 """Add the hgwebdir entries for this subrepo, and any of its subrepos.
199 """Add the hgwebdir entries for this subrepo, and any of its subrepos.
200
200
201 ``serverpath`` is the path component of the URL for this repo.
201 ``serverpath`` is the path component of the URL for this repo.
202
202
203 ``webconf`` is the dictionary of hgwebdir entries.
203 ``webconf`` is the dictionary of hgwebdir entries.
204 """
204 """
205 pass
205 pass
206
206
207 def storeclean(self, path):
207 def storeclean(self, path):
208 """
208 """
209 returns true if the repository has not changed since it was last
209 returns true if the repository has not changed since it was last
210 cloned from or pushed to a given repository.
210 cloned from or pushed to a given repository.
211 """
211 """
212 return False
212 return False
213
213
214 def dirty(self, ignoreupdate=False, missing=False):
214 def dirty(self, ignoreupdate=False, missing=False):
215 """returns true if the dirstate of the subrepo is dirty or does not
215 """returns true if the dirstate of the subrepo is dirty or does not
216 match current stored state. If ignoreupdate is true, only check
216 match current stored state. If ignoreupdate is true, only check
217 whether the subrepo has uncommitted changes in its dirstate. If missing
217 whether the subrepo has uncommitted changes in its dirstate. If missing
218 is true, check for deleted files.
218 is true, check for deleted files.
219 """
219 """
220 raise NotImplementedError
220 raise NotImplementedError
221
221
222 def dirtyreason(self, ignoreupdate=False, missing=False):
222 def dirtyreason(self, ignoreupdate=False, missing=False):
223 """return reason string if it is ``dirty()``
223 """return reason string if it is ``dirty()``
224
224
225 Returned string should have enough information for the message
225 Returned string should have enough information for the message
226 of exception.
226 of exception.
227
227
228 This returns None, otherwise.
228 This returns None, otherwise.
229 """
229 """
230 if self.dirty(ignoreupdate=ignoreupdate, missing=missing):
230 if self.dirty(ignoreupdate=ignoreupdate, missing=missing):
231 return _('uncommitted changes in subrepository "%s"'
231 return _('uncommitted changes in subrepository "%s"'
232 ) % subrelpath(self)
232 ) % subrelpath(self)
233
233
234 def bailifchanged(self, ignoreupdate=False, hint=None):
234 def bailifchanged(self, ignoreupdate=False, hint=None):
235 """raise Abort if subrepository is ``dirty()``
235 """raise Abort if subrepository is ``dirty()``
236 """
236 """
237 dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate,
237 dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate,
238 missing=True)
238 missing=True)
239 if dirtyreason:
239 if dirtyreason:
240 raise error.Abort(dirtyreason, hint=hint)
240 raise error.Abort(dirtyreason, hint=hint)
241
241
242 def basestate(self):
242 def basestate(self):
243 """current working directory base state, disregarding .hgsubstate
243 """current working directory base state, disregarding .hgsubstate
244 state and working directory modifications"""
244 state and working directory modifications"""
245 raise NotImplementedError
245 raise NotImplementedError
246
246
247 def checknested(self, path):
247 def checknested(self, path):
248 """check if path is a subrepository within this repository"""
248 """check if path is a subrepository within this repository"""
249 return False
249 return False
250
250
251 def commit(self, text, user, date):
251 def commit(self, text, user, date):
252 """commit the current changes to the subrepo with the given
252 """commit the current changes to the subrepo with the given
253 log message. Use given user and date if possible. Return the
253 log message. Use given user and date if possible. Return the
254 new state of the subrepo.
254 new state of the subrepo.
255 """
255 """
256 raise NotImplementedError
256 raise NotImplementedError
257
257
258 def phase(self, state):
258 def phase(self, state):
259 """returns phase of specified state in the subrepository.
259 """returns phase of specified state in the subrepository.
260 """
260 """
261 return phases.public
261 return phases.public
262
262
263 def remove(self):
263 def remove(self):
264 """remove the subrepo
264 """remove the subrepo
265
265
266 (should verify the dirstate is not dirty first)
266 (should verify the dirstate is not dirty first)
267 """
267 """
268 raise NotImplementedError
268 raise NotImplementedError
269
269
270 def get(self, state, overwrite=False):
270 def get(self, state, overwrite=False):
271 """run whatever commands are needed to put the subrepo into
271 """run whatever commands are needed to put the subrepo into
272 this state
272 this state
273 """
273 """
274 raise NotImplementedError
274 raise NotImplementedError
275
275
276 def merge(self, state):
276 def merge(self, state):
277 """merge currently-saved state with the new state."""
277 """merge currently-saved state with the new state."""
278 raise NotImplementedError
278 raise NotImplementedError
279
279
280 def push(self, opts):
280 def push(self, opts):
281 """perform whatever action is analogous to 'hg push'
281 """perform whatever action is analogous to 'hg push'
282
282
283 This may be a no-op on some systems.
283 This may be a no-op on some systems.
284 """
284 """
285 raise NotImplementedError
285 raise NotImplementedError
286
286
287 def add(self, ui, match, prefix, explicitonly, **opts):
287 def add(self, ui, match, prefix, explicitonly, **opts):
288 return []
288 return []
289
289
290 def addremove(self, matcher, prefix, opts):
290 def addremove(self, matcher, prefix, opts):
291 self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
291 self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
292 return 1
292 return 1
293
293
294 def cat(self, match, fm, fntemplate, prefix, **opts):
294 def cat(self, match, fm, fntemplate, prefix, **opts):
295 return 1
295 return 1
296
296
297 def status(self, rev2, **opts):
297 def status(self, rev2, **opts):
298 return scmutil.status([], [], [], [], [], [], [])
298 return scmutil.status([], [], [], [], [], [], [])
299
299
300 def diff(self, ui, diffopts, node2, match, prefix, **opts):
300 def diff(self, ui, diffopts, node2, match, prefix, **opts):
301 pass
301 pass
302
302
303 def outgoing(self, ui, dest, opts):
303 def outgoing(self, ui, dest, opts):
304 return 1
304 return 1
305
305
306 def incoming(self, ui, source, opts):
306 def incoming(self, ui, source, opts):
307 return 1
307 return 1
308
308
309 def files(self):
309 def files(self):
310 """return filename iterator"""
310 """return filename iterator"""
311 raise NotImplementedError
311 raise NotImplementedError
312
312
313 def filedata(self, name, decode):
313 def filedata(self, name, decode):
314 """return file data, optionally passed through repo decoders"""
314 """return file data, optionally passed through repo decoders"""
315 raise NotImplementedError
315 raise NotImplementedError
316
316
317 def fileflags(self, name):
317 def fileflags(self, name):
318 """return file flags"""
318 """return file flags"""
319 return ''
319 return ''
320
320
321 def getfileset(self, expr):
321 def getfileset(self, expr):
322 """Resolve the fileset expression for this repo"""
322 """Resolve the fileset expression for this repo"""
323 return set()
323 return set()
324
324
325 def printfiles(self, ui, m, fm, fmt, subrepos):
325 def printfiles(self, ui, m, fm, fmt, subrepos):
326 """handle the files command for this subrepo"""
326 """handle the files command for this subrepo"""
327 return 1
327 return 1
328
328
329 def archive(self, archiver, prefix, match=None, decode=True):
329 def archive(self, archiver, prefix, match=None, decode=True):
330 if match is not None:
330 if match is not None:
331 files = [f for f in self.files() if match(f)]
331 files = [f for f in self.files() if match(f)]
332 else:
332 else:
333 files = self.files()
333 files = self.files()
334 total = len(files)
334 total = len(files)
335 relpath = subrelpath(self)
335 relpath = subrelpath(self)
336 self.ui.progress(_('archiving (%s)') % relpath, 0,
336 self.ui.progress(_('archiving (%s)') % relpath, 0,
337 unit=_('files'), total=total)
337 unit=_('files'), total=total)
338 for i, name in enumerate(files):
338 for i, name in enumerate(files):
339 flags = self.fileflags(name)
339 flags = self.fileflags(name)
340 mode = 'x' in flags and 0o755 or 0o644
340 mode = 'x' in flags and 0o755 or 0o644
341 symlink = 'l' in flags
341 symlink = 'l' in flags
342 archiver.addfile(prefix + self._path + '/' + name,
342 archiver.addfile(prefix + self._path + '/' + name,
343 mode, symlink, self.filedata(name, decode))
343 mode, symlink, self.filedata(name, decode))
344 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
344 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
345 unit=_('files'), total=total)
345 unit=_('files'), total=total)
346 self.ui.progress(_('archiving (%s)') % relpath, None)
346 self.ui.progress(_('archiving (%s)') % relpath, None)
347 return total
347 return total
348
348
349 def walk(self, match):
349 def walk(self, match):
350 '''
350 '''
351 walk recursively through the directory tree, finding all files
351 walk recursively through the directory tree, finding all files
352 matched by the match function
352 matched by the match function
353 '''
353 '''
354
354
355 def forget(self, match, prefix, dryrun, interactive):
355 def forget(self, match, prefix, dryrun, interactive):
356 return ([], [])
356 return ([], [])
357
357
358 def removefiles(self, matcher, prefix, after, force, subrepos,
358 def removefiles(self, matcher, prefix, after, force, subrepos,
359 dryrun, warnings):
359 dryrun, warnings):
360 """remove the matched files from the subrepository and the filesystem,
360 """remove the matched files from the subrepository and the filesystem,
361 possibly by force and/or after the file has been removed from the
361 possibly by force and/or after the file has been removed from the
362 filesystem. Return 0 on success, 1 on any warning.
362 filesystem. Return 0 on success, 1 on any warning.
363 """
363 """
364 warnings.append(_("warning: removefiles not implemented (%s)")
364 warnings.append(_("warning: removefiles not implemented (%s)")
365 % self._path)
365 % self._path)
366 return 1
366 return 1
367
367
368 def revert(self, substate, *pats, **opts):
368 def revert(self, substate, *pats, **opts):
369 self.ui.warn(_('%s: reverting %s subrepos is unsupported\n') \
369 self.ui.warn(_('%s: reverting %s subrepos is unsupported\n') \
370 % (substate[0], substate[2]))
370 % (substate[0], substate[2]))
371 return []
371 return []
372
372
373 def shortid(self, revid):
373 def shortid(self, revid):
374 return revid
374 return revid
375
375
376 def unshare(self):
376 def unshare(self):
377 '''
377 '''
378 convert this repository from shared to normal storage.
378 convert this repository from shared to normal storage.
379 '''
379 '''
380
380
381 def verify(self):
381 def verify(self):
382 '''verify the integrity of the repository. Return 0 on success or
382 '''verify the integrity of the repository. Return 0 on success or
383 warning, 1 on any error.
383 warning, 1 on any error.
384 '''
384 '''
385 return 0
385 return 0
386
386
387 @propertycache
387 @propertycache
388 def wvfs(self):
388 def wvfs(self):
389 """return vfs to access the working directory of this subrepository
389 """return vfs to access the working directory of this subrepository
390 """
390 """
391 return vfsmod.vfs(self._ctx.repo().wvfs.join(self._path))
391 return vfsmod.vfs(self._ctx.repo().wvfs.join(self._path))
392
392
393 @propertycache
393 @propertycache
394 def _relpath(self):
394 def _relpath(self):
395 """return path to this subrepository as seen from outermost repository
395 """return path to this subrepository as seen from outermost repository
396 """
396 """
397 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
397 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
398
398
399 class hgsubrepo(abstractsubrepo):
399 class hgsubrepo(abstractsubrepo):
400 def __init__(self, ctx, path, state, allowcreate):
400 def __init__(self, ctx, path, state, allowcreate):
401 super(hgsubrepo, self).__init__(ctx, path)
401 super(hgsubrepo, self).__init__(ctx, path)
402 self._state = state
402 self._state = state
403 r = ctx.repo()
403 r = ctx.repo()
404 root = r.wjoin(path)
404 root = r.wjoin(path)
405 create = allowcreate and not r.wvfs.exists('%s/.hg' % path)
405 create = allowcreate and not r.wvfs.exists('%s/.hg' % path)
406 self._repo = hg.repository(r.baseui, root, create=create)
406 self._repo = hg.repository(r.baseui, root, create=create)
407
407
408 # Propagate the parent's --hidden option
408 # Propagate the parent's --hidden option
409 if r is r.unfiltered():
409 if r is r.unfiltered():
410 self._repo = self._repo.unfiltered()
410 self._repo = self._repo.unfiltered()
411
411
412 self.ui = self._repo.ui
412 self.ui = self._repo.ui
413 for s, k in [('ui', 'commitsubrepos')]:
413 for s, k in [('ui', 'commitsubrepos')]:
414 v = r.ui.config(s, k)
414 v = r.ui.config(s, k)
415 if v:
415 if v:
416 self.ui.setconfig(s, k, v, 'subrepo')
416 self.ui.setconfig(s, k, v, 'subrepo')
417 # internal config: ui._usedassubrepo
417 # internal config: ui._usedassubrepo
418 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
418 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
419 self._initrepo(r, state[0], create)
419 self._initrepo(r, state[0], create)
420
420
421 @annotatesubrepoerror
421 @annotatesubrepoerror
422 def addwebdirpath(self, serverpath, webconf):
422 def addwebdirpath(self, serverpath, webconf):
423 cmdutil.addwebdirpath(self._repo, subrelpath(self), webconf)
423 cmdutil.addwebdirpath(self._repo, subrelpath(self), webconf)
424
424
425 def storeclean(self, path):
425 def storeclean(self, path):
426 with self._repo.lock():
426 with self._repo.lock():
427 return self._storeclean(path)
427 return self._storeclean(path)
428
428
429 def _storeclean(self, path):
429 def _storeclean(self, path):
430 clean = True
430 clean = True
431 itercache = self._calcstorehash(path)
431 itercache = self._calcstorehash(path)
432 for filehash in self._readstorehashcache(path):
432 for filehash in self._readstorehashcache(path):
433 if filehash != next(itercache, None):
433 if filehash != next(itercache, None):
434 clean = False
434 clean = False
435 break
435 break
436 if clean:
436 if clean:
437 # if not empty:
437 # if not empty:
438 # the cached and current pull states have a different size
438 # the cached and current pull states have a different size
439 clean = next(itercache, None) is None
439 clean = next(itercache, None) is None
440 return clean
440 return clean
441
441
442 def _calcstorehash(self, remotepath):
442 def _calcstorehash(self, remotepath):
443 '''calculate a unique "store hash"
443 '''calculate a unique "store hash"
444
444
445 This method is used to to detect when there are changes that may
445 This method is used to to detect when there are changes that may
446 require a push to a given remote path.'''
446 require a push to a given remote path.'''
447 # sort the files that will be hashed in increasing (likely) file size
447 # sort the files that will be hashed in increasing (likely) file size
448 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
448 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
449 yield '# %s\n' % _expandedabspath(remotepath)
449 yield '# %s\n' % _expandedabspath(remotepath)
450 vfs = self._repo.vfs
450 vfs = self._repo.vfs
451 for relname in filelist:
451 for relname in filelist:
452 filehash = node.hex(hashlib.sha1(vfs.tryread(relname)).digest())
452 filehash = node.hex(hashlib.sha1(vfs.tryread(relname)).digest())
453 yield '%s = %s\n' % (relname, filehash)
453 yield '%s = %s\n' % (relname, filehash)
454
454
455 @propertycache
455 @propertycache
456 def _cachestorehashvfs(self):
456 def _cachestorehashvfs(self):
457 return vfsmod.vfs(self._repo.vfs.join('cache/storehash'))
457 return vfsmod.vfs(self._repo.vfs.join('cache/storehash'))
458
458
459 def _readstorehashcache(self, remotepath):
459 def _readstorehashcache(self, remotepath):
460 '''read the store hash cache for a given remote repository'''
460 '''read the store hash cache for a given remote repository'''
461 cachefile = _getstorehashcachename(remotepath)
461 cachefile = _getstorehashcachename(remotepath)
462 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
462 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
463
463
464 def _cachestorehash(self, remotepath):
464 def _cachestorehash(self, remotepath):
465 '''cache the current store hash
465 '''cache the current store hash
466
466
467 Each remote repo requires its own store hash cache, because a subrepo
467 Each remote repo requires its own store hash cache, because a subrepo
468 store may be "clean" versus a given remote repo, but not versus another
468 store may be "clean" versus a given remote repo, but not versus another
469 '''
469 '''
470 cachefile = _getstorehashcachename(remotepath)
470 cachefile = _getstorehashcachename(remotepath)
471 with self._repo.lock():
471 with self._repo.lock():
472 storehash = list(self._calcstorehash(remotepath))
472 storehash = list(self._calcstorehash(remotepath))
473 vfs = self._cachestorehashvfs
473 vfs = self._cachestorehashvfs
474 vfs.writelines(cachefile, storehash, mode='wb', notindexed=True)
474 vfs.writelines(cachefile, storehash, mode='wb', notindexed=True)
475
475
476 def _getctx(self):
476 def _getctx(self):
477 '''fetch the context for this subrepo revision, possibly a workingctx
477 '''fetch the context for this subrepo revision, possibly a workingctx
478 '''
478 '''
479 if self._ctx.rev() is None:
479 if self._ctx.rev() is None:
480 return self._repo[None] # workingctx if parent is workingctx
480 return self._repo[None] # workingctx if parent is workingctx
481 else:
481 else:
482 rev = self._state[1]
482 rev = self._state[1]
483 return self._repo[rev]
483 return self._repo[rev]
484
484
485 @annotatesubrepoerror
485 @annotatesubrepoerror
486 def _initrepo(self, parentrepo, source, create):
486 def _initrepo(self, parentrepo, source, create):
487 self._repo._subparent = parentrepo
487 self._repo._subparent = parentrepo
488 self._repo._subsource = source
488 self._repo._subsource = source
489
489
490 if create:
490 if create:
491 lines = ['[paths]\n']
491 lines = ['[paths]\n']
492
492
493 def addpathconfig(key, value):
493 def addpathconfig(key, value):
494 if value:
494 if value:
495 lines.append('%s = %s\n' % (key, value))
495 lines.append('%s = %s\n' % (key, value))
496 self.ui.setconfig('paths', key, value, 'subrepo')
496 self.ui.setconfig('paths', key, value, 'subrepo')
497
497
498 defpath = _abssource(self._repo, abort=False)
498 defpath = _abssource(self._repo, abort=False)
499 defpushpath = _abssource(self._repo, True, abort=False)
499 defpushpath = _abssource(self._repo, True, abort=False)
500 addpathconfig('default', defpath)
500 addpathconfig('default', defpath)
501 if defpath != defpushpath:
501 if defpath != defpushpath:
502 addpathconfig('default-push', defpushpath)
502 addpathconfig('default-push', defpushpath)
503
503
504 self._repo.vfs.write('hgrc', util.tonativeeol(''.join(lines)))
504 self._repo.vfs.write('hgrc', util.tonativeeol(''.join(lines)))
505
505
506 @annotatesubrepoerror
506 @annotatesubrepoerror
507 def add(self, ui, match, prefix, explicitonly, **opts):
507 def add(self, ui, match, prefix, explicitonly, **opts):
508 return cmdutil.add(ui, self._repo, match,
508 return cmdutil.add(ui, self._repo, match,
509 self.wvfs.reljoin(prefix, self._path),
509 self.wvfs.reljoin(prefix, self._path),
510 explicitonly, **opts)
510 explicitonly, **opts)
511
511
512 @annotatesubrepoerror
512 @annotatesubrepoerror
513 def addremove(self, m, prefix, opts):
513 def addremove(self, m, prefix, opts):
514 # In the same way as sub directories are processed, once in a subrepo,
514 # In the same way as sub directories are processed, once in a subrepo,
515 # always entry any of its subrepos. Don't corrupt the options that will
515 # always entry any of its subrepos. Don't corrupt the options that will
516 # be used to process sibling subrepos however.
516 # be used to process sibling subrepos however.
517 opts = copy.copy(opts)
517 opts = copy.copy(opts)
518 opts['subrepos'] = True
518 opts['subrepos'] = True
519 return scmutil.addremove(self._repo, m,
519 return scmutil.addremove(self._repo, m,
520 self.wvfs.reljoin(prefix, self._path), opts)
520 self.wvfs.reljoin(prefix, self._path), opts)
521
521
522 @annotatesubrepoerror
522 @annotatesubrepoerror
523 def cat(self, match, fm, fntemplate, prefix, **opts):
523 def cat(self, match, fm, fntemplate, prefix, **opts):
524 rev = self._state[1]
524 rev = self._state[1]
525 ctx = self._repo[rev]
525 ctx = self._repo[rev]
526 return cmdutil.cat(self.ui, self._repo, ctx, match, fm, fntemplate,
526 return cmdutil.cat(self.ui, self._repo, ctx, match, fm, fntemplate,
527 prefix, **opts)
527 prefix, **opts)
528
528
529 @annotatesubrepoerror
529 @annotatesubrepoerror
530 def status(self, rev2, **opts):
530 def status(self, rev2, **opts):
531 try:
531 try:
532 rev1 = self._state[1]
532 rev1 = self._state[1]
533 ctx1 = self._repo[rev1]
533 ctx1 = self._repo[rev1]
534 ctx2 = self._repo[rev2]
534 ctx2 = self._repo[rev2]
535 return self._repo.status(ctx1, ctx2, **opts)
535 return self._repo.status(ctx1, ctx2, **opts)
536 except error.RepoLookupError as inst:
536 except error.RepoLookupError as inst:
537 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
537 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
538 % (inst, subrelpath(self)))
538 % (inst, subrelpath(self)))
539 return scmutil.status([], [], [], [], [], [], [])
539 return scmutil.status([], [], [], [], [], [], [])
540
540
541 @annotatesubrepoerror
541 @annotatesubrepoerror
542 def diff(self, ui, diffopts, node2, match, prefix, **opts):
542 def diff(self, ui, diffopts, node2, match, prefix, **opts):
543 try:
543 try:
544 node1 = node.bin(self._state[1])
544 node1 = node.bin(self._state[1])
545 # We currently expect node2 to come from substate and be
545 # We currently expect node2 to come from substate and be
546 # in hex format
546 # in hex format
547 if node2 is not None:
547 if node2 is not None:
548 node2 = node.bin(node2)
548 node2 = node.bin(node2)
549 logcmdutil.diffordiffstat(ui, self._repo, diffopts,
549 logcmdutil.diffordiffstat(ui, self._repo, diffopts,
550 node1, node2, match,
550 node1, node2, match,
551 prefix=posixpath.join(prefix, self._path),
551 prefix=posixpath.join(prefix, self._path),
552 listsubrepos=True, **opts)
552 listsubrepos=True, **opts)
553 except error.RepoLookupError as inst:
553 except error.RepoLookupError as inst:
554 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
554 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
555 % (inst, subrelpath(self)))
555 % (inst, subrelpath(self)))
556
556
557 @annotatesubrepoerror
557 @annotatesubrepoerror
558 def archive(self, archiver, prefix, match=None, decode=True):
558 def archive(self, archiver, prefix, match=None, decode=True):
559 self._get(self._state + ('hg',))
559 self._get(self._state + ('hg',))
560 files = self.files()
560 files = self.files()
561 if match:
561 if match:
562 files = [f for f in files if match(f)]
562 files = [f for f in files if match(f)]
563 rev = self._state[1]
563 rev = self._state[1]
564 ctx = self._repo[rev]
564 ctx = self._repo[rev]
565 scmutil.prefetchfiles(self._repo, [ctx.rev()],
565 scmutil.prefetchfiles(self._repo, [ctx.rev()],
566 scmutil.matchfiles(self._repo, files))
566 scmutil.matchfiles(self._repo, files))
567 total = abstractsubrepo.archive(self, archiver, prefix, match)
567 total = abstractsubrepo.archive(self, archiver, prefix, match)
568 for subpath in ctx.substate:
568 for subpath in ctx.substate:
569 s = subrepo(ctx, subpath, True)
569 s = subrepo(ctx, subpath, True)
570 submatch = matchmod.subdirmatcher(subpath, match)
570 submatch = matchmod.subdirmatcher(subpath, match)
571 total += s.archive(archiver, prefix + self._path + '/', submatch,
571 total += s.archive(archiver, prefix + self._path + '/', submatch,
572 decode)
572 decode)
573 return total
573 return total
574
574
575 @annotatesubrepoerror
575 @annotatesubrepoerror
576 def dirty(self, ignoreupdate=False, missing=False):
576 def dirty(self, ignoreupdate=False, missing=False):
577 r = self._state[1]
577 r = self._state[1]
578 if r == '' and not ignoreupdate: # no state recorded
578 if r == '' and not ignoreupdate: # no state recorded
579 return True
579 return True
580 w = self._repo[None]
580 w = self._repo[None]
581 if r != w.p1().hex() and not ignoreupdate:
581 if r != w.p1().hex() and not ignoreupdate:
582 # different version checked out
582 # different version checked out
583 return True
583 return True
584 return w.dirty(missing=missing) # working directory changed
584 return w.dirty(missing=missing) # working directory changed
585
585
586 def basestate(self):
586 def basestate(self):
587 return self._repo['.'].hex()
587 return self._repo['.'].hex()
588
588
589 def checknested(self, path):
589 def checknested(self, path):
590 return self._repo._checknested(self._repo.wjoin(path))
590 return self._repo._checknested(self._repo.wjoin(path))
591
591
592 @annotatesubrepoerror
592 @annotatesubrepoerror
593 def commit(self, text, user, date):
593 def commit(self, text, user, date):
594 # don't bother committing in the subrepo if it's only been
594 # don't bother committing in the subrepo if it's only been
595 # updated
595 # updated
596 if not self.dirty(True):
596 if not self.dirty(True):
597 return self._repo['.'].hex()
597 return self._repo['.'].hex()
598 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
598 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
599 n = self._repo.commit(text, user, date)
599 n = self._repo.commit(text, user, date)
600 if not n:
600 if not n:
601 return self._repo['.'].hex() # different version checked out
601 return self._repo['.'].hex() # different version checked out
602 return node.hex(n)
602 return node.hex(n)
603
603
604 @annotatesubrepoerror
604 @annotatesubrepoerror
605 def phase(self, state):
605 def phase(self, state):
606 return self._repo[state or '.'].phase()
606 return self._repo[state or '.'].phase()
607
607
608 @annotatesubrepoerror
608 @annotatesubrepoerror
609 def remove(self):
609 def remove(self):
610 # we can't fully delete the repository as it may contain
610 # we can't fully delete the repository as it may contain
611 # local-only history
611 # local-only history
612 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
612 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
613 hg.clean(self._repo, node.nullid, False)
613 hg.clean(self._repo, node.nullid, False)
614
614
615 def _get(self, state):
615 def _get(self, state):
616 source, revision, kind = state
616 source, revision, kind = state
617 parentrepo = self._repo._subparent
617 parentrepo = self._repo._subparent
618
618
619 if revision in self._repo.unfiltered():
619 if revision in self._repo.unfiltered():
620 # Allow shared subrepos tracked at null to setup the sharedpath
620 # Allow shared subrepos tracked at null to setup the sharedpath
621 if len(self._repo) != 0 or not parentrepo.shared():
621 if len(self._repo) != 0 or not parentrepo.shared():
622 return True
622 return True
623 self._repo._subsource = source
623 self._repo._subsource = source
624 srcurl = _abssource(self._repo)
624 srcurl = _abssource(self._repo)
625 other = hg.peer(self._repo, {}, srcurl)
625 other = hg.peer(self._repo, {}, srcurl)
626 if len(self._repo) == 0:
626 if len(self._repo) == 0:
627 # use self._repo.vfs instead of self.wvfs to remove .hg only
627 # use self._repo.vfs instead of self.wvfs to remove .hg only
628 self._repo.vfs.rmtree()
628 self._repo.vfs.rmtree()
629
629
630 # A remote subrepo could be shared if there is a local copy
630 # A remote subrepo could be shared if there is a local copy
631 # relative to the parent's share source. But clone pooling doesn't
631 # relative to the parent's share source. But clone pooling doesn't
632 # assemble the repos in a tree, so that can't be consistently done.
632 # assemble the repos in a tree, so that can't be consistently done.
633 # A simpler option is for the user to configure clone pooling, and
633 # A simpler option is for the user to configure clone pooling, and
634 # work with that.
634 # work with that.
635 if parentrepo.shared() and hg.islocal(srcurl):
635 if parentrepo.shared() and hg.islocal(srcurl):
636 self.ui.status(_('sharing subrepo %s from %s\n')
636 self.ui.status(_('sharing subrepo %s from %s\n')
637 % (subrelpath(self), srcurl))
637 % (subrelpath(self), srcurl))
638 shared = hg.share(self._repo._subparent.baseui,
638 shared = hg.share(self._repo._subparent.baseui,
639 other, self._repo.root,
639 other, self._repo.root,
640 update=False, bookmarks=False)
640 update=False, bookmarks=False)
641 self._repo = shared.local()
641 self._repo = shared.local()
642 else:
642 else:
643 # TODO: find a common place for this and this code in the
643 # TODO: find a common place for this and this code in the
644 # share.py wrap of the clone command.
644 # share.py wrap of the clone command.
645 if parentrepo.shared():
645 if parentrepo.shared():
646 pool = self.ui.config('share', 'pool')
646 pool = self.ui.config('share', 'pool')
647 if pool:
647 if pool:
648 pool = util.expandpath(pool)
648 pool = util.expandpath(pool)
649
649
650 shareopts = {
650 shareopts = {
651 'pool': pool,
651 'pool': pool,
652 'mode': self.ui.config('share', 'poolnaming'),
652 'mode': self.ui.config('share', 'poolnaming'),
653 }
653 }
654 else:
654 else:
655 shareopts = {}
655 shareopts = {}
656
656
657 self.ui.status(_('cloning subrepo %s from %s\n')
657 self.ui.status(_('cloning subrepo %s from %s\n')
658 % (subrelpath(self), srcurl))
658 % (subrelpath(self), srcurl))
659 other, cloned = hg.clone(self._repo._subparent.baseui, {},
659 other, cloned = hg.clone(self._repo._subparent.baseui, {},
660 other, self._repo.root,
660 other, self._repo.root,
661 update=False, shareopts=shareopts)
661 update=False, shareopts=shareopts)
662 self._repo = cloned.local()
662 self._repo = cloned.local()
663 self._initrepo(parentrepo, source, create=True)
663 self._initrepo(parentrepo, source, create=True)
664 self._cachestorehash(srcurl)
664 self._cachestorehash(srcurl)
665 else:
665 else:
666 self.ui.status(_('pulling subrepo %s from %s\n')
666 self.ui.status(_('pulling subrepo %s from %s\n')
667 % (subrelpath(self), srcurl))
667 % (subrelpath(self), srcurl))
668 cleansub = self.storeclean(srcurl)
668 cleansub = self.storeclean(srcurl)
669 exchange.pull(self._repo, other)
669 exchange.pull(self._repo, other)
670 if cleansub:
670 if cleansub:
671 # keep the repo clean after pull
671 # keep the repo clean after pull
672 self._cachestorehash(srcurl)
672 self._cachestorehash(srcurl)
673 return False
673 return False
674
674
675 @annotatesubrepoerror
675 @annotatesubrepoerror
676 def get(self, state, overwrite=False):
676 def get(self, state, overwrite=False):
677 inrepo = self._get(state)
677 inrepo = self._get(state)
678 source, revision, kind = state
678 source, revision, kind = state
679 repo = self._repo
679 repo = self._repo
680 repo.ui.debug("getting subrepo %s\n" % self._path)
680 repo.ui.debug("getting subrepo %s\n" % self._path)
681 if inrepo:
681 if inrepo:
682 urepo = repo.unfiltered()
682 urepo = repo.unfiltered()
683 ctx = urepo[revision]
683 ctx = urepo[revision]
684 if ctx.hidden():
684 if ctx.hidden():
685 urepo.ui.warn(
685 urepo.ui.warn(
686 _('revision %s in subrepository "%s" is hidden\n') \
686 _('revision %s in subrepository "%s" is hidden\n') \
687 % (revision[0:12], self._path))
687 % (revision[0:12], self._path))
688 repo = urepo
688 repo = urepo
689 hg.updaterepo(repo, revision, overwrite)
689 hg.updaterepo(repo, revision, overwrite)
690
690
691 @annotatesubrepoerror
691 @annotatesubrepoerror
692 def merge(self, state):
692 def merge(self, state):
693 self._get(state)
693 self._get(state)
694 cur = self._repo['.']
694 cur = self._repo['.']
695 dst = self._repo[state[1]]
695 dst = self._repo[state[1]]
696 anc = dst.ancestor(cur)
696 anc = dst.ancestor(cur)
697
697
698 def mergefunc():
698 def mergefunc():
699 if anc == cur and dst.branch() == cur.branch():
699 if anc == cur and dst.branch() == cur.branch():
700 self.ui.debug('updating subrepository "%s"\n'
700 self.ui.debug('updating subrepository "%s"\n'
701 % subrelpath(self))
701 % subrelpath(self))
702 hg.update(self._repo, state[1])
702 hg.update(self._repo, state[1])
703 elif anc == dst:
703 elif anc == dst:
704 self.ui.debug('skipping subrepository "%s"\n'
704 self.ui.debug('skipping subrepository "%s"\n'
705 % subrelpath(self))
705 % subrelpath(self))
706 else:
706 else:
707 self.ui.debug('merging subrepository "%s"\n' % subrelpath(self))
707 self.ui.debug('merging subrepository "%s"\n' % subrelpath(self))
708 hg.merge(self._repo, state[1], remind=False)
708 hg.merge(self._repo, state[1], remind=False)
709
709
710 wctx = self._repo[None]
710 wctx = self._repo[None]
711 if self.dirty():
711 if self.dirty():
712 if anc != dst:
712 if anc != dst:
713 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
713 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
714 mergefunc()
714 mergefunc()
715 else:
715 else:
716 mergefunc()
716 mergefunc()
717 else:
717 else:
718 mergefunc()
718 mergefunc()
719
719
720 @annotatesubrepoerror
720 @annotatesubrepoerror
721 def push(self, opts):
721 def push(self, opts):
722 force = opts.get('force')
722 force = opts.get('force')
723 newbranch = opts.get('new_branch')
723 newbranch = opts.get('new_branch')
724 ssh = opts.get('ssh')
724 ssh = opts.get('ssh')
725
725
726 # push subrepos depth-first for coherent ordering
726 # push subrepos depth-first for coherent ordering
727 c = self._repo['.']
727 c = self._repo['.']
728 subs = c.substate # only repos that are committed
728 subs = c.substate # only repos that are committed
729 for s in sorted(subs):
729 for s in sorted(subs):
730 if c.sub(s).push(opts) == 0:
730 if c.sub(s).push(opts) == 0:
731 return False
731 return False
732
732
733 dsturl = _abssource(self._repo, True)
733 dsturl = _abssource(self._repo, True)
734 if not force:
734 if not force:
735 if self.storeclean(dsturl):
735 if self.storeclean(dsturl):
736 self.ui.status(
736 self.ui.status(
737 _('no changes made to subrepo %s since last push to %s\n')
737 _('no changes made to subrepo %s since last push to %s\n')
738 % (subrelpath(self), dsturl))
738 % (subrelpath(self), dsturl))
739 return None
739 return None
740 self.ui.status(_('pushing subrepo %s to %s\n') %
740 self.ui.status(_('pushing subrepo %s to %s\n') %
741 (subrelpath(self), dsturl))
741 (subrelpath(self), dsturl))
742 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
742 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
743 res = exchange.push(self._repo, other, force, newbranch=newbranch)
743 res = exchange.push(self._repo, other, force, newbranch=newbranch)
744
744
745 # the repo is now clean
745 # the repo is now clean
746 self._cachestorehash(dsturl)
746 self._cachestorehash(dsturl)
747 return res.cgresult
747 return res.cgresult
748
748
749 @annotatesubrepoerror
749 @annotatesubrepoerror
750 def outgoing(self, ui, dest, opts):
750 def outgoing(self, ui, dest, opts):
751 if 'rev' in opts or 'branch' in opts:
751 if 'rev' in opts or 'branch' in opts:
752 opts = copy.copy(opts)
752 opts = copy.copy(opts)
753 opts.pop('rev', None)
753 opts.pop('rev', None)
754 opts.pop('branch', None)
754 opts.pop('branch', None)
755 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
755 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
756
756
757 @annotatesubrepoerror
757 @annotatesubrepoerror
758 def incoming(self, ui, source, opts):
758 def incoming(self, ui, source, opts):
759 if 'rev' in opts or 'branch' in opts:
759 if 'rev' in opts or 'branch' in opts:
760 opts = copy.copy(opts)
760 opts = copy.copy(opts)
761 opts.pop('rev', None)
761 opts.pop('rev', None)
762 opts.pop('branch', None)
762 opts.pop('branch', None)
763 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
763 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
764
764
765 @annotatesubrepoerror
765 @annotatesubrepoerror
766 def files(self):
766 def files(self):
767 rev = self._state[1]
767 rev = self._state[1]
768 ctx = self._repo[rev]
768 ctx = self._repo[rev]
769 return ctx.manifest().keys()
769 return ctx.manifest().keys()
770
770
771 def filedata(self, name, decode):
771 def filedata(self, name, decode):
772 rev = self._state[1]
772 rev = self._state[1]
773 data = self._repo[rev][name].data()
773 data = self._repo[rev][name].data()
774 if decode:
774 if decode:
775 data = self._repo.wwritedata(name, data)
775 data = self._repo.wwritedata(name, data)
776 return data
776 return data
777
777
778 def fileflags(self, name):
778 def fileflags(self, name):
779 rev = self._state[1]
779 rev = self._state[1]
780 ctx = self._repo[rev]
780 ctx = self._repo[rev]
781 return ctx.flags(name)
781 return ctx.flags(name)
782
782
783 @annotatesubrepoerror
783 @annotatesubrepoerror
784 def printfiles(self, ui, m, fm, fmt, subrepos):
784 def printfiles(self, ui, m, fm, fmt, subrepos):
785 # If the parent context is a workingctx, use the workingctx here for
785 # If the parent context is a workingctx, use the workingctx here for
786 # consistency.
786 # consistency.
787 if self._ctx.rev() is None:
787 if self._ctx.rev() is None:
788 ctx = self._repo[None]
788 ctx = self._repo[None]
789 else:
789 else:
790 rev = self._state[1]
790 rev = self._state[1]
791 ctx = self._repo[rev]
791 ctx = self._repo[rev]
792 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
792 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
793
793
794 @annotatesubrepoerror
794 @annotatesubrepoerror
795 def getfileset(self, expr):
795 def getfileset(self, expr):
796 if self._ctx.rev() is None:
796 if self._ctx.rev() is None:
797 ctx = self._repo[None]
797 ctx = self._repo[None]
798 else:
798 else:
799 rev = self._state[1]
799 rev = self._state[1]
800 ctx = self._repo[rev]
800 ctx = self._repo[rev]
801
801
802 files = ctx.getfileset(expr)
802 files = ctx.getfileset(expr)
803
803
804 for subpath in ctx.substate:
804 for subpath in ctx.substate:
805 sub = ctx.sub(subpath)
805 sub = ctx.sub(subpath)
806
806
807 try:
807 try:
808 files.extend(subpath + '/' + f for f in sub.getfileset(expr))
808 files.extend(subpath + '/' + f for f in sub.getfileset(expr))
809 except error.LookupError:
809 except error.LookupError:
810 self.ui.status(_("skipping missing subrepository: %s\n")
810 self.ui.status(_("skipping missing subrepository: %s\n")
811 % self.wvfs.reljoin(reporelpath(self), subpath))
811 % self.wvfs.reljoin(reporelpath(self), subpath))
812 return files
812 return files
813
813
814 def walk(self, match):
814 def walk(self, match):
815 ctx = self._repo[None]
815 ctx = self._repo[None]
816 return ctx.walk(match)
816 return ctx.walk(match)
817
817
818 @annotatesubrepoerror
818 @annotatesubrepoerror
819 def forget(self, match, prefix, dryrun, interactive):
819 def forget(self, match, prefix, dryrun, interactive):
820 return cmdutil.forget(self.ui, self._repo, match,
820 return cmdutil.forget(self.ui, self._repo, match,
821 self.wvfs.reljoin(prefix, self._path),
821 self.wvfs.reljoin(prefix, self._path),
822 True, dryrun=dryrun, interactive=interactive)
822 True, dryrun=dryrun, interactive=interactive)
823
823
824 @annotatesubrepoerror
824 @annotatesubrepoerror
825 def removefiles(self, matcher, prefix, after, force, subrepos,
825 def removefiles(self, matcher, prefix, after, force, subrepos,
826 dryrun, warnings):
826 dryrun, warnings):
827 return cmdutil.remove(self.ui, self._repo, matcher,
827 return cmdutil.remove(self.ui, self._repo, matcher,
828 self.wvfs.reljoin(prefix, self._path),
828 self.wvfs.reljoin(prefix, self._path),
829 after, force, subrepos, dryrun)
829 after, force, subrepos, dryrun)
830
830
831 @annotatesubrepoerror
831 @annotatesubrepoerror
832 def revert(self, substate, *pats, **opts):
832 def revert(self, substate, *pats, **opts):
833 # reverting a subrepo is a 2 step process:
833 # reverting a subrepo is a 2 step process:
834 # 1. if the no_backup is not set, revert all modified
834 # 1. if the no_backup is not set, revert all modified
835 # files inside the subrepo
835 # files inside the subrepo
836 # 2. update the subrepo to the revision specified in
836 # 2. update the subrepo to the revision specified in
837 # the corresponding substate dictionary
837 # the corresponding substate dictionary
838 self.ui.status(_('reverting subrepo %s\n') % substate[0])
838 self.ui.status(_('reverting subrepo %s\n') % substate[0])
839 if not opts.get(r'no_backup'):
839 if not opts.get(r'no_backup'):
840 # Revert all files on the subrepo, creating backups
840 # Revert all files on the subrepo, creating backups
841 # Note that this will not recursively revert subrepos
841 # Note that this will not recursively revert subrepos
842 # We could do it if there was a set:subrepos() predicate
842 # We could do it if there was a set:subrepos() predicate
843 opts = opts.copy()
843 opts = opts.copy()
844 opts[r'date'] = None
844 opts[r'date'] = None
845 opts[r'rev'] = substate[1]
845 opts[r'rev'] = substate[1]
846
846
847 self.filerevert(*pats, **opts)
847 self.filerevert(*pats, **opts)
848
848
849 # Update the repo to the revision specified in the given substate
849 # Update the repo to the revision specified in the given substate
850 if not opts.get(r'dry_run'):
850 if not opts.get(r'dry_run'):
851 self.get(substate, overwrite=True)
851 self.get(substate, overwrite=True)
852
852
853 def filerevert(self, *pats, **opts):
853 def filerevert(self, *pats, **opts):
854 ctx = self._repo[opts[r'rev']]
854 ctx = self._repo[opts[r'rev']]
855 parents = self._repo.dirstate.parents()
855 parents = self._repo.dirstate.parents()
856 if opts.get(r'all'):
856 if opts.get(r'all'):
857 pats = ['set:modified()']
857 pats = ['set:modified()']
858 else:
858 else:
859 pats = []
859 pats = []
860 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
860 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
861
861
862 def shortid(self, revid):
862 def shortid(self, revid):
863 return revid[:12]
863 return revid[:12]
864
864
865 @annotatesubrepoerror
865 @annotatesubrepoerror
866 def unshare(self):
866 def unshare(self):
867 # subrepo inherently violates our import layering rules
867 # subrepo inherently violates our import layering rules
868 # because it wants to make repo objects from deep inside the stack
868 # because it wants to make repo objects from deep inside the stack
869 # so we manually delay the circular imports to not break
869 # so we manually delay the circular imports to not break
870 # scripts that don't use our demand-loading
870 # scripts that don't use our demand-loading
871 global hg
871 global hg
872 from . import hg as h
872 from . import hg as h
873 hg = h
873 hg = h
874
874
875 # Nothing prevents a user from sharing in a repo, and then making that a
875 # Nothing prevents a user from sharing in a repo, and then making that a
876 # subrepo. Alternately, the previous unshare attempt may have failed
876 # subrepo. Alternately, the previous unshare attempt may have failed
877 # part way through. So recurse whether or not this layer is shared.
877 # part way through. So recurse whether or not this layer is shared.
878 if self._repo.shared():
878 if self._repo.shared():
879 self.ui.status(_("unsharing subrepo '%s'\n") % self._relpath)
879 self.ui.status(_("unsharing subrepo '%s'\n") % self._relpath)
880
880
881 hg.unshare(self.ui, self._repo)
881 hg.unshare(self.ui, self._repo)
882
882
883 def verify(self):
883 def verify(self):
884 try:
884 try:
885 rev = self._state[1]
885 rev = self._state[1]
886 ctx = self._repo.unfiltered()[rev]
886 ctx = self._repo.unfiltered()[rev]
887 if ctx.hidden():
887 if ctx.hidden():
888 # Since hidden revisions aren't pushed/pulled, it seems worth an
888 # Since hidden revisions aren't pushed/pulled, it seems worth an
889 # explicit warning.
889 # explicit warning.
890 ui = self._repo.ui
890 ui = self._repo.ui
891 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
891 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
892 (self._relpath, node.short(self._ctx.node())))
892 (self._relpath, node.short(self._ctx.node())))
893 return 0
893 return 0
894 except error.RepoLookupError:
894 except error.RepoLookupError:
895 # A missing subrepo revision may be a case of needing to pull it, so
895 # A missing subrepo revision may be a case of needing to pull it, so
896 # don't treat this as an error.
896 # don't treat this as an error.
897 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
897 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
898 (self._relpath, node.short(self._ctx.node())))
898 (self._relpath, node.short(self._ctx.node())))
899 return 0
899 return 0
900
900
901 @propertycache
901 @propertycache
902 def wvfs(self):
902 def wvfs(self):
903 """return own wvfs for efficiency and consistency
903 """return own wvfs for efficiency and consistency
904 """
904 """
905 return self._repo.wvfs
905 return self._repo.wvfs
906
906
907 @propertycache
907 @propertycache
908 def _relpath(self):
908 def _relpath(self):
909 """return path to this subrepository as seen from outermost repository
909 """return path to this subrepository as seen from outermost repository
910 """
910 """
911 # Keep consistent dir separators by avoiding vfs.join(self._path)
911 # Keep consistent dir separators by avoiding vfs.join(self._path)
912 return reporelpath(self._repo)
912 return reporelpath(self._repo)
913
913
914 class svnsubrepo(abstractsubrepo):
914 class svnsubrepo(abstractsubrepo):
915 def __init__(self, ctx, path, state, allowcreate):
915 def __init__(self, ctx, path, state, allowcreate):
916 super(svnsubrepo, self).__init__(ctx, path)
916 super(svnsubrepo, self).__init__(ctx, path)
917 self._state = state
917 self._state = state
918 self._exe = procutil.findexe('svn')
918 self._exe = procutil.findexe('svn')
919 if not self._exe:
919 if not self._exe:
920 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
920 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
921 % self._path)
921 % self._path)
922
922
923 def _svncommand(self, commands, filename='', failok=False):
923 def _svncommand(self, commands, filename='', failok=False):
924 cmd = [self._exe]
924 cmd = [self._exe]
925 extrakw = {}
925 extrakw = {}
926 if not self.ui.interactive():
926 if not self.ui.interactive():
927 # Making stdin be a pipe should prevent svn from behaving
927 # Making stdin be a pipe should prevent svn from behaving
928 # interactively even if we can't pass --non-interactive.
928 # interactively even if we can't pass --non-interactive.
929 extrakw[r'stdin'] = subprocess.PIPE
929 extrakw[r'stdin'] = subprocess.PIPE
930 # Starting in svn 1.5 --non-interactive is a global flag
930 # Starting in svn 1.5 --non-interactive is a global flag
931 # instead of being per-command, but we need to support 1.4 so
931 # instead of being per-command, but we need to support 1.4 so
932 # we have to be intelligent about what commands take
932 # we have to be intelligent about what commands take
933 # --non-interactive.
933 # --non-interactive.
934 if commands[0] in ('update', 'checkout', 'commit'):
934 if commands[0] in ('update', 'checkout', 'commit'):
935 cmd.append('--non-interactive')
935 cmd.append('--non-interactive')
936 cmd.extend(commands)
936 cmd.extend(commands)
937 if filename is not None:
937 if filename is not None:
938 path = self.wvfs.reljoin(self._ctx.repo().origroot,
938 path = self.wvfs.reljoin(self._ctx.repo().origroot,
939 self._path, filename)
939 self._path, filename)
940 cmd.append(path)
940 cmd.append(path)
941 env = dict(encoding.environ)
941 env = dict(encoding.environ)
942 # Avoid localized output, preserve current locale for everything else.
942 # Avoid localized output, preserve current locale for everything else.
943 lc_all = env.get('LC_ALL')
943 lc_all = env.get('LC_ALL')
944 if lc_all:
944 if lc_all:
945 env['LANG'] = lc_all
945 env['LANG'] = lc_all
946 del env['LC_ALL']
946 del env['LC_ALL']
947 env['LC_MESSAGES'] = 'C'
947 env['LC_MESSAGES'] = 'C'
948 p = subprocess.Popen(cmd, bufsize=-1, close_fds=procutil.closefds,
948 p = subprocess.Popen(cmd, bufsize=-1, close_fds=procutil.closefds,
949 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
949 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
950 universal_newlines=True, env=env, **extrakw)
950 universal_newlines=True, env=env, **extrakw)
951 stdout, stderr = p.communicate()
951 stdout, stderr = p.communicate()
952 stderr = stderr.strip()
952 stderr = stderr.strip()
953 if not failok:
953 if not failok:
954 if p.returncode:
954 if p.returncode:
955 raise error.Abort(stderr or 'exited with code %d'
955 raise error.Abort(stderr or 'exited with code %d'
956 % p.returncode)
956 % p.returncode)
957 if stderr:
957 if stderr:
958 self.ui.warn(stderr + '\n')
958 self.ui.warn(stderr + '\n')
959 return stdout, stderr
959 return stdout, stderr
960
960
961 @propertycache
961 @propertycache
962 def _svnversion(self):
962 def _svnversion(self):
963 output, err = self._svncommand(['--version', '--quiet'], filename=None)
963 output, err = self._svncommand(['--version', '--quiet'], filename=None)
964 m = re.search(br'^(\d+)\.(\d+)', output)
964 m = re.search(br'^(\d+)\.(\d+)', output)
965 if not m:
965 if not m:
966 raise error.Abort(_('cannot retrieve svn tool version'))
966 raise error.Abort(_('cannot retrieve svn tool version'))
967 return (int(m.group(1)), int(m.group(2)))
967 return (int(m.group(1)), int(m.group(2)))
968
968
969 def _svnmissing(self):
969 def _svnmissing(self):
970 return not self.wvfs.exists('.svn')
970 return not self.wvfs.exists('.svn')
971
971
972 def _wcrevs(self):
972 def _wcrevs(self):
973 # Get the working directory revision as well as the last
973 # Get the working directory revision as well as the last
974 # commit revision so we can compare the subrepo state with
974 # commit revision so we can compare the subrepo state with
975 # both. We used to store the working directory one.
975 # both. We used to store the working directory one.
976 output, err = self._svncommand(['info', '--xml'])
976 output, err = self._svncommand(['info', '--xml'])
977 doc = xml.dom.minidom.parseString(output)
977 doc = xml.dom.minidom.parseString(output)
978 entries = doc.getElementsByTagName('entry')
978 entries = doc.getElementsByTagName('entry')
979 lastrev, rev = '0', '0'
979 lastrev, rev = '0', '0'
980 if entries:
980 if entries:
981 rev = str(entries[0].getAttribute('revision')) or '0'
981 rev = str(entries[0].getAttribute('revision')) or '0'
982 commits = entries[0].getElementsByTagName('commit')
982 commits = entries[0].getElementsByTagName('commit')
983 if commits:
983 if commits:
984 lastrev = str(commits[0].getAttribute('revision')) or '0'
984 lastrev = str(commits[0].getAttribute('revision')) or '0'
985 return (lastrev, rev)
985 return (lastrev, rev)
986
986
987 def _wcrev(self):
987 def _wcrev(self):
988 return self._wcrevs()[0]
988 return self._wcrevs()[0]
989
989
990 def _wcchanged(self):
990 def _wcchanged(self):
991 """Return (changes, extchanges, missing) where changes is True
991 """Return (changes, extchanges, missing) where changes is True
992 if the working directory was changed, extchanges is
992 if the working directory was changed, extchanges is
993 True if any of these changes concern an external entry and missing
993 True if any of these changes concern an external entry and missing
994 is True if any change is a missing entry.
994 is True if any change is a missing entry.
995 """
995 """
996 output, err = self._svncommand(['status', '--xml'])
996 output, err = self._svncommand(['status', '--xml'])
997 externals, changes, missing = [], [], []
997 externals, changes, missing = [], [], []
998 doc = xml.dom.minidom.parseString(output)
998 doc = xml.dom.minidom.parseString(output)
999 for e in doc.getElementsByTagName('entry'):
999 for e in doc.getElementsByTagName('entry'):
1000 s = e.getElementsByTagName('wc-status')
1000 s = e.getElementsByTagName('wc-status')
1001 if not s:
1001 if not s:
1002 continue
1002 continue
1003 item = s[0].getAttribute('item')
1003 item = s[0].getAttribute('item')
1004 props = s[0].getAttribute('props')
1004 props = s[0].getAttribute('props')
1005 path = e.getAttribute('path')
1005 path = e.getAttribute('path')
1006 if item == 'external':
1006 if item == 'external':
1007 externals.append(path)
1007 externals.append(path)
1008 elif item == 'missing':
1008 elif item == 'missing':
1009 missing.append(path)
1009 missing.append(path)
1010 if (item not in ('', 'normal', 'unversioned', 'external')
1010 if (item not in ('', 'normal', 'unversioned', 'external')
1011 or props not in ('', 'none', 'normal')):
1011 or props not in ('', 'none', 'normal')):
1012 changes.append(path)
1012 changes.append(path)
1013 for path in changes:
1013 for path in changes:
1014 for ext in externals:
1014 for ext in externals:
1015 if path == ext or path.startswith(ext + pycompat.ossep):
1015 if path == ext or path.startswith(ext + pycompat.ossep):
1016 return True, True, bool(missing)
1016 return True, True, bool(missing)
1017 return bool(changes), False, bool(missing)
1017 return bool(changes), False, bool(missing)
1018
1018
1019 @annotatesubrepoerror
1019 @annotatesubrepoerror
1020 def dirty(self, ignoreupdate=False, missing=False):
1020 def dirty(self, ignoreupdate=False, missing=False):
1021 if self._svnmissing():
1021 if self._svnmissing():
1022 return self._state[1] != ''
1022 return self._state[1] != ''
1023 wcchanged = self._wcchanged()
1023 wcchanged = self._wcchanged()
1024 changed = wcchanged[0] or (missing and wcchanged[2])
1024 changed = wcchanged[0] or (missing and wcchanged[2])
1025 if not changed:
1025 if not changed:
1026 if self._state[1] in self._wcrevs() or ignoreupdate:
1026 if self._state[1] in self._wcrevs() or ignoreupdate:
1027 return False
1027 return False
1028 return True
1028 return True
1029
1029
1030 def basestate(self):
1030 def basestate(self):
1031 lastrev, rev = self._wcrevs()
1031 lastrev, rev = self._wcrevs()
1032 if lastrev != rev:
1032 if lastrev != rev:
1033 # Last committed rev is not the same than rev. We would
1033 # Last committed rev is not the same than rev. We would
1034 # like to take lastrev but we do not know if the subrepo
1034 # like to take lastrev but we do not know if the subrepo
1035 # URL exists at lastrev. Test it and fallback to rev it
1035 # URL exists at lastrev. Test it and fallback to rev it
1036 # is not there.
1036 # is not there.
1037 try:
1037 try:
1038 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1038 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1039 return lastrev
1039 return lastrev
1040 except error.Abort:
1040 except error.Abort:
1041 pass
1041 pass
1042 return rev
1042 return rev
1043
1043
1044 @annotatesubrepoerror
1044 @annotatesubrepoerror
1045 def commit(self, text, user, date):
1045 def commit(self, text, user, date):
1046 # user and date are out of our hands since svn is centralized
1046 # user and date are out of our hands since svn is centralized
1047 changed, extchanged, missing = self._wcchanged()
1047 changed, extchanged, missing = self._wcchanged()
1048 if not changed:
1048 if not changed:
1049 return self.basestate()
1049 return self.basestate()
1050 if extchanged:
1050 if extchanged:
1051 # Do not try to commit externals
1051 # Do not try to commit externals
1052 raise error.Abort(_('cannot commit svn externals'))
1052 raise error.Abort(_('cannot commit svn externals'))
1053 if missing:
1053 if missing:
1054 # svn can commit with missing entries but aborting like hg
1054 # svn can commit with missing entries but aborting like hg
1055 # seems a better approach.
1055 # seems a better approach.
1056 raise error.Abort(_('cannot commit missing svn entries'))
1056 raise error.Abort(_('cannot commit missing svn entries'))
1057 commitinfo, err = self._svncommand(['commit', '-m', text])
1057 commitinfo, err = self._svncommand(['commit', '-m', text])
1058 self.ui.status(commitinfo)
1058 self.ui.status(commitinfo)
1059 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1059 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1060 if not newrev:
1060 if not newrev:
1061 if not commitinfo.strip():
1061 if not commitinfo.strip():
1062 # Sometimes, our definition of "changed" differs from
1062 # Sometimes, our definition of "changed" differs from
1063 # svn one. For instance, svn ignores missing files
1063 # svn one. For instance, svn ignores missing files
1064 # when committing. If there are only missing files, no
1064 # when committing. If there are only missing files, no
1065 # commit is made, no output and no error code.
1065 # commit is made, no output and no error code.
1066 raise error.Abort(_('failed to commit svn changes'))
1066 raise error.Abort(_('failed to commit svn changes'))
1067 raise error.Abort(commitinfo.splitlines()[-1])
1067 raise error.Abort(commitinfo.splitlines()[-1])
1068 newrev = newrev.groups()[0]
1068 newrev = newrev.groups()[0]
1069 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1069 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1070 return newrev
1070 return newrev
1071
1071
1072 @annotatesubrepoerror
1072 @annotatesubrepoerror
1073 def remove(self):
1073 def remove(self):
1074 if self.dirty():
1074 if self.dirty():
1075 self.ui.warn(_('not removing repo %s because '
1075 self.ui.warn(_('not removing repo %s because '
1076 'it has changes.\n') % self._path)
1076 'it has changes.\n') % self._path)
1077 return
1077 return
1078 self.ui.note(_('removing subrepo %s\n') % self._path)
1078 self.ui.note(_('removing subrepo %s\n') % self._path)
1079
1079
1080 self.wvfs.rmtree(forcibly=True)
1080 self.wvfs.rmtree(forcibly=True)
1081 try:
1081 try:
1082 pwvfs = self._ctx.repo().wvfs
1082 pwvfs = self._ctx.repo().wvfs
1083 pwvfs.removedirs(pwvfs.dirname(self._path))
1083 pwvfs.removedirs(pwvfs.dirname(self._path))
1084 except OSError:
1084 except OSError:
1085 pass
1085 pass
1086
1086
1087 @annotatesubrepoerror
1087 @annotatesubrepoerror
1088 def get(self, state, overwrite=False):
1088 def get(self, state, overwrite=False):
1089 if overwrite:
1089 if overwrite:
1090 self._svncommand(['revert', '--recursive'])
1090 self._svncommand(['revert', '--recursive'])
1091 args = ['checkout']
1091 args = ['checkout']
1092 if self._svnversion >= (1, 5):
1092 if self._svnversion >= (1, 5):
1093 args.append('--force')
1093 args.append('--force')
1094 # The revision must be specified at the end of the URL to properly
1094 # The revision must be specified at the end of the URL to properly
1095 # update to a directory which has since been deleted and recreated.
1095 # update to a directory which has since been deleted and recreated.
1096 args.append('%s@%s' % (state[0], state[1]))
1096 args.append('%s@%s' % (state[0], state[1]))
1097
1097
1098 # SEC: check that the ssh url is safe
1098 # SEC: check that the ssh url is safe
1099 util.checksafessh(state[0])
1099 util.checksafessh(state[0])
1100
1100
1101 status, err = self._svncommand(args, failok=True)
1101 status, err = self._svncommand(args, failok=True)
1102 _sanitize(self.ui, self.wvfs, '.svn')
1102 _sanitize(self.ui, self.wvfs, '.svn')
1103 if not re.search('Checked out revision [0-9]+.', status):
1103 if not re.search('Checked out revision [0-9]+.', status):
1104 if ('is already a working copy for a different URL' in err
1104 if ('is already a working copy for a different URL' in err
1105 and (self._wcchanged()[:2] == (False, False))):
1105 and (self._wcchanged()[:2] == (False, False))):
1106 # obstructed but clean working copy, so just blow it away.
1106 # obstructed but clean working copy, so just blow it away.
1107 self.remove()
1107 self.remove()
1108 self.get(state, overwrite=False)
1108 self.get(state, overwrite=False)
1109 return
1109 return
1110 raise error.Abort((status or err).splitlines()[-1])
1110 raise error.Abort((status or err).splitlines()[-1])
1111 self.ui.status(status)
1111 self.ui.status(status)
1112
1112
1113 @annotatesubrepoerror
1113 @annotatesubrepoerror
1114 def merge(self, state):
1114 def merge(self, state):
1115 old = self._state[1]
1115 old = self._state[1]
1116 new = state[1]
1116 new = state[1]
1117 wcrev = self._wcrev()
1117 wcrev = self._wcrev()
1118 if new != wcrev:
1118 if new != wcrev:
1119 dirty = old == wcrev or self._wcchanged()[0]
1119 dirty = old == wcrev or self._wcchanged()[0]
1120 if _updateprompt(self.ui, self, dirty, wcrev, new):
1120 if _updateprompt(self.ui, self, dirty, wcrev, new):
1121 self.get(state, False)
1121 self.get(state, False)
1122
1122
1123 def push(self, opts):
1123 def push(self, opts):
1124 # push is a no-op for SVN
1124 # push is a no-op for SVN
1125 return True
1125 return True
1126
1126
1127 @annotatesubrepoerror
1127 @annotatesubrepoerror
1128 def files(self):
1128 def files(self):
1129 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1129 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1130 doc = xml.dom.minidom.parseString(output)
1130 doc = xml.dom.minidom.parseString(output)
1131 paths = []
1131 paths = []
1132 for e in doc.getElementsByTagName('entry'):
1132 for e in doc.getElementsByTagName('entry'):
1133 kind = pycompat.bytestr(e.getAttribute('kind'))
1133 kind = pycompat.bytestr(e.getAttribute('kind'))
1134 if kind != 'file':
1134 if kind != 'file':
1135 continue
1135 continue
1136 name = ''.join(c.data for c
1136 name = ''.join(c.data for c
1137 in e.getElementsByTagName('name')[0].childNodes
1137 in e.getElementsByTagName('name')[0].childNodes
1138 if c.nodeType == c.TEXT_NODE)
1138 if c.nodeType == c.TEXT_NODE)
1139 paths.append(name.encode('utf-8'))
1139 paths.append(name.encode('utf-8'))
1140 return paths
1140 return paths
1141
1141
1142 def filedata(self, name, decode):
1142 def filedata(self, name, decode):
1143 return self._svncommand(['cat'], name)[0]
1143 return self._svncommand(['cat'], name)[0]
1144
1144
1145
1145
1146 class gitsubrepo(abstractsubrepo):
1146 class gitsubrepo(abstractsubrepo):
1147 def __init__(self, ctx, path, state, allowcreate):
1147 def __init__(self, ctx, path, state, allowcreate):
1148 super(gitsubrepo, self).__init__(ctx, path)
1148 super(gitsubrepo, self).__init__(ctx, path)
1149 self._state = state
1149 self._state = state
1150 self._abspath = ctx.repo().wjoin(path)
1150 self._abspath = ctx.repo().wjoin(path)
1151 self._subparent = ctx.repo()
1151 self._subparent = ctx.repo()
1152 self._ensuregit()
1152 self._ensuregit()
1153
1153
1154 def _ensuregit(self):
1154 def _ensuregit(self):
1155 try:
1155 try:
1156 self._gitexecutable = 'git'
1156 self._gitexecutable = 'git'
1157 out, err = self._gitnodir(['--version'])
1157 out, err = self._gitnodir(['--version'])
1158 except OSError as e:
1158 except OSError as e:
1159 genericerror = _("error executing git for subrepo '%s': %s")
1159 genericerror = _("error executing git for subrepo '%s': %s")
1160 notfoundhint = _("check git is installed and in your PATH")
1160 notfoundhint = _("check git is installed and in your PATH")
1161 if e.errno != errno.ENOENT:
1161 if e.errno != errno.ENOENT:
1162 raise error.Abort(genericerror % (
1162 raise error.Abort(genericerror % (
1163 self._path, encoding.strtolocal(e.strerror)))
1163 self._path, encoding.strtolocal(e.strerror)))
1164 elif pycompat.iswindows:
1164 elif pycompat.iswindows:
1165 try:
1165 try:
1166 self._gitexecutable = 'git.cmd'
1166 self._gitexecutable = 'git.cmd'
1167 out, err = self._gitnodir(['--version'])
1167 out, err = self._gitnodir(['--version'])
1168 except OSError as e2:
1168 except OSError as e2:
1169 if e2.errno == errno.ENOENT:
1169 if e2.errno == errno.ENOENT:
1170 raise error.Abort(_("couldn't find 'git' or 'git.cmd'"
1170 raise error.Abort(_("couldn't find 'git' or 'git.cmd'"
1171 " for subrepo '%s'") % self._path,
1171 " for subrepo '%s'") % self._path,
1172 hint=notfoundhint)
1172 hint=notfoundhint)
1173 else:
1173 else:
1174 raise error.Abort(genericerror % (self._path,
1174 raise error.Abort(genericerror % (self._path,
1175 encoding.strtolocal(e2.strerror)))
1175 encoding.strtolocal(e2.strerror)))
1176 else:
1176 else:
1177 raise error.Abort(_("couldn't find git for subrepo '%s'")
1177 raise error.Abort(_("couldn't find git for subrepo '%s'")
1178 % self._path, hint=notfoundhint)
1178 % self._path, hint=notfoundhint)
1179 versionstatus = self._checkversion(out)
1179 versionstatus = self._checkversion(out)
1180 if versionstatus == 'unknown':
1180 if versionstatus == 'unknown':
1181 self.ui.warn(_('cannot retrieve git version\n'))
1181 self.ui.warn(_('cannot retrieve git version\n'))
1182 elif versionstatus == 'abort':
1182 elif versionstatus == 'abort':
1183 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1183 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1184 elif versionstatus == 'warning':
1184 elif versionstatus == 'warning':
1185 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1185 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1186
1186
1187 @staticmethod
1187 @staticmethod
1188 def _gitversion(out):
1188 def _gitversion(out):
1189 m = re.search(br'^git version (\d+)\.(\d+)\.(\d+)', out)
1189 m = re.search(br'^git version (\d+)\.(\d+)\.(\d+)', out)
1190 if m:
1190 if m:
1191 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1191 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1192
1192
1193 m = re.search(br'^git version (\d+)\.(\d+)', out)
1193 m = re.search(br'^git version (\d+)\.(\d+)', out)
1194 if m:
1194 if m:
1195 return (int(m.group(1)), int(m.group(2)), 0)
1195 return (int(m.group(1)), int(m.group(2)), 0)
1196
1196
1197 return -1
1197 return -1
1198
1198
1199 @staticmethod
1199 @staticmethod
1200 def _checkversion(out):
1200 def _checkversion(out):
1201 '''ensure git version is new enough
1201 '''ensure git version is new enough
1202
1202
1203 >>> _checkversion = gitsubrepo._checkversion
1203 >>> _checkversion = gitsubrepo._checkversion
1204 >>> _checkversion(b'git version 1.6.0')
1204 >>> _checkversion(b'git version 1.6.0')
1205 'ok'
1205 'ok'
1206 >>> _checkversion(b'git version 1.8.5')
1206 >>> _checkversion(b'git version 1.8.5')
1207 'ok'
1207 'ok'
1208 >>> _checkversion(b'git version 1.4.0')
1208 >>> _checkversion(b'git version 1.4.0')
1209 'abort'
1209 'abort'
1210 >>> _checkversion(b'git version 1.5.0')
1210 >>> _checkversion(b'git version 1.5.0')
1211 'warning'
1211 'warning'
1212 >>> _checkversion(b'git version 1.9-rc0')
1212 >>> _checkversion(b'git version 1.9-rc0')
1213 'ok'
1213 'ok'
1214 >>> _checkversion(b'git version 1.9.0.265.g81cdec2')
1214 >>> _checkversion(b'git version 1.9.0.265.g81cdec2')
1215 'ok'
1215 'ok'
1216 >>> _checkversion(b'git version 1.9.0.GIT')
1216 >>> _checkversion(b'git version 1.9.0.GIT')
1217 'ok'
1217 'ok'
1218 >>> _checkversion(b'git version 12345')
1218 >>> _checkversion(b'git version 12345')
1219 'unknown'
1219 'unknown'
1220 >>> _checkversion(b'no')
1220 >>> _checkversion(b'no')
1221 'unknown'
1221 'unknown'
1222 '''
1222 '''
1223 version = gitsubrepo._gitversion(out)
1223 version = gitsubrepo._gitversion(out)
1224 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1224 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1225 # despite the docstring comment. For now, error on 1.4.0, warn on
1225 # despite the docstring comment. For now, error on 1.4.0, warn on
1226 # 1.5.0 but attempt to continue.
1226 # 1.5.0 but attempt to continue.
1227 if version == -1:
1227 if version == -1:
1228 return 'unknown'
1228 return 'unknown'
1229 if version < (1, 5, 0):
1229 if version < (1, 5, 0):
1230 return 'abort'
1230 return 'abort'
1231 elif version < (1, 6, 0):
1231 elif version < (1, 6, 0):
1232 return 'warning'
1232 return 'warning'
1233 return 'ok'
1233 return 'ok'
1234
1234
1235 def _gitcommand(self, commands, env=None, stream=False):
1235 def _gitcommand(self, commands, env=None, stream=False):
1236 return self._gitdir(commands, env=env, stream=stream)[0]
1236 return self._gitdir(commands, env=env, stream=stream)[0]
1237
1237
1238 def _gitdir(self, commands, env=None, stream=False):
1238 def _gitdir(self, commands, env=None, stream=False):
1239 return self._gitnodir(commands, env=env, stream=stream,
1239 return self._gitnodir(commands, env=env, stream=stream,
1240 cwd=self._abspath)
1240 cwd=self._abspath)
1241
1241
1242 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1242 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1243 """Calls the git command
1243 """Calls the git command
1244
1244
1245 The methods tries to call the git command. versions prior to 1.6.0
1245 The methods tries to call the git command. versions prior to 1.6.0
1246 are not supported and very probably fail.
1246 are not supported and very probably fail.
1247 """
1247 """
1248 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1248 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1249 if env is None:
1249 if env is None:
1250 env = encoding.environ.copy()
1250 env = encoding.environ.copy()
1251 # disable localization for Git output (issue5176)
1251 # disable localization for Git output (issue5176)
1252 env['LC_ALL'] = 'C'
1252 env['LC_ALL'] = 'C'
1253 # fix for Git CVE-2015-7545
1253 # fix for Git CVE-2015-7545
1254 if 'GIT_ALLOW_PROTOCOL' not in env:
1254 if 'GIT_ALLOW_PROTOCOL' not in env:
1255 env['GIT_ALLOW_PROTOCOL'] = 'file:git:http:https:ssh'
1255 env['GIT_ALLOW_PROTOCOL'] = 'file:git:http:https:ssh'
1256 # unless ui.quiet is set, print git's stderr,
1256 # unless ui.quiet is set, print git's stderr,
1257 # which is mostly progress and useful info
1257 # which is mostly progress and useful info
1258 errpipe = None
1258 errpipe = None
1259 if self.ui.quiet:
1259 if self.ui.quiet:
1260 errpipe = open(os.devnull, 'w')
1260 errpipe = open(os.devnull, 'w')
1261 if self.ui._colormode and len(commands) and commands[0] == "diff":
1261 if self.ui._colormode and len(commands) and commands[0] == "diff":
1262 # insert the argument in the front,
1262 # insert the argument in the front,
1263 # the end of git diff arguments is used for paths
1263 # the end of git diff arguments is used for paths
1264 commands.insert(1, '--color')
1264 commands.insert(1, '--color')
1265 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1265 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1266 cwd=cwd, env=env, close_fds=procutil.closefds,
1266 cwd=cwd, env=env, close_fds=procutil.closefds,
1267 stdout=subprocess.PIPE, stderr=errpipe)
1267 stdout=subprocess.PIPE, stderr=errpipe)
1268 if stream:
1268 if stream:
1269 return p.stdout, None
1269 return p.stdout, None
1270
1270
1271 retdata = p.stdout.read().strip()
1271 retdata = p.stdout.read().strip()
1272 # wait for the child to exit to avoid race condition.
1272 # wait for the child to exit to avoid race condition.
1273 p.wait()
1273 p.wait()
1274
1274
1275 if p.returncode != 0 and p.returncode != 1:
1275 if p.returncode != 0 and p.returncode != 1:
1276 # there are certain error codes that are ok
1276 # there are certain error codes that are ok
1277 command = commands[0]
1277 command = commands[0]
1278 if command in ('cat-file', 'symbolic-ref'):
1278 if command in ('cat-file', 'symbolic-ref'):
1279 return retdata, p.returncode
1279 return retdata, p.returncode
1280 # for all others, abort
1280 # for all others, abort
1281 raise error.Abort(_('git %s error %d in %s') %
1281 raise error.Abort(_('git %s error %d in %s') %
1282 (command, p.returncode, self._relpath))
1282 (command, p.returncode, self._relpath))
1283
1283
1284 return retdata, p.returncode
1284 return retdata, p.returncode
1285
1285
1286 def _gitmissing(self):
1286 def _gitmissing(self):
1287 return not self.wvfs.exists('.git')
1287 return not self.wvfs.exists('.git')
1288
1288
1289 def _gitstate(self):
1289 def _gitstate(self):
1290 return self._gitcommand(['rev-parse', 'HEAD'])
1290 return self._gitcommand(['rev-parse', 'HEAD'])
1291
1291
1292 def _gitcurrentbranch(self):
1292 def _gitcurrentbranch(self):
1293 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1293 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1294 if err:
1294 if err:
1295 current = None
1295 current = None
1296 return current
1296 return current
1297
1297
1298 def _gitremote(self, remote):
1298 def _gitremote(self, remote):
1299 out = self._gitcommand(['remote', 'show', '-n', remote])
1299 out = self._gitcommand(['remote', 'show', '-n', remote])
1300 line = out.split('\n')[1]
1300 line = out.split('\n')[1]
1301 i = line.index('URL: ') + len('URL: ')
1301 i = line.index('URL: ') + len('URL: ')
1302 return line[i:]
1302 return line[i:]
1303
1303
1304 def _githavelocally(self, revision):
1304 def _githavelocally(self, revision):
1305 out, code = self._gitdir(['cat-file', '-e', revision])
1305 out, code = self._gitdir(['cat-file', '-e', revision])
1306 return code == 0
1306 return code == 0
1307
1307
1308 def _gitisancestor(self, r1, r2):
1308 def _gitisancestor(self, r1, r2):
1309 base = self._gitcommand(['merge-base', r1, r2])
1309 base = self._gitcommand(['merge-base', r1, r2])
1310 return base == r1
1310 return base == r1
1311
1311
1312 def _gitisbare(self):
1312 def _gitisbare(self):
1313 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1313 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1314
1314
1315 def _gitupdatestat(self):
1315 def _gitupdatestat(self):
1316 """This must be run before git diff-index.
1316 """This must be run before git diff-index.
1317 diff-index only looks at changes to file stat;
1317 diff-index only looks at changes to file stat;
1318 this command looks at file contents and updates the stat."""
1318 this command looks at file contents and updates the stat."""
1319 self._gitcommand(['update-index', '-q', '--refresh'])
1319 self._gitcommand(['update-index', '-q', '--refresh'])
1320
1320
1321 def _gitbranchmap(self):
1321 def _gitbranchmap(self):
1322 '''returns 2 things:
1322 '''returns 2 things:
1323 a map from git branch to revision
1323 a map from git branch to revision
1324 a map from revision to branches'''
1324 a map from revision to branches'''
1325 branch2rev = {}
1325 branch2rev = {}
1326 rev2branch = {}
1326 rev2branch = {}
1327
1327
1328 out = self._gitcommand(['for-each-ref', '--format',
1328 out = self._gitcommand(['for-each-ref', '--format',
1329 '%(objectname) %(refname)'])
1329 '%(objectname) %(refname)'])
1330 for line in out.split('\n'):
1330 for line in out.split('\n'):
1331 revision, ref = line.split(' ')
1331 revision, ref = line.split(' ')
1332 if (not ref.startswith('refs/heads/') and
1332 if (not ref.startswith('refs/heads/') and
1333 not ref.startswith('refs/remotes/')):
1333 not ref.startswith('refs/remotes/')):
1334 continue
1334 continue
1335 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1335 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1336 continue # ignore remote/HEAD redirects
1336 continue # ignore remote/HEAD redirects
1337 branch2rev[ref] = revision
1337 branch2rev[ref] = revision
1338 rev2branch.setdefault(revision, []).append(ref)
1338 rev2branch.setdefault(revision, []).append(ref)
1339 return branch2rev, rev2branch
1339 return branch2rev, rev2branch
1340
1340
1341 def _gittracking(self, branches):
1341 def _gittracking(self, branches):
1342 'return map of remote branch to local tracking branch'
1342 'return map of remote branch to local tracking branch'
1343 # assumes no more than one local tracking branch for each remote
1343 # assumes no more than one local tracking branch for each remote
1344 tracking = {}
1344 tracking = {}
1345 for b in branches:
1345 for b in branches:
1346 if b.startswith('refs/remotes/'):
1346 if b.startswith('refs/remotes/'):
1347 continue
1347 continue
1348 bname = b.split('/', 2)[2]
1348 bname = b.split('/', 2)[2]
1349 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1349 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1350 if remote:
1350 if remote:
1351 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1351 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1352 tracking['refs/remotes/%s/%s' %
1352 tracking['refs/remotes/%s/%s' %
1353 (remote, ref.split('/', 2)[2])] = b
1353 (remote, ref.split('/', 2)[2])] = b
1354 return tracking
1354 return tracking
1355
1355
1356 def _abssource(self, source):
1356 def _abssource(self, source):
1357 if '://' not in source:
1357 if '://' not in source:
1358 # recognize the scp syntax as an absolute source
1358 # recognize the scp syntax as an absolute source
1359 colon = source.find(':')
1359 colon = source.find(':')
1360 if colon != -1 and '/' not in source[:colon]:
1360 if colon != -1 and '/' not in source[:colon]:
1361 return source
1361 return source
1362 self._subsource = source
1362 self._subsource = source
1363 return _abssource(self)
1363 return _abssource(self)
1364
1364
1365 def _fetch(self, source, revision):
1365 def _fetch(self, source, revision):
1366 if self._gitmissing():
1366 if self._gitmissing():
1367 # SEC: check for safe ssh url
1367 # SEC: check for safe ssh url
1368 util.checksafessh(source)
1368 util.checksafessh(source)
1369
1369
1370 source = self._abssource(source)
1370 source = self._abssource(source)
1371 self.ui.status(_('cloning subrepo %s from %s\n') %
1371 self.ui.status(_('cloning subrepo %s from %s\n') %
1372 (self._relpath, source))
1372 (self._relpath, source))
1373 self._gitnodir(['clone', source, self._abspath])
1373 self._gitnodir(['clone', source, self._abspath])
1374 if self._githavelocally(revision):
1374 if self._githavelocally(revision):
1375 return
1375 return
1376 self.ui.status(_('pulling subrepo %s from %s\n') %
1376 self.ui.status(_('pulling subrepo %s from %s\n') %
1377 (self._relpath, self._gitremote('origin')))
1377 (self._relpath, self._gitremote('origin')))
1378 # try only origin: the originally cloned repo
1378 # try only origin: the originally cloned repo
1379 self._gitcommand(['fetch'])
1379 self._gitcommand(['fetch'])
1380 if not self._githavelocally(revision):
1380 if not self._githavelocally(revision):
1381 raise error.Abort(_('revision %s does not exist in subrepository '
1381 raise error.Abort(_('revision %s does not exist in subrepository '
1382 '"%s"\n') % (revision, self._relpath))
1382 '"%s"\n') % (revision, self._relpath))
1383
1383
1384 @annotatesubrepoerror
1384 @annotatesubrepoerror
1385 def dirty(self, ignoreupdate=False, missing=False):
1385 def dirty(self, ignoreupdate=False, missing=False):
1386 if self._gitmissing():
1386 if self._gitmissing():
1387 return self._state[1] != ''
1387 return self._state[1] != ''
1388 if self._gitisbare():
1388 if self._gitisbare():
1389 return True
1389 return True
1390 if not ignoreupdate and self._state[1] != self._gitstate():
1390 if not ignoreupdate and self._state[1] != self._gitstate():
1391 # different version checked out
1391 # different version checked out
1392 return True
1392 return True
1393 # check for staged changes or modified files; ignore untracked files
1393 # check for staged changes or modified files; ignore untracked files
1394 self._gitupdatestat()
1394 self._gitupdatestat()
1395 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1395 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1396 return code == 1
1396 return code == 1
1397
1397
1398 def basestate(self):
1398 def basestate(self):
1399 return self._gitstate()
1399 return self._gitstate()
1400
1400
1401 @annotatesubrepoerror
1401 @annotatesubrepoerror
1402 def get(self, state, overwrite=False):
1402 def get(self, state, overwrite=False):
1403 source, revision, kind = state
1403 source, revision, kind = state
1404 if not revision:
1404 if not revision:
1405 self.remove()
1405 self.remove()
1406 return
1406 return
1407 self._fetch(source, revision)
1407 self._fetch(source, revision)
1408 # if the repo was set to be bare, unbare it
1408 # if the repo was set to be bare, unbare it
1409 if self._gitisbare():
1409 if self._gitisbare():
1410 self._gitcommand(['config', 'core.bare', 'false'])
1410 self._gitcommand(['config', 'core.bare', 'false'])
1411 if self._gitstate() == revision:
1411 if self._gitstate() == revision:
1412 self._gitcommand(['reset', '--hard', 'HEAD'])
1412 self._gitcommand(['reset', '--hard', 'HEAD'])
1413 return
1413 return
1414 elif self._gitstate() == revision:
1414 elif self._gitstate() == revision:
1415 if overwrite:
1415 if overwrite:
1416 # first reset the index to unmark new files for commit, because
1416 # first reset the index to unmark new files for commit, because
1417 # reset --hard will otherwise throw away files added for commit,
1417 # reset --hard will otherwise throw away files added for commit,
1418 # not just unmark them.
1418 # not just unmark them.
1419 self._gitcommand(['reset', 'HEAD'])
1419 self._gitcommand(['reset', 'HEAD'])
1420 self._gitcommand(['reset', '--hard', 'HEAD'])
1420 self._gitcommand(['reset', '--hard', 'HEAD'])
1421 return
1421 return
1422 branch2rev, rev2branch = self._gitbranchmap()
1422 branch2rev, rev2branch = self._gitbranchmap()
1423
1423
1424 def checkout(args):
1424 def checkout(args):
1425 cmd = ['checkout']
1425 cmd = ['checkout']
1426 if overwrite:
1426 if overwrite:
1427 # first reset the index to unmark new files for commit, because
1427 # first reset the index to unmark new files for commit, because
1428 # the -f option will otherwise throw away files added for
1428 # the -f option will otherwise throw away files added for
1429 # commit, not just unmark them.
1429 # commit, not just unmark them.
1430 self._gitcommand(['reset', 'HEAD'])
1430 self._gitcommand(['reset', 'HEAD'])
1431 cmd.append('-f')
1431 cmd.append('-f')
1432 self._gitcommand(cmd + args)
1432 self._gitcommand(cmd + args)
1433 _sanitize(self.ui, self.wvfs, '.git')
1433 _sanitize(self.ui, self.wvfs, '.git')
1434
1434
1435 def rawcheckout():
1435 def rawcheckout():
1436 # no branch to checkout, check it out with no branch
1436 # no branch to checkout, check it out with no branch
1437 self.ui.warn(_('checking out detached HEAD in '
1437 self.ui.warn(_('checking out detached HEAD in '
1438 'subrepository "%s"\n') % self._relpath)
1438 'subrepository "%s"\n') % self._relpath)
1439 self.ui.warn(_('check out a git branch if you intend '
1439 self.ui.warn(_('check out a git branch if you intend '
1440 'to make changes\n'))
1440 'to make changes\n'))
1441 checkout(['-q', revision])
1441 checkout(['-q', revision])
1442
1442
1443 if revision not in rev2branch:
1443 if revision not in rev2branch:
1444 rawcheckout()
1444 rawcheckout()
1445 return
1445 return
1446 branches = rev2branch[revision]
1446 branches = rev2branch[revision]
1447 firstlocalbranch = None
1447 firstlocalbranch = None
1448 for b in branches:
1448 for b in branches:
1449 if b == 'refs/heads/master':
1449 if b == 'refs/heads/master':
1450 # master trumps all other branches
1450 # master trumps all other branches
1451 checkout(['refs/heads/master'])
1451 checkout(['refs/heads/master'])
1452 return
1452 return
1453 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1453 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1454 firstlocalbranch = b
1454 firstlocalbranch = b
1455 if firstlocalbranch:
1455 if firstlocalbranch:
1456 checkout([firstlocalbranch])
1456 checkout([firstlocalbranch])
1457 return
1457 return
1458
1458
1459 tracking = self._gittracking(branch2rev.keys())
1459 tracking = self._gittracking(branch2rev.keys())
1460 # choose a remote branch already tracked if possible
1460 # choose a remote branch already tracked if possible
1461 remote = branches[0]
1461 remote = branches[0]
1462 if remote not in tracking:
1462 if remote not in tracking:
1463 for b in branches:
1463 for b in branches:
1464 if b in tracking:
1464 if b in tracking:
1465 remote = b
1465 remote = b
1466 break
1466 break
1467
1467
1468 if remote not in tracking:
1468 if remote not in tracking:
1469 # create a new local tracking branch
1469 # create a new local tracking branch
1470 local = remote.split('/', 3)[3]
1470 local = remote.split('/', 3)[3]
1471 checkout(['-b', local, remote])
1471 checkout(['-b', local, remote])
1472 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1472 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1473 # When updating to a tracked remote branch,
1473 # When updating to a tracked remote branch,
1474 # if the local tracking branch is downstream of it,
1474 # if the local tracking branch is downstream of it,
1475 # a normal `git pull` would have performed a "fast-forward merge"
1475 # a normal `git pull` would have performed a "fast-forward merge"
1476 # which is equivalent to updating the local branch to the remote.
1476 # which is equivalent to updating the local branch to the remote.
1477 # Since we are only looking at branching at update, we need to
1477 # Since we are only looking at branching at update, we need to
1478 # detect this situation and perform this action lazily.
1478 # detect this situation and perform this action lazily.
1479 if tracking[remote] != self._gitcurrentbranch():
1479 if tracking[remote] != self._gitcurrentbranch():
1480 checkout([tracking[remote]])
1480 checkout([tracking[remote]])
1481 self._gitcommand(['merge', '--ff', remote])
1481 self._gitcommand(['merge', '--ff', remote])
1482 _sanitize(self.ui, self.wvfs, '.git')
1482 _sanitize(self.ui, self.wvfs, '.git')
1483 else:
1483 else:
1484 # a real merge would be required, just checkout the revision
1484 # a real merge would be required, just checkout the revision
1485 rawcheckout()
1485 rawcheckout()
1486
1486
1487 @annotatesubrepoerror
1487 @annotatesubrepoerror
1488 def commit(self, text, user, date):
1488 def commit(self, text, user, date):
1489 if self._gitmissing():
1489 if self._gitmissing():
1490 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1490 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1491 cmd = ['commit', '-a', '-m', text]
1491 cmd = ['commit', '-a', '-m', text]
1492 env = encoding.environ.copy()
1492 env = encoding.environ.copy()
1493 if user:
1493 if user:
1494 cmd += ['--author', user]
1494 cmd += ['--author', user]
1495 if date:
1495 if date:
1496 # git's date parser silently ignores when seconds < 1e9
1496 # git's date parser silently ignores when seconds < 1e9
1497 # convert to ISO8601
1497 # convert to ISO8601
1498 env['GIT_AUTHOR_DATE'] = dateutil.datestr(date,
1498 env['GIT_AUTHOR_DATE'] = dateutil.datestr(date,
1499 '%Y-%m-%dT%H:%M:%S %1%2')
1499 '%Y-%m-%dT%H:%M:%S %1%2')
1500 self._gitcommand(cmd, env=env)
1500 self._gitcommand(cmd, env=env)
1501 # make sure commit works otherwise HEAD might not exist under certain
1501 # make sure commit works otherwise HEAD might not exist under certain
1502 # circumstances
1502 # circumstances
1503 return self._gitstate()
1503 return self._gitstate()
1504
1504
1505 @annotatesubrepoerror
1505 @annotatesubrepoerror
1506 def merge(self, state):
1506 def merge(self, state):
1507 source, revision, kind = state
1507 source, revision, kind = state
1508 self._fetch(source, revision)
1508 self._fetch(source, revision)
1509 base = self._gitcommand(['merge-base', revision, self._state[1]])
1509 base = self._gitcommand(['merge-base', revision, self._state[1]])
1510 self._gitupdatestat()
1510 self._gitupdatestat()
1511 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1511 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1512
1512
1513 def mergefunc():
1513 def mergefunc():
1514 if base == revision:
1514 if base == revision:
1515 self.get(state) # fast forward merge
1515 self.get(state) # fast forward merge
1516 elif base != self._state[1]:
1516 elif base != self._state[1]:
1517 self._gitcommand(['merge', '--no-commit', revision])
1517 self._gitcommand(['merge', '--no-commit', revision])
1518 _sanitize(self.ui, self.wvfs, '.git')
1518 _sanitize(self.ui, self.wvfs, '.git')
1519
1519
1520 if self.dirty():
1520 if self.dirty():
1521 if self._gitstate() != revision:
1521 if self._gitstate() != revision:
1522 dirty = self._gitstate() == self._state[1] or code != 0
1522 dirty = self._gitstate() == self._state[1] or code != 0
1523 if _updateprompt(self.ui, self, dirty,
1523 if _updateprompt(self.ui, self, dirty,
1524 self._state[1][:7], revision[:7]):
1524 self._state[1][:7], revision[:7]):
1525 mergefunc()
1525 mergefunc()
1526 else:
1526 else:
1527 mergefunc()
1527 mergefunc()
1528
1528
1529 @annotatesubrepoerror
1529 @annotatesubrepoerror
1530 def push(self, opts):
1530 def push(self, opts):
1531 force = opts.get('force')
1531 force = opts.get('force')
1532
1532
1533 if not self._state[1]:
1533 if not self._state[1]:
1534 return True
1534 return True
1535 if self._gitmissing():
1535 if self._gitmissing():
1536 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1536 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1537 # if a branch in origin contains the revision, nothing to do
1537 # if a branch in origin contains the revision, nothing to do
1538 branch2rev, rev2branch = self._gitbranchmap()
1538 branch2rev, rev2branch = self._gitbranchmap()
1539 if self._state[1] in rev2branch:
1539 if self._state[1] in rev2branch:
1540 for b in rev2branch[self._state[1]]:
1540 for b in rev2branch[self._state[1]]:
1541 if b.startswith('refs/remotes/origin/'):
1541 if b.startswith('refs/remotes/origin/'):
1542 return True
1542 return True
1543 for b, revision in branch2rev.iteritems():
1543 for b, revision in branch2rev.iteritems():
1544 if b.startswith('refs/remotes/origin/'):
1544 if b.startswith('refs/remotes/origin/'):
1545 if self._gitisancestor(self._state[1], revision):
1545 if self._gitisancestor(self._state[1], revision):
1546 return True
1546 return True
1547 # otherwise, try to push the currently checked out branch
1547 # otherwise, try to push the currently checked out branch
1548 cmd = ['push']
1548 cmd = ['push']
1549 if force:
1549 if force:
1550 cmd.append('--force')
1550 cmd.append('--force')
1551
1551
1552 current = self._gitcurrentbranch()
1552 current = self._gitcurrentbranch()
1553 if current:
1553 if current:
1554 # determine if the current branch is even useful
1554 # determine if the current branch is even useful
1555 if not self._gitisancestor(self._state[1], current):
1555 if not self._gitisancestor(self._state[1], current):
1556 self.ui.warn(_('unrelated git branch checked out '
1556 self.ui.warn(_('unrelated git branch checked out '
1557 'in subrepository "%s"\n') % self._relpath)
1557 'in subrepository "%s"\n') % self._relpath)
1558 return False
1558 return False
1559 self.ui.status(_('pushing branch %s of subrepository "%s"\n') %
1559 self.ui.status(_('pushing branch %s of subrepository "%s"\n') %
1560 (current.split('/', 2)[2], self._relpath))
1560 (current.split('/', 2)[2], self._relpath))
1561 ret = self._gitdir(cmd + ['origin', current])
1561 ret = self._gitdir(cmd + ['origin', current])
1562 return ret[1] == 0
1562 return ret[1] == 0
1563 else:
1563 else:
1564 self.ui.warn(_('no branch checked out in subrepository "%s"\n'
1564 self.ui.warn(_('no branch checked out in subrepository "%s"\n'
1565 'cannot push revision %s\n') %
1565 'cannot push revision %s\n') %
1566 (self._relpath, self._state[1]))
1566 (self._relpath, self._state[1]))
1567 return False
1567 return False
1568
1568
1569 @annotatesubrepoerror
1569 @annotatesubrepoerror
1570 def add(self, ui, match, prefix, explicitonly, **opts):
1570 def add(self, ui, match, prefix, explicitonly, **opts):
1571 if self._gitmissing():
1571 if self._gitmissing():
1572 return []
1572 return []
1573
1573
1574 (modified, added, removed,
1574 (modified, added, removed,
1575 deleted, unknown, ignored, clean) = self.status(None, unknown=True,
1575 deleted, unknown, ignored, clean) = self.status(None, unknown=True,
1576 clean=True)
1576 clean=True)
1577
1577
1578 tracked = set()
1578 tracked = set()
1579 # dirstates 'amn' warn, 'r' is added again
1579 # dirstates 'amn' warn, 'r' is added again
1580 for l in (modified, added, deleted, clean):
1580 for l in (modified, added, deleted, clean):
1581 tracked.update(l)
1581 tracked.update(l)
1582
1582
1583 # Unknown files not of interest will be rejected by the matcher
1583 # Unknown files not of interest will be rejected by the matcher
1584 files = unknown
1584 files = unknown
1585 files.extend(match.files())
1585 files.extend(match.files())
1586
1586
1587 rejected = []
1587 rejected = []
1588
1588
1589 files = [f for f in sorted(set(files)) if match(f)]
1589 files = [f for f in sorted(set(files)) if match(f)]
1590 for f in files:
1590 for f in files:
1591 exact = match.exact(f)
1591 exact = match.exact(f)
1592 command = ["add"]
1592 command = ["add"]
1593 if exact:
1593 if exact:
1594 command.append("-f") #should be added, even if ignored
1594 command.append("-f") #should be added, even if ignored
1595 if ui.verbose or not exact:
1595 if ui.verbose or not exact:
1596 ui.status(_('adding %s\n') % match.rel(f))
1596 ui.status(_('adding %s\n') % match.rel(f))
1597
1597
1598 if f in tracked: # hg prints 'adding' even if already tracked
1598 if f in tracked: # hg prints 'adding' even if already tracked
1599 if exact:
1599 if exact:
1600 rejected.append(f)
1600 rejected.append(f)
1601 continue
1601 continue
1602 if not opts.get(r'dry_run'):
1602 if not opts.get(r'dry_run'):
1603 self._gitcommand(command + [f])
1603 self._gitcommand(command + [f])
1604
1604
1605 for f in rejected:
1605 for f in rejected:
1606 ui.warn(_("%s already tracked!\n") % match.abs(f))
1606 ui.warn(_("%s already tracked!\n") % match.abs(f))
1607
1607
1608 return rejected
1608 return rejected
1609
1609
1610 @annotatesubrepoerror
1610 @annotatesubrepoerror
1611 def remove(self):
1611 def remove(self):
1612 if self._gitmissing():
1612 if self._gitmissing():
1613 return
1613 return
1614 if self.dirty():
1614 if self.dirty():
1615 self.ui.warn(_('not removing repo %s because '
1615 self.ui.warn(_('not removing repo %s because '
1616 'it has changes.\n') % self._relpath)
1616 'it has changes.\n') % self._relpath)
1617 return
1617 return
1618 # we can't fully delete the repository as it may contain
1618 # we can't fully delete the repository as it may contain
1619 # local-only history
1619 # local-only history
1620 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1620 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1621 self._gitcommand(['config', 'core.bare', 'true'])
1621 self._gitcommand(['config', 'core.bare', 'true'])
1622 for f, kind in self.wvfs.readdir():
1622 for f, kind in self.wvfs.readdir():
1623 if f == '.git':
1623 if f == '.git':
1624 continue
1624 continue
1625 if kind == stat.S_IFDIR:
1625 if kind == stat.S_IFDIR:
1626 self.wvfs.rmtree(f)
1626 self.wvfs.rmtree(f)
1627 else:
1627 else:
1628 self.wvfs.unlink(f)
1628 self.wvfs.unlink(f)
1629
1629
1630 def archive(self, archiver, prefix, match=None, decode=True):
1630 def archive(self, archiver, prefix, match=None, decode=True):
1631 total = 0
1631 total = 0
1632 source, revision = self._state
1632 source, revision = self._state
1633 if not revision:
1633 if not revision:
1634 return total
1634 return total
1635 self._fetch(source, revision)
1635 self._fetch(source, revision)
1636
1636
1637 # Parse git's native archive command.
1637 # Parse git's native archive command.
1638 # This should be much faster than manually traversing the trees
1638 # This should be much faster than manually traversing the trees
1639 # and objects with many subprocess calls.
1639 # and objects with many subprocess calls.
1640 tarstream = self._gitcommand(['archive', revision], stream=True)
1640 tarstream = self._gitcommand(['archive', revision], stream=True)
1641 tar = tarfile.open(fileobj=tarstream, mode=r'r|')
1641 tar = tarfile.open(fileobj=tarstream, mode=r'r|')
1642 relpath = subrelpath(self)
1642 relpath = subrelpath(self)
1643 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1643 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1644 for i, info in enumerate(tar):
1644 for i, info in enumerate(tar):
1645 if info.isdir():
1645 if info.isdir():
1646 continue
1646 continue
1647 if match and not match(info.name):
1647 if match and not match(info.name):
1648 continue
1648 continue
1649 if info.issym():
1649 if info.issym():
1650 data = info.linkname
1650 data = info.linkname
1651 else:
1651 else:
1652 data = tar.extractfile(info).read()
1652 data = tar.extractfile(info).read()
1653 archiver.addfile(prefix + self._path + '/' + info.name,
1653 archiver.addfile(prefix + self._path + '/' + info.name,
1654 info.mode, info.issym(), data)
1654 info.mode, info.issym(), data)
1655 total += 1
1655 total += 1
1656 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1656 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1657 unit=_('files'))
1657 unit=_('files'))
1658 self.ui.progress(_('archiving (%s)') % relpath, None)
1658 self.ui.progress(_('archiving (%s)') % relpath, None)
1659 return total
1659 return total
1660
1660
1661
1661
1662 @annotatesubrepoerror
1662 @annotatesubrepoerror
1663 def cat(self, match, fm, fntemplate, prefix, **opts):
1663 def cat(self, match, fm, fntemplate, prefix, **opts):
1664 rev = self._state[1]
1664 rev = self._state[1]
1665 if match.anypats():
1665 if match.anypats():
1666 return 1 #No support for include/exclude yet
1666 return 1 #No support for include/exclude yet
1667
1667
1668 if not match.files():
1668 if not match.files():
1669 return 1
1669 return 1
1670
1670
1671 # TODO: add support for non-plain formatter (see cmdutil.cat())
1671 # TODO: add support for non-plain formatter (see cmdutil.cat())
1672 for f in match.files():
1672 for f in match.files():
1673 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1673 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1674 fp = cmdutil.makefileobj(self._ctx, fntemplate,
1674 fp = cmdutil.makefileobj(self._ctx, fntemplate,
1675 pathname=self.wvfs.reljoin(prefix, f))
1675 pathname=self.wvfs.reljoin(prefix, f))
1676 fp.write(output)
1676 fp.write(output)
1677 fp.close()
1677 fp.close()
1678 return 0
1678 return 0
1679
1679
1680
1680
1681 @annotatesubrepoerror
1681 @annotatesubrepoerror
1682 def status(self, rev2, **opts):
1682 def status(self, rev2, **opts):
1683 rev1 = self._state[1]
1683 rev1 = self._state[1]
1684 if self._gitmissing() or not rev1:
1684 if self._gitmissing() or not rev1:
1685 # if the repo is missing, return no results
1685 # if the repo is missing, return no results
1686 return scmutil.status([], [], [], [], [], [], [])
1686 return scmutil.status([], [], [], [], [], [], [])
1687 modified, added, removed = [], [], []
1687 modified, added, removed = [], [], []
1688 self._gitupdatestat()
1688 self._gitupdatestat()
1689 if rev2:
1689 if rev2:
1690 command = ['diff-tree', '--no-renames', '-r', rev1, rev2]
1690 command = ['diff-tree', '--no-renames', '-r', rev1, rev2]
1691 else:
1691 else:
1692 command = ['diff-index', '--no-renames', rev1]
1692 command = ['diff-index', '--no-renames', rev1]
1693 out = self._gitcommand(command)
1693 out = self._gitcommand(command)
1694 for line in out.split('\n'):
1694 for line in out.split('\n'):
1695 tab = line.find('\t')
1695 tab = line.find('\t')
1696 if tab == -1:
1696 if tab == -1:
1697 continue
1697 continue
1698 status, f = line[tab - 1], line[tab + 1:]
1698 status, f = line[tab - 1:tab], line[tab + 1:]
1699 if status == 'M':
1699 if status == 'M':
1700 modified.append(f)
1700 modified.append(f)
1701 elif status == 'A':
1701 elif status == 'A':
1702 added.append(f)
1702 added.append(f)
1703 elif status == 'D':
1703 elif status == 'D':
1704 removed.append(f)
1704 removed.append(f)
1705
1705
1706 deleted, unknown, ignored, clean = [], [], [], []
1706 deleted, unknown, ignored, clean = [], [], [], []
1707
1707
1708 command = ['status', '--porcelain', '-z']
1708 command = ['status', '--porcelain', '-z']
1709 if opts.get(r'unknown'):
1709 if opts.get(r'unknown'):
1710 command += ['--untracked-files=all']
1710 command += ['--untracked-files=all']
1711 if opts.get(r'ignored'):
1711 if opts.get(r'ignored'):
1712 command += ['--ignored']
1712 command += ['--ignored']
1713 out = self._gitcommand(command)
1713 out = self._gitcommand(command)
1714
1714
1715 changedfiles = set()
1715 changedfiles = set()
1716 changedfiles.update(modified)
1716 changedfiles.update(modified)
1717 changedfiles.update(added)
1717 changedfiles.update(added)
1718 changedfiles.update(removed)
1718 changedfiles.update(removed)
1719 for line in out.split('\0'):
1719 for line in out.split('\0'):
1720 if not line:
1720 if not line:
1721 continue
1721 continue
1722 st = line[0:2]
1722 st = line[0:2]
1723 #moves and copies show 2 files on one line
1723 #moves and copies show 2 files on one line
1724 if line.find('\0') >= 0:
1724 if line.find('\0') >= 0:
1725 filename1, filename2 = line[3:].split('\0')
1725 filename1, filename2 = line[3:].split('\0')
1726 else:
1726 else:
1727 filename1 = line[3:]
1727 filename1 = line[3:]
1728 filename2 = None
1728 filename2 = None
1729
1729
1730 changedfiles.add(filename1)
1730 changedfiles.add(filename1)
1731 if filename2:
1731 if filename2:
1732 changedfiles.add(filename2)
1732 changedfiles.add(filename2)
1733
1733
1734 if st == '??':
1734 if st == '??':
1735 unknown.append(filename1)
1735 unknown.append(filename1)
1736 elif st == '!!':
1736 elif st == '!!':
1737 ignored.append(filename1)
1737 ignored.append(filename1)
1738
1738
1739 if opts.get(r'clean'):
1739 if opts.get(r'clean'):
1740 out = self._gitcommand(['ls-files'])
1740 out = self._gitcommand(['ls-files'])
1741 for f in out.split('\n'):
1741 for f in out.split('\n'):
1742 if not f in changedfiles:
1742 if not f in changedfiles:
1743 clean.append(f)
1743 clean.append(f)
1744
1744
1745 return scmutil.status(modified, added, removed, deleted,
1745 return scmutil.status(modified, added, removed, deleted,
1746 unknown, ignored, clean)
1746 unknown, ignored, clean)
1747
1747
1748 @annotatesubrepoerror
1748 @annotatesubrepoerror
1749 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1749 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1750 node1 = self._state[1]
1750 node1 = self._state[1]
1751 cmd = ['diff', '--no-renames']
1751 cmd = ['diff', '--no-renames']
1752 if opts[r'stat']:
1752 if opts[r'stat']:
1753 cmd.append('--stat')
1753 cmd.append('--stat')
1754 else:
1754 else:
1755 # for Git, this also implies '-p'
1755 # for Git, this also implies '-p'
1756 cmd.append('-U%d' % diffopts.context)
1756 cmd.append('-U%d' % diffopts.context)
1757
1757
1758 gitprefix = self.wvfs.reljoin(prefix, self._path)
1758 gitprefix = self.wvfs.reljoin(prefix, self._path)
1759
1759
1760 if diffopts.noprefix:
1760 if diffopts.noprefix:
1761 cmd.extend(['--src-prefix=%s/' % gitprefix,
1761 cmd.extend(['--src-prefix=%s/' % gitprefix,
1762 '--dst-prefix=%s/' % gitprefix])
1762 '--dst-prefix=%s/' % gitprefix])
1763 else:
1763 else:
1764 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
1764 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
1765 '--dst-prefix=b/%s/' % gitprefix])
1765 '--dst-prefix=b/%s/' % gitprefix])
1766
1766
1767 if diffopts.ignorews:
1767 if diffopts.ignorews:
1768 cmd.append('--ignore-all-space')
1768 cmd.append('--ignore-all-space')
1769 if diffopts.ignorewsamount:
1769 if diffopts.ignorewsamount:
1770 cmd.append('--ignore-space-change')
1770 cmd.append('--ignore-space-change')
1771 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1771 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1772 and diffopts.ignoreblanklines:
1772 and diffopts.ignoreblanklines:
1773 cmd.append('--ignore-blank-lines')
1773 cmd.append('--ignore-blank-lines')
1774
1774
1775 cmd.append(node1)
1775 cmd.append(node1)
1776 if node2:
1776 if node2:
1777 cmd.append(node2)
1777 cmd.append(node2)
1778
1778
1779 output = ""
1779 output = ""
1780 if match.always():
1780 if match.always():
1781 output += self._gitcommand(cmd) + '\n'
1781 output += self._gitcommand(cmd) + '\n'
1782 else:
1782 else:
1783 st = self.status(node2)[:3]
1783 st = self.status(node2)[:3]
1784 files = [f for sublist in st for f in sublist]
1784 files = [f for sublist in st for f in sublist]
1785 for f in files:
1785 for f in files:
1786 if match(f):
1786 if match(f):
1787 output += self._gitcommand(cmd + ['--', f]) + '\n'
1787 output += self._gitcommand(cmd + ['--', f]) + '\n'
1788
1788
1789 if output.strip():
1789 if output.strip():
1790 ui.write(output)
1790 ui.write(output)
1791
1791
1792 @annotatesubrepoerror
1792 @annotatesubrepoerror
1793 def revert(self, substate, *pats, **opts):
1793 def revert(self, substate, *pats, **opts):
1794 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1794 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1795 if not opts.get(r'no_backup'):
1795 if not opts.get(r'no_backup'):
1796 status = self.status(None)
1796 status = self.status(None)
1797 names = status.modified
1797 names = status.modified
1798 for name in names:
1798 for name in names:
1799 bakname = scmutil.origpath(self.ui, self._subparent, name)
1799 bakname = scmutil.origpath(self.ui, self._subparent, name)
1800 self.ui.note(_('saving current version of %s as %s\n') %
1800 self.ui.note(_('saving current version of %s as %s\n') %
1801 (name, bakname))
1801 (name, bakname))
1802 self.wvfs.rename(name, bakname)
1802 self.wvfs.rename(name, bakname)
1803
1803
1804 if not opts.get(r'dry_run'):
1804 if not opts.get(r'dry_run'):
1805 self.get(substate, overwrite=True)
1805 self.get(substate, overwrite=True)
1806 return []
1806 return []
1807
1807
1808 def shortid(self, revid):
1808 def shortid(self, revid):
1809 return revid[:7]
1809 return revid[:7]
1810
1810
1811 types = {
1811 types = {
1812 'hg': hgsubrepo,
1812 'hg': hgsubrepo,
1813 'svn': svnsubrepo,
1813 'svn': svnsubrepo,
1814 'git': gitsubrepo,
1814 'git': gitsubrepo,
1815 }
1815 }
General Comments 0
You need to be logged in to leave comments. Login now