##// END OF EJS Templates
subrepo: add progress bar support to archive
Martin Geisler -
r13144:aae2d5cb default
parent child Browse files
Show More
@@ -1,279 +1,279 b''
1 # archival.py - revision archival for mercurial
1 # archival.py - revision archival for mercurial
2 #
2 #
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.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 i18n import _
8 from i18n import _
9 from node import hex
9 from node import hex
10 import cmdutil
10 import cmdutil
11 import util, encoding
11 import util, encoding
12 import cStringIO, os, stat, tarfile, time, zipfile
12 import cStringIO, os, stat, tarfile, time, zipfile
13 import zlib, gzip
13 import zlib, gzip
14
14
15 def tidyprefix(dest, kind, prefix):
15 def tidyprefix(dest, kind, prefix):
16 '''choose prefix to use for names in archive. make sure prefix is
16 '''choose prefix to use for names in archive. make sure prefix is
17 safe for consumers.'''
17 safe for consumers.'''
18
18
19 if prefix:
19 if prefix:
20 prefix = util.normpath(prefix)
20 prefix = util.normpath(prefix)
21 else:
21 else:
22 if not isinstance(dest, str):
22 if not isinstance(dest, str):
23 raise ValueError('dest must be string if no prefix')
23 raise ValueError('dest must be string if no prefix')
24 prefix = os.path.basename(dest)
24 prefix = os.path.basename(dest)
25 lower = prefix.lower()
25 lower = prefix.lower()
26 for sfx in exts.get(kind, []):
26 for sfx in exts.get(kind, []):
27 if lower.endswith(sfx):
27 if lower.endswith(sfx):
28 prefix = prefix[:-len(sfx)]
28 prefix = prefix[:-len(sfx)]
29 break
29 break
30 lpfx = os.path.normpath(util.localpath(prefix))
30 lpfx = os.path.normpath(util.localpath(prefix))
31 prefix = util.pconvert(lpfx)
31 prefix = util.pconvert(lpfx)
32 if not prefix.endswith('/'):
32 if not prefix.endswith('/'):
33 prefix += '/'
33 prefix += '/'
34 if prefix.startswith('../') or os.path.isabs(lpfx) or '/../' in prefix:
34 if prefix.startswith('../') or os.path.isabs(lpfx) or '/../' in prefix:
35 raise util.Abort(_('archive prefix contains illegal components'))
35 raise util.Abort(_('archive prefix contains illegal components'))
36 return prefix
36 return prefix
37
37
38 exts = {
38 exts = {
39 'tar': ['.tar'],
39 'tar': ['.tar'],
40 'tbz2': ['.tbz2', '.tar.bz2'],
40 'tbz2': ['.tbz2', '.tar.bz2'],
41 'tgz': ['.tgz', '.tar.gz'],
41 'tgz': ['.tgz', '.tar.gz'],
42 'zip': ['.zip'],
42 'zip': ['.zip'],
43 }
43 }
44
44
45 def guesskind(dest):
45 def guesskind(dest):
46 for kind, extensions in exts.iteritems():
46 for kind, extensions in exts.iteritems():
47 if util.any(dest.endswith(ext) for ext in extensions):
47 if util.any(dest.endswith(ext) for ext in extensions):
48 return kind
48 return kind
49 return None
49 return None
50
50
51
51
52 class tarit(object):
52 class tarit(object):
53 '''write archive to tar file or stream. can write uncompressed,
53 '''write archive to tar file or stream. can write uncompressed,
54 or compress with gzip or bzip2.'''
54 or compress with gzip or bzip2.'''
55
55
56 class GzipFileWithTime(gzip.GzipFile):
56 class GzipFileWithTime(gzip.GzipFile):
57
57
58 def __init__(self, *args, **kw):
58 def __init__(self, *args, **kw):
59 timestamp = None
59 timestamp = None
60 if 'timestamp' in kw:
60 if 'timestamp' in kw:
61 timestamp = kw.pop('timestamp')
61 timestamp = kw.pop('timestamp')
62 if timestamp is None:
62 if timestamp is None:
63 self.timestamp = time.time()
63 self.timestamp = time.time()
64 else:
64 else:
65 self.timestamp = timestamp
65 self.timestamp = timestamp
66 gzip.GzipFile.__init__(self, *args, **kw)
66 gzip.GzipFile.__init__(self, *args, **kw)
67
67
68 def _write_gzip_header(self):
68 def _write_gzip_header(self):
69 self.fileobj.write('\037\213') # magic header
69 self.fileobj.write('\037\213') # magic header
70 self.fileobj.write('\010') # compression method
70 self.fileobj.write('\010') # compression method
71 # Python 2.6 deprecates self.filename
71 # Python 2.6 deprecates self.filename
72 fname = getattr(self, 'name', None) or self.filename
72 fname = getattr(self, 'name', None) or self.filename
73 if fname and fname.endswith('.gz'):
73 if fname and fname.endswith('.gz'):
74 fname = fname[:-3]
74 fname = fname[:-3]
75 flags = 0
75 flags = 0
76 if fname:
76 if fname:
77 flags = gzip.FNAME
77 flags = gzip.FNAME
78 self.fileobj.write(chr(flags))
78 self.fileobj.write(chr(flags))
79 gzip.write32u(self.fileobj, long(self.timestamp))
79 gzip.write32u(self.fileobj, long(self.timestamp))
80 self.fileobj.write('\002')
80 self.fileobj.write('\002')
81 self.fileobj.write('\377')
81 self.fileobj.write('\377')
82 if fname:
82 if fname:
83 self.fileobj.write(fname + '\000')
83 self.fileobj.write(fname + '\000')
84
84
85 def __init__(self, dest, mtime, kind=''):
85 def __init__(self, dest, mtime, kind=''):
86 self.mtime = mtime
86 self.mtime = mtime
87
87
88 def taropen(name, mode, fileobj=None):
88 def taropen(name, mode, fileobj=None):
89 if kind == 'gz':
89 if kind == 'gz':
90 mode = mode[0]
90 mode = mode[0]
91 if not fileobj:
91 if not fileobj:
92 fileobj = open(name, mode + 'b')
92 fileobj = open(name, mode + 'b')
93 gzfileobj = self.GzipFileWithTime(name, mode + 'b',
93 gzfileobj = self.GzipFileWithTime(name, mode + 'b',
94 zlib.Z_BEST_COMPRESSION,
94 zlib.Z_BEST_COMPRESSION,
95 fileobj, timestamp=mtime)
95 fileobj, timestamp=mtime)
96 return tarfile.TarFile.taropen(name, mode, gzfileobj)
96 return tarfile.TarFile.taropen(name, mode, gzfileobj)
97 else:
97 else:
98 return tarfile.open(name, mode + kind, fileobj)
98 return tarfile.open(name, mode + kind, fileobj)
99
99
100 if isinstance(dest, str):
100 if isinstance(dest, str):
101 self.z = taropen(dest, mode='w:')
101 self.z = taropen(dest, mode='w:')
102 else:
102 else:
103 # Python 2.5-2.5.1 have a regression that requires a name arg
103 # Python 2.5-2.5.1 have a regression that requires a name arg
104 self.z = taropen(name='', mode='w|', fileobj=dest)
104 self.z = taropen(name='', mode='w|', fileobj=dest)
105
105
106 def addfile(self, name, mode, islink, data):
106 def addfile(self, name, mode, islink, data):
107 i = tarfile.TarInfo(name)
107 i = tarfile.TarInfo(name)
108 i.mtime = self.mtime
108 i.mtime = self.mtime
109 i.size = len(data)
109 i.size = len(data)
110 if islink:
110 if islink:
111 i.type = tarfile.SYMTYPE
111 i.type = tarfile.SYMTYPE
112 i.mode = 0777
112 i.mode = 0777
113 i.linkname = data
113 i.linkname = data
114 data = None
114 data = None
115 i.size = 0
115 i.size = 0
116 else:
116 else:
117 i.mode = mode
117 i.mode = mode
118 data = cStringIO.StringIO(data)
118 data = cStringIO.StringIO(data)
119 self.z.addfile(i, data)
119 self.z.addfile(i, data)
120
120
121 def done(self):
121 def done(self):
122 self.z.close()
122 self.z.close()
123
123
124 class tellable(object):
124 class tellable(object):
125 '''provide tell method for zipfile.ZipFile when writing to http
125 '''provide tell method for zipfile.ZipFile when writing to http
126 response file object.'''
126 response file object.'''
127
127
128 def __init__(self, fp):
128 def __init__(self, fp):
129 self.fp = fp
129 self.fp = fp
130 self.offset = 0
130 self.offset = 0
131
131
132 def __getattr__(self, key):
132 def __getattr__(self, key):
133 return getattr(self.fp, key)
133 return getattr(self.fp, key)
134
134
135 def write(self, s):
135 def write(self, s):
136 self.fp.write(s)
136 self.fp.write(s)
137 self.offset += len(s)
137 self.offset += len(s)
138
138
139 def tell(self):
139 def tell(self):
140 return self.offset
140 return self.offset
141
141
142 class zipit(object):
142 class zipit(object):
143 '''write archive to zip file or stream. can write uncompressed,
143 '''write archive to zip file or stream. can write uncompressed,
144 or compressed with deflate.'''
144 or compressed with deflate.'''
145
145
146 def __init__(self, dest, mtime, compress=True):
146 def __init__(self, dest, mtime, compress=True):
147 if not isinstance(dest, str):
147 if not isinstance(dest, str):
148 try:
148 try:
149 dest.tell()
149 dest.tell()
150 except (AttributeError, IOError):
150 except (AttributeError, IOError):
151 dest = tellable(dest)
151 dest = tellable(dest)
152 self.z = zipfile.ZipFile(dest, 'w',
152 self.z = zipfile.ZipFile(dest, 'w',
153 compress and zipfile.ZIP_DEFLATED or
153 compress and zipfile.ZIP_DEFLATED or
154 zipfile.ZIP_STORED)
154 zipfile.ZIP_STORED)
155
155
156 # Python's zipfile module emits deprecation warnings if we try
156 # Python's zipfile module emits deprecation warnings if we try
157 # to store files with a date before 1980.
157 # to store files with a date before 1980.
158 epoch = 315532800 # calendar.timegm((1980, 1, 1, 0, 0, 0, 1, 1, 0))
158 epoch = 315532800 # calendar.timegm((1980, 1, 1, 0, 0, 0, 1, 1, 0))
159 if mtime < epoch:
159 if mtime < epoch:
160 mtime = epoch
160 mtime = epoch
161
161
162 self.date_time = time.gmtime(mtime)[:6]
162 self.date_time = time.gmtime(mtime)[:6]
163
163
164 def addfile(self, name, mode, islink, data):
164 def addfile(self, name, mode, islink, data):
165 i = zipfile.ZipInfo(name, self.date_time)
165 i = zipfile.ZipInfo(name, self.date_time)
166 i.compress_type = self.z.compression
166 i.compress_type = self.z.compression
167 # unzip will not honor unix file modes unless file creator is
167 # unzip will not honor unix file modes unless file creator is
168 # set to unix (id 3).
168 # set to unix (id 3).
169 i.create_system = 3
169 i.create_system = 3
170 ftype = stat.S_IFREG
170 ftype = stat.S_IFREG
171 if islink:
171 if islink:
172 mode = 0777
172 mode = 0777
173 ftype = stat.S_IFLNK
173 ftype = stat.S_IFLNK
174 i.external_attr = (mode | ftype) << 16L
174 i.external_attr = (mode | ftype) << 16L
175 self.z.writestr(i, data)
175 self.z.writestr(i, data)
176
176
177 def done(self):
177 def done(self):
178 self.z.close()
178 self.z.close()
179
179
180 class fileit(object):
180 class fileit(object):
181 '''write archive as files in directory.'''
181 '''write archive as files in directory.'''
182
182
183 def __init__(self, name, mtime):
183 def __init__(self, name, mtime):
184 self.basedir = name
184 self.basedir = name
185 self.opener = util.opener(self.basedir)
185 self.opener = util.opener(self.basedir)
186
186
187 def addfile(self, name, mode, islink, data):
187 def addfile(self, name, mode, islink, data):
188 if islink:
188 if islink:
189 self.opener.symlink(data, name)
189 self.opener.symlink(data, name)
190 return
190 return
191 f = self.opener(name, "w", atomictemp=True)
191 f = self.opener(name, "w", atomictemp=True)
192 f.write(data)
192 f.write(data)
193 f.rename()
193 f.rename()
194 destfile = os.path.join(self.basedir, name)
194 destfile = os.path.join(self.basedir, name)
195 os.chmod(destfile, mode)
195 os.chmod(destfile, mode)
196
196
197 def done(self):
197 def done(self):
198 pass
198 pass
199
199
200 archivers = {
200 archivers = {
201 'files': fileit,
201 'files': fileit,
202 'tar': tarit,
202 'tar': tarit,
203 'tbz2': lambda name, mtime: tarit(name, mtime, 'bz2'),
203 'tbz2': lambda name, mtime: tarit(name, mtime, 'bz2'),
204 'tgz': lambda name, mtime: tarit(name, mtime, 'gz'),
204 'tgz': lambda name, mtime: tarit(name, mtime, 'gz'),
205 'uzip': lambda name, mtime: zipit(name, mtime, False),
205 'uzip': lambda name, mtime: zipit(name, mtime, False),
206 'zip': zipit,
206 'zip': zipit,
207 }
207 }
208
208
209 def archive(repo, dest, node, kind, decode=True, matchfn=None,
209 def archive(repo, dest, node, kind, decode=True, matchfn=None,
210 prefix=None, mtime=None, subrepos=False):
210 prefix=None, mtime=None, subrepos=False):
211 '''create archive of repo as it was at node.
211 '''create archive of repo as it was at node.
212
212
213 dest can be name of directory, name of archive file, or file
213 dest can be name of directory, name of archive file, or file
214 object to write archive to.
214 object to write archive to.
215
215
216 kind is type of archive to create.
216 kind is type of archive to create.
217
217
218 decode tells whether to put files through decode filters from
218 decode tells whether to put files through decode filters from
219 hgrc.
219 hgrc.
220
220
221 matchfn is function to filter names of files to write to archive.
221 matchfn is function to filter names of files to write to archive.
222
222
223 prefix is name of path to put before every archive member.'''
223 prefix is name of path to put before every archive member.'''
224
224
225 if kind == 'files':
225 if kind == 'files':
226 if prefix:
226 if prefix:
227 raise util.Abort(_('cannot give prefix when archiving to files'))
227 raise util.Abort(_('cannot give prefix when archiving to files'))
228 else:
228 else:
229 prefix = tidyprefix(dest, kind, prefix)
229 prefix = tidyprefix(dest, kind, prefix)
230
230
231 def write(name, mode, islink, getdata):
231 def write(name, mode, islink, getdata):
232 if matchfn and not matchfn(name):
232 if matchfn and not matchfn(name):
233 return
233 return
234 data = getdata()
234 data = getdata()
235 if decode:
235 if decode:
236 data = repo.wwritedata(name, data)
236 data = repo.wwritedata(name, data)
237 archiver.addfile(prefix + name, mode, islink, data)
237 archiver.addfile(prefix + name, mode, islink, data)
238
238
239 if kind not in archivers:
239 if kind not in archivers:
240 raise util.Abort(_("unknown archive type '%s'") % kind)
240 raise util.Abort(_("unknown archive type '%s'") % kind)
241
241
242 ctx = repo[node]
242 ctx = repo[node]
243 archiver = archivers[kind](dest, mtime or ctx.date()[0])
243 archiver = archivers[kind](dest, mtime or ctx.date()[0])
244
244
245 if repo.ui.configbool("ui", "archivemeta", True):
245 if repo.ui.configbool("ui", "archivemeta", True):
246 def metadata():
246 def metadata():
247 base = 'repo: %s\nnode: %s\nbranch: %s\n' % (
247 base = 'repo: %s\nnode: %s\nbranch: %s\n' % (
248 repo[0].hex(), hex(node), encoding.fromlocal(ctx.branch()))
248 repo[0].hex(), hex(node), encoding.fromlocal(ctx.branch()))
249
249
250 tags = ''.join('tag: %s\n' % t for t in ctx.tags()
250 tags = ''.join('tag: %s\n' % t for t in ctx.tags()
251 if repo.tagtype(t) == 'global')
251 if repo.tagtype(t) == 'global')
252 if not tags:
252 if not tags:
253 repo.ui.pushbuffer()
253 repo.ui.pushbuffer()
254 opts = {'template': '{latesttag}\n{latesttagdistance}',
254 opts = {'template': '{latesttag}\n{latesttagdistance}',
255 'style': '', 'patch': None, 'git': None}
255 'style': '', 'patch': None, 'git': None}
256 cmdutil.show_changeset(repo.ui, repo, opts).show(ctx)
256 cmdutil.show_changeset(repo.ui, repo, opts).show(ctx)
257 ltags, dist = repo.ui.popbuffer().split('\n')
257 ltags, dist = repo.ui.popbuffer().split('\n')
258 tags = ''.join('latesttag: %s\n' % t for t in ltags.split(':'))
258 tags = ''.join('latesttag: %s\n' % t for t in ltags.split(':'))
259 tags += 'latesttagdistance: %s\n' % dist
259 tags += 'latesttagdistance: %s\n' % dist
260
260
261 return base + tags
261 return base + tags
262
262
263 write('.hg_archival.txt', 0644, False, metadata)
263 write('.hg_archival.txt', 0644, False, metadata)
264
264
265 total = len(ctx.manifest())
265 total = len(ctx.manifest())
266 repo.ui.progress(_('archiving'), 0, unit=_('files'), total=total)
266 repo.ui.progress(_('archiving'), 0, unit=_('files'), total=total)
267 for i, f in enumerate(ctx):
267 for i, f in enumerate(ctx):
268 ff = ctx.flags(f)
268 ff = ctx.flags(f)
269 write(f, 'x' in ff and 0755 or 0644, 'l' in ff, ctx[f].data)
269 write(f, 'x' in ff and 0755 or 0644, 'l' in ff, ctx[f].data)
270 repo.ui.progress(_('archiving'), i + 1, item=f,
270 repo.ui.progress(_('archiving'), i + 1, item=f,
271 unit=_('files'), total=total)
271 unit=_('files'), total=total)
272 repo.ui.progress(_('archiving'), None)
272 repo.ui.progress(_('archiving'), None)
273
273
274 if subrepos:
274 if subrepos:
275 for subpath in ctx.substate:
275 for subpath in ctx.substate:
276 sub = ctx.sub(subpath)
276 sub = ctx.sub(subpath)
277 sub.archive(archiver, prefix)
277 sub.archive(repo.ui, archiver, prefix)
278
278
279 archiver.done()
279 archiver.done()
@@ -1,881 +1,895 b''
1 # subrepo.py - sub-repository handling for Mercurial
1 # subrepo.py - sub-repository handling for Mercurial
2 #
2 #
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import errno, os, re, xml.dom.minidom, shutil, urlparse, posixpath
8 import errno, os, re, xml.dom.minidom, shutil, urlparse, posixpath
9 import stat, subprocess, tarfile
9 import stat, subprocess, tarfile
10 from i18n import _
10 from i18n import _
11 import config, util, node, error, cmdutil
11 import config, util, node, error, cmdutil
12 hg = None
12 hg = None
13
13
14 nullstate = ('', '', 'empty')
14 nullstate = ('', '', 'empty')
15
15
16 def state(ctx, ui):
16 def state(ctx, ui):
17 """return a state dict, mapping subrepo paths configured in .hgsub
17 """return a state dict, mapping subrepo paths configured in .hgsub
18 to tuple: (source from .hgsub, revision from .hgsubstate, kind
18 to tuple: (source from .hgsub, revision from .hgsubstate, kind
19 (key in types dict))
19 (key in types dict))
20 """
20 """
21 p = config.config()
21 p = config.config()
22 def read(f, sections=None, remap=None):
22 def read(f, sections=None, remap=None):
23 if f in ctx:
23 if f in ctx:
24 try:
24 try:
25 data = ctx[f].data()
25 data = ctx[f].data()
26 except IOError, err:
26 except IOError, err:
27 if err.errno != errno.ENOENT:
27 if err.errno != errno.ENOENT:
28 raise
28 raise
29 # handle missing subrepo spec files as removed
29 # handle missing subrepo spec files as removed
30 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
30 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
31 return
31 return
32 p.parse(f, data, sections, remap, read)
32 p.parse(f, data, sections, remap, read)
33 else:
33 else:
34 raise util.Abort(_("subrepo spec file %s not found") % f)
34 raise util.Abort(_("subrepo spec file %s not found") % f)
35
35
36 if '.hgsub' in ctx:
36 if '.hgsub' in ctx:
37 read('.hgsub')
37 read('.hgsub')
38
38
39 for path, src in ui.configitems('subpaths'):
39 for path, src in ui.configitems('subpaths'):
40 p.set('subpaths', path, src, ui.configsource('subpaths', path))
40 p.set('subpaths', path, src, ui.configsource('subpaths', path))
41
41
42 rev = {}
42 rev = {}
43 if '.hgsubstate' in ctx:
43 if '.hgsubstate' in ctx:
44 try:
44 try:
45 for l in ctx['.hgsubstate'].data().splitlines():
45 for l in ctx['.hgsubstate'].data().splitlines():
46 revision, path = l.split(" ", 1)
46 revision, path = l.split(" ", 1)
47 rev[path] = revision
47 rev[path] = revision
48 except IOError, err:
48 except IOError, err:
49 if err.errno != errno.ENOENT:
49 if err.errno != errno.ENOENT:
50 raise
50 raise
51
51
52 state = {}
52 state = {}
53 for path, src in p[''].items():
53 for path, src in p[''].items():
54 kind = 'hg'
54 kind = 'hg'
55 if src.startswith('['):
55 if src.startswith('['):
56 if ']' not in src:
56 if ']' not in src:
57 raise util.Abort(_('missing ] in subrepo source'))
57 raise util.Abort(_('missing ] in subrepo source'))
58 kind, src = src.split(']', 1)
58 kind, src = src.split(']', 1)
59 kind = kind[1:]
59 kind = kind[1:]
60
60
61 for pattern, repl in p.items('subpaths'):
61 for pattern, repl in p.items('subpaths'):
62 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
62 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
63 # does a string decode.
63 # does a string decode.
64 repl = repl.encode('string-escape')
64 repl = repl.encode('string-escape')
65 # However, we still want to allow back references to go
65 # However, we still want to allow back references to go
66 # through unharmed, so we turn r'\\1' into r'\1'. Again,
66 # through unharmed, so we turn r'\\1' into r'\1'. Again,
67 # extra escapes are needed because re.sub string decodes.
67 # extra escapes are needed because re.sub string decodes.
68 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
68 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
69 try:
69 try:
70 src = re.sub(pattern, repl, src, 1)
70 src = re.sub(pattern, repl, src, 1)
71 except re.error, e:
71 except re.error, e:
72 raise util.Abort(_("bad subrepository pattern in %s: %s")
72 raise util.Abort(_("bad subrepository pattern in %s: %s")
73 % (p.source('subpaths', pattern), e))
73 % (p.source('subpaths', pattern), e))
74
74
75 state[path] = (src.strip(), rev.get(path, ''), kind)
75 state[path] = (src.strip(), rev.get(path, ''), kind)
76
76
77 return state
77 return state
78
78
79 def writestate(repo, state):
79 def writestate(repo, state):
80 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
80 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
81 repo.wwrite('.hgsubstate',
81 repo.wwrite('.hgsubstate',
82 ''.join(['%s %s\n' % (state[s][1], s)
82 ''.join(['%s %s\n' % (state[s][1], s)
83 for s in sorted(state)]), '')
83 for s in sorted(state)]), '')
84
84
85 def submerge(repo, wctx, mctx, actx):
85 def submerge(repo, wctx, mctx, actx):
86 """delegated from merge.applyupdates: merging of .hgsubstate file
86 """delegated from merge.applyupdates: merging of .hgsubstate file
87 in working context, merging context and ancestor context"""
87 in working context, merging context and ancestor context"""
88 if mctx == actx: # backwards?
88 if mctx == actx: # backwards?
89 actx = wctx.p1()
89 actx = wctx.p1()
90 s1 = wctx.substate
90 s1 = wctx.substate
91 s2 = mctx.substate
91 s2 = mctx.substate
92 sa = actx.substate
92 sa = actx.substate
93 sm = {}
93 sm = {}
94
94
95 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
95 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
96
96
97 def debug(s, msg, r=""):
97 def debug(s, msg, r=""):
98 if r:
98 if r:
99 r = "%s:%s:%s" % r
99 r = "%s:%s:%s" % r
100 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
100 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
101
101
102 for s, l in s1.items():
102 for s, l in s1.items():
103 a = sa.get(s, nullstate)
103 a = sa.get(s, nullstate)
104 ld = l # local state with possible dirty flag for compares
104 ld = l # local state with possible dirty flag for compares
105 if wctx.sub(s).dirty():
105 if wctx.sub(s).dirty():
106 ld = (l[0], l[1] + "+")
106 ld = (l[0], l[1] + "+")
107 if wctx == actx: # overwrite
107 if wctx == actx: # overwrite
108 a = ld
108 a = ld
109
109
110 if s in s2:
110 if s in s2:
111 r = s2[s]
111 r = s2[s]
112 if ld == r or r == a: # no change or local is newer
112 if ld == r or r == a: # no change or local is newer
113 sm[s] = l
113 sm[s] = l
114 continue
114 continue
115 elif ld == a: # other side changed
115 elif ld == a: # other side changed
116 debug(s, "other changed, get", r)
116 debug(s, "other changed, get", r)
117 wctx.sub(s).get(r)
117 wctx.sub(s).get(r)
118 sm[s] = r
118 sm[s] = r
119 elif ld[0] != r[0]: # sources differ
119 elif ld[0] != r[0]: # sources differ
120 if repo.ui.promptchoice(
120 if repo.ui.promptchoice(
121 _(' subrepository sources for %s differ\n'
121 _(' subrepository sources for %s differ\n'
122 'use (l)ocal source (%s) or (r)emote source (%s)?')
122 'use (l)ocal source (%s) or (r)emote source (%s)?')
123 % (s, l[0], r[0]),
123 % (s, l[0], r[0]),
124 (_('&Local'), _('&Remote')), 0):
124 (_('&Local'), _('&Remote')), 0):
125 debug(s, "prompt changed, get", r)
125 debug(s, "prompt changed, get", r)
126 wctx.sub(s).get(r)
126 wctx.sub(s).get(r)
127 sm[s] = r
127 sm[s] = r
128 elif ld[1] == a[1]: # local side is unchanged
128 elif ld[1] == a[1]: # local side is unchanged
129 debug(s, "other side changed, get", r)
129 debug(s, "other side changed, get", r)
130 wctx.sub(s).get(r)
130 wctx.sub(s).get(r)
131 sm[s] = r
131 sm[s] = r
132 else:
132 else:
133 debug(s, "both sides changed, merge with", r)
133 debug(s, "both sides changed, merge with", r)
134 wctx.sub(s).merge(r)
134 wctx.sub(s).merge(r)
135 sm[s] = l
135 sm[s] = l
136 elif ld == a: # remote removed, local unchanged
136 elif ld == a: # remote removed, local unchanged
137 debug(s, "remote removed, remove")
137 debug(s, "remote removed, remove")
138 wctx.sub(s).remove()
138 wctx.sub(s).remove()
139 else:
139 else:
140 if repo.ui.promptchoice(
140 if repo.ui.promptchoice(
141 _(' local changed subrepository %s which remote removed\n'
141 _(' local changed subrepository %s which remote removed\n'
142 'use (c)hanged version or (d)elete?') % s,
142 'use (c)hanged version or (d)elete?') % s,
143 (_('&Changed'), _('&Delete')), 0):
143 (_('&Changed'), _('&Delete')), 0):
144 debug(s, "prompt remove")
144 debug(s, "prompt remove")
145 wctx.sub(s).remove()
145 wctx.sub(s).remove()
146
146
147 for s, r in s2.items():
147 for s, r in s2.items():
148 if s in s1:
148 if s in s1:
149 continue
149 continue
150 elif s not in sa:
150 elif s not in sa:
151 debug(s, "remote added, get", r)
151 debug(s, "remote added, get", r)
152 mctx.sub(s).get(r)
152 mctx.sub(s).get(r)
153 sm[s] = r
153 sm[s] = r
154 elif r != sa[s]:
154 elif r != sa[s]:
155 if repo.ui.promptchoice(
155 if repo.ui.promptchoice(
156 _(' remote changed subrepository %s which local removed\n'
156 _(' remote changed subrepository %s which local removed\n'
157 'use (c)hanged version or (d)elete?') % s,
157 'use (c)hanged version or (d)elete?') % s,
158 (_('&Changed'), _('&Delete')), 0) == 0:
158 (_('&Changed'), _('&Delete')), 0) == 0:
159 debug(s, "prompt recreate", r)
159 debug(s, "prompt recreate", r)
160 wctx.sub(s).get(r)
160 wctx.sub(s).get(r)
161 sm[s] = r
161 sm[s] = r
162
162
163 # record merged .hgsubstate
163 # record merged .hgsubstate
164 writestate(repo, sm)
164 writestate(repo, sm)
165
165
166 def reporelpath(repo):
166 def reporelpath(repo):
167 """return path to this (sub)repo as seen from outermost repo"""
167 """return path to this (sub)repo as seen from outermost repo"""
168 parent = repo
168 parent = repo
169 while hasattr(parent, '_subparent'):
169 while hasattr(parent, '_subparent'):
170 parent = parent._subparent
170 parent = parent._subparent
171 return repo.root[len(parent.root)+1:]
171 return repo.root[len(parent.root)+1:]
172
172
173 def subrelpath(sub):
173 def subrelpath(sub):
174 """return path to this subrepo as seen from outermost repo"""
174 """return path to this subrepo as seen from outermost repo"""
175 if not hasattr(sub, '_repo'):
175 if not hasattr(sub, '_repo'):
176 return sub._path
176 return sub._path
177 return reporelpath(sub._repo)
177 return reporelpath(sub._repo)
178
178
179 def _abssource(repo, push=False, abort=True):
179 def _abssource(repo, push=False, abort=True):
180 """return pull/push path of repo - either based on parent repo .hgsub info
180 """return pull/push path of repo - either based on parent repo .hgsub info
181 or on the top repo config. Abort or return None if no source found."""
181 or on the top repo config. Abort or return None if no source found."""
182 if hasattr(repo, '_subparent'):
182 if hasattr(repo, '_subparent'):
183 source = repo._subsource
183 source = repo._subsource
184 if source.startswith('/') or '://' in source:
184 if source.startswith('/') or '://' in source:
185 return source
185 return source
186 parent = _abssource(repo._subparent, push, abort=False)
186 parent = _abssource(repo._subparent, push, abort=False)
187 if parent:
187 if parent:
188 if '://' in parent:
188 if '://' in parent:
189 if parent[-1] == '/':
189 if parent[-1] == '/':
190 parent = parent[:-1]
190 parent = parent[:-1]
191 r = urlparse.urlparse(parent + '/' + source)
191 r = urlparse.urlparse(parent + '/' + source)
192 r = urlparse.urlunparse((r[0], r[1],
192 r = urlparse.urlunparse((r[0], r[1],
193 posixpath.normpath(r[2]),
193 posixpath.normpath(r[2]),
194 r[3], r[4], r[5]))
194 r[3], r[4], r[5]))
195 return r
195 return r
196 else: # plain file system path
196 else: # plain file system path
197 return posixpath.normpath(os.path.join(parent, repo._subsource))
197 return posixpath.normpath(os.path.join(parent, repo._subsource))
198 else: # recursion reached top repo
198 else: # recursion reached top repo
199 if hasattr(repo, '_subtoppath'):
199 if hasattr(repo, '_subtoppath'):
200 return repo._subtoppath
200 return repo._subtoppath
201 if push and repo.ui.config('paths', 'default-push'):
201 if push and repo.ui.config('paths', 'default-push'):
202 return repo.ui.config('paths', 'default-push')
202 return repo.ui.config('paths', 'default-push')
203 if repo.ui.config('paths', 'default'):
203 if repo.ui.config('paths', 'default'):
204 return repo.ui.config('paths', 'default')
204 return repo.ui.config('paths', 'default')
205 if abort:
205 if abort:
206 raise util.Abort(_("default path for subrepository %s not found") %
206 raise util.Abort(_("default path for subrepository %s not found") %
207 reporelpath(repo))
207 reporelpath(repo))
208
208
209 def itersubrepos(ctx1, ctx2):
209 def itersubrepos(ctx1, ctx2):
210 """find subrepos in ctx1 or ctx2"""
210 """find subrepos in ctx1 or ctx2"""
211 # Create a (subpath, ctx) mapping where we prefer subpaths from
211 # Create a (subpath, ctx) mapping where we prefer subpaths from
212 # ctx1. The subpaths from ctx2 are important when the .hgsub file
212 # ctx1. The subpaths from ctx2 are important when the .hgsub file
213 # has been modified (in ctx2) but not yet committed (in ctx1).
213 # has been modified (in ctx2) but not yet committed (in ctx1).
214 subpaths = dict.fromkeys(ctx2.substate, ctx2)
214 subpaths = dict.fromkeys(ctx2.substate, ctx2)
215 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
215 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
216 for subpath, ctx in sorted(subpaths.iteritems()):
216 for subpath, ctx in sorted(subpaths.iteritems()):
217 yield subpath, ctx.sub(subpath)
217 yield subpath, ctx.sub(subpath)
218
218
219 def subrepo(ctx, path):
219 def subrepo(ctx, path):
220 """return instance of the right subrepo class for subrepo in path"""
220 """return instance of the right subrepo class for subrepo in path"""
221 # subrepo inherently violates our import layering rules
221 # subrepo inherently violates our import layering rules
222 # because it wants to make repo objects from deep inside the stack
222 # because it wants to make repo objects from deep inside the stack
223 # so we manually delay the circular imports to not break
223 # so we manually delay the circular imports to not break
224 # scripts that don't use our demand-loading
224 # scripts that don't use our demand-loading
225 global hg
225 global hg
226 import hg as h
226 import hg as h
227 hg = h
227 hg = h
228
228
229 util.path_auditor(ctx._repo.root)(path)
229 util.path_auditor(ctx._repo.root)(path)
230 state = ctx.substate.get(path, nullstate)
230 state = ctx.substate.get(path, nullstate)
231 if state[2] not in types:
231 if state[2] not in types:
232 raise util.Abort(_('unknown subrepo type %s') % state[2])
232 raise util.Abort(_('unknown subrepo type %s') % state[2])
233 return types[state[2]](ctx, path, state[:2])
233 return types[state[2]](ctx, path, state[:2])
234
234
235 # subrepo classes need to implement the following abstract class:
235 # subrepo classes need to implement the following abstract class:
236
236
237 class abstractsubrepo(object):
237 class abstractsubrepo(object):
238
238
239 def dirty(self):
239 def dirty(self):
240 """returns true if the dirstate of the subrepo does not match
240 """returns true if the dirstate of the subrepo does not match
241 current stored state
241 current stored state
242 """
242 """
243 raise NotImplementedError
243 raise NotImplementedError
244
244
245 def checknested(self, path):
245 def checknested(self, path):
246 """check if path is a subrepository within this repository"""
246 """check if path is a subrepository within this repository"""
247 return False
247 return False
248
248
249 def commit(self, text, user, date):
249 def commit(self, text, user, date):
250 """commit the current changes to the subrepo with the given
250 """commit the current changes to the subrepo with the given
251 log message. Use given user and date if possible. Return the
251 log message. Use given user and date if possible. Return the
252 new state of the subrepo.
252 new state of the subrepo.
253 """
253 """
254 raise NotImplementedError
254 raise NotImplementedError
255
255
256 def remove(self):
256 def remove(self):
257 """remove the subrepo
257 """remove the subrepo
258
258
259 (should verify the dirstate is not dirty first)
259 (should verify the dirstate is not dirty first)
260 """
260 """
261 raise NotImplementedError
261 raise NotImplementedError
262
262
263 def get(self, state):
263 def get(self, state):
264 """run whatever commands are needed to put the subrepo into
264 """run whatever commands are needed to put the subrepo into
265 this state
265 this state
266 """
266 """
267 raise NotImplementedError
267 raise NotImplementedError
268
268
269 def merge(self, state):
269 def merge(self, state):
270 """merge currently-saved state with the new state."""
270 """merge currently-saved state with the new state."""
271 raise NotImplementedError
271 raise NotImplementedError
272
272
273 def push(self, force):
273 def push(self, force):
274 """perform whatever action is analogous to 'hg push'
274 """perform whatever action is analogous to 'hg push'
275
275
276 This may be a no-op on some systems.
276 This may be a no-op on some systems.
277 """
277 """
278 raise NotImplementedError
278 raise NotImplementedError
279
279
280 def add(self, ui, match, dryrun, prefix):
280 def add(self, ui, match, dryrun, prefix):
281 return []
281 return []
282
282
283 def status(self, rev2, **opts):
283 def status(self, rev2, **opts):
284 return [], [], [], [], [], [], []
284 return [], [], [], [], [], [], []
285
285
286 def diff(self, diffopts, node2, match, prefix, **opts):
286 def diff(self, diffopts, node2, match, prefix, **opts):
287 pass
287 pass
288
288
289 def outgoing(self, ui, dest, opts):
289 def outgoing(self, ui, dest, opts):
290 return 1
290 return 1
291
291
292 def incoming(self, ui, source, opts):
292 def incoming(self, ui, source, opts):
293 return 1
293 return 1
294
294
295 def files(self):
295 def files(self):
296 """return filename iterator"""
296 """return filename iterator"""
297 raise NotImplementedError
297 raise NotImplementedError
298
298
299 def filedata(self, name):
299 def filedata(self, name):
300 """return file data"""
300 """return file data"""
301 raise NotImplementedError
301 raise NotImplementedError
302
302
303 def fileflags(self, name):
303 def fileflags(self, name):
304 """return file flags"""
304 """return file flags"""
305 return ''
305 return ''
306
306
307 def archive(self, archiver, prefix):
307 def archive(self, ui, archiver, prefix):
308 for name in self.files():
308 files = self.files()
309 total = len(files)
310 relpath = subrelpath(self)
311 ui.progress(_('archiving (%s)') % relpath, 0,
312 unit=_('files'), total=total)
313 for i, name in enumerate(files):
309 flags = self.fileflags(name)
314 flags = self.fileflags(name)
310 mode = 'x' in flags and 0755 or 0644
315 mode = 'x' in flags and 0755 or 0644
311 symlink = 'l' in flags
316 symlink = 'l' in flags
312 archiver.addfile(os.path.join(prefix, self._path, name),
317 archiver.addfile(os.path.join(prefix, self._path, name),
313 mode, symlink, self.filedata(name))
318 mode, symlink, self.filedata(name))
319 ui.progress(_('archiving (%s)') % relpath, i + 1,
320 unit=_('files'), total=total)
321 ui.progress(_('archiving (%s)') % relpath, None)
314
322
315
323
316 class hgsubrepo(abstractsubrepo):
324 class hgsubrepo(abstractsubrepo):
317 def __init__(self, ctx, path, state):
325 def __init__(self, ctx, path, state):
318 self._path = path
326 self._path = path
319 self._state = state
327 self._state = state
320 r = ctx._repo
328 r = ctx._repo
321 root = r.wjoin(path)
329 root = r.wjoin(path)
322 create = False
330 create = False
323 if not os.path.exists(os.path.join(root, '.hg')):
331 if not os.path.exists(os.path.join(root, '.hg')):
324 create = True
332 create = True
325 util.makedirs(root)
333 util.makedirs(root)
326 self._repo = hg.repository(r.ui, root, create=create)
334 self._repo = hg.repository(r.ui, root, create=create)
327 self._repo._subparent = r
335 self._repo._subparent = r
328 self._repo._subsource = state[0]
336 self._repo._subsource = state[0]
329
337
330 if create:
338 if create:
331 fp = self._repo.opener("hgrc", "w", text=True)
339 fp = self._repo.opener("hgrc", "w", text=True)
332 fp.write('[paths]\n')
340 fp.write('[paths]\n')
333
341
334 def addpathconfig(key, value):
342 def addpathconfig(key, value):
335 if value:
343 if value:
336 fp.write('%s = %s\n' % (key, value))
344 fp.write('%s = %s\n' % (key, value))
337 self._repo.ui.setconfig('paths', key, value)
345 self._repo.ui.setconfig('paths', key, value)
338
346
339 defpath = _abssource(self._repo, abort=False)
347 defpath = _abssource(self._repo, abort=False)
340 defpushpath = _abssource(self._repo, True, abort=False)
348 defpushpath = _abssource(self._repo, True, abort=False)
341 addpathconfig('default', defpath)
349 addpathconfig('default', defpath)
342 if defpath != defpushpath:
350 if defpath != defpushpath:
343 addpathconfig('default-push', defpushpath)
351 addpathconfig('default-push', defpushpath)
344 fp.close()
352 fp.close()
345
353
346 def add(self, ui, match, dryrun, prefix):
354 def add(self, ui, match, dryrun, prefix):
347 return cmdutil.add(ui, self._repo, match, dryrun, True,
355 return cmdutil.add(ui, self._repo, match, dryrun, True,
348 os.path.join(prefix, self._path))
356 os.path.join(prefix, self._path))
349
357
350 def status(self, rev2, **opts):
358 def status(self, rev2, **opts):
351 try:
359 try:
352 rev1 = self._state[1]
360 rev1 = self._state[1]
353 ctx1 = self._repo[rev1]
361 ctx1 = self._repo[rev1]
354 ctx2 = self._repo[rev2]
362 ctx2 = self._repo[rev2]
355 return self._repo.status(ctx1, ctx2, **opts)
363 return self._repo.status(ctx1, ctx2, **opts)
356 except error.RepoLookupError, inst:
364 except error.RepoLookupError, inst:
357 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
365 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
358 % (inst, subrelpath(self)))
366 % (inst, subrelpath(self)))
359 return [], [], [], [], [], [], []
367 return [], [], [], [], [], [], []
360
368
361 def diff(self, diffopts, node2, match, prefix, **opts):
369 def diff(self, diffopts, node2, match, prefix, **opts):
362 try:
370 try:
363 node1 = node.bin(self._state[1])
371 node1 = node.bin(self._state[1])
364 # We currently expect node2 to come from substate and be
372 # We currently expect node2 to come from substate and be
365 # in hex format
373 # in hex format
366 if node2 is not None:
374 if node2 is not None:
367 node2 = node.bin(node2)
375 node2 = node.bin(node2)
368 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
376 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
369 node1, node2, match,
377 node1, node2, match,
370 prefix=os.path.join(prefix, self._path),
378 prefix=os.path.join(prefix, self._path),
371 listsubrepos=True, **opts)
379 listsubrepos=True, **opts)
372 except error.RepoLookupError, inst:
380 except error.RepoLookupError, inst:
373 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
381 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
374 % (inst, subrelpath(self)))
382 % (inst, subrelpath(self)))
375
383
376 def archive(self, archiver, prefix):
384 def archive(self, ui, archiver, prefix):
377 abstractsubrepo.archive(self, archiver, prefix)
385 abstractsubrepo.archive(self, ui, archiver, prefix)
378
386
379 rev = self._state[1]
387 rev = self._state[1]
380 ctx = self._repo[rev]
388 ctx = self._repo[rev]
381 for subpath in ctx.substate:
389 for subpath in ctx.substate:
382 s = subrepo(ctx, subpath)
390 s = subrepo(ctx, subpath)
383 s.archive(archiver, os.path.join(prefix, self._path))
391 s.archive(ui, archiver, os.path.join(prefix, self._path))
384
392
385 def dirty(self):
393 def dirty(self):
386 r = self._state[1]
394 r = self._state[1]
387 if r == '':
395 if r == '':
388 return True
396 return True
389 w = self._repo[None]
397 w = self._repo[None]
390 if w.p1() != self._repo[r]: # version checked out change
398 if w.p1() != self._repo[r]: # version checked out change
391 return True
399 return True
392 return w.dirty() # working directory changed
400 return w.dirty() # working directory changed
393
401
394 def checknested(self, path):
402 def checknested(self, path):
395 return self._repo._checknested(self._repo.wjoin(path))
403 return self._repo._checknested(self._repo.wjoin(path))
396
404
397 def commit(self, text, user, date):
405 def commit(self, text, user, date):
398 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
406 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
399 n = self._repo.commit(text, user, date)
407 n = self._repo.commit(text, user, date)
400 if not n:
408 if not n:
401 return self._repo['.'].hex() # different version checked out
409 return self._repo['.'].hex() # different version checked out
402 return node.hex(n)
410 return node.hex(n)
403
411
404 def remove(self):
412 def remove(self):
405 # we can't fully delete the repository as it may contain
413 # we can't fully delete the repository as it may contain
406 # local-only history
414 # local-only history
407 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
415 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
408 hg.clean(self._repo, node.nullid, False)
416 hg.clean(self._repo, node.nullid, False)
409
417
410 def _get(self, state):
418 def _get(self, state):
411 source, revision, kind = state
419 source, revision, kind = state
412 try:
420 try:
413 self._repo.lookup(revision)
421 self._repo.lookup(revision)
414 except error.RepoError:
422 except error.RepoError:
415 self._repo._subsource = source
423 self._repo._subsource = source
416 srcurl = _abssource(self._repo)
424 srcurl = _abssource(self._repo)
417 self._repo.ui.status(_('pulling subrepo %s from %s\n')
425 self._repo.ui.status(_('pulling subrepo %s from %s\n')
418 % (subrelpath(self), srcurl))
426 % (subrelpath(self), srcurl))
419 other = hg.repository(self._repo.ui, srcurl)
427 other = hg.repository(self._repo.ui, srcurl)
420 self._repo.pull(other)
428 self._repo.pull(other)
421
429
422 def get(self, state):
430 def get(self, state):
423 self._get(state)
431 self._get(state)
424 source, revision, kind = state
432 source, revision, kind = state
425 self._repo.ui.debug("getting subrepo %s\n" % self._path)
433 self._repo.ui.debug("getting subrepo %s\n" % self._path)
426 hg.clean(self._repo, revision, False)
434 hg.clean(self._repo, revision, False)
427
435
428 def merge(self, state):
436 def merge(self, state):
429 self._get(state)
437 self._get(state)
430 cur = self._repo['.']
438 cur = self._repo['.']
431 dst = self._repo[state[1]]
439 dst = self._repo[state[1]]
432 anc = dst.ancestor(cur)
440 anc = dst.ancestor(cur)
433 if anc == cur:
441 if anc == cur:
434 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
442 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
435 hg.update(self._repo, state[1])
443 hg.update(self._repo, state[1])
436 elif anc == dst:
444 elif anc == dst:
437 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
445 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
438 else:
446 else:
439 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
447 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
440 hg.merge(self._repo, state[1], remind=False)
448 hg.merge(self._repo, state[1], remind=False)
441
449
442 def push(self, force):
450 def push(self, force):
443 # push subrepos depth-first for coherent ordering
451 # push subrepos depth-first for coherent ordering
444 c = self._repo['']
452 c = self._repo['']
445 subs = c.substate # only repos that are committed
453 subs = c.substate # only repos that are committed
446 for s in sorted(subs):
454 for s in sorted(subs):
447 if not c.sub(s).push(force):
455 if not c.sub(s).push(force):
448 return False
456 return False
449
457
450 dsturl = _abssource(self._repo, True)
458 dsturl = _abssource(self._repo, True)
451 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
459 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
452 (subrelpath(self), dsturl))
460 (subrelpath(self), dsturl))
453 other = hg.repository(self._repo.ui, dsturl)
461 other = hg.repository(self._repo.ui, dsturl)
454 return self._repo.push(other, force)
462 return self._repo.push(other, force)
455
463
456 def outgoing(self, ui, dest, opts):
464 def outgoing(self, ui, dest, opts):
457 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
465 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
458
466
459 def incoming(self, ui, source, opts):
467 def incoming(self, ui, source, opts):
460 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
468 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
461
469
462 def files(self):
470 def files(self):
463 rev = self._state[1]
471 rev = self._state[1]
464 ctx = self._repo[rev]
472 ctx = self._repo[rev]
465 return ctx.manifest()
473 return ctx.manifest()
466
474
467 def filedata(self, name):
475 def filedata(self, name):
468 rev = self._state[1]
476 rev = self._state[1]
469 return self._repo[rev][name].data()
477 return self._repo[rev][name].data()
470
478
471 def fileflags(self, name):
479 def fileflags(self, name):
472 rev = self._state[1]
480 rev = self._state[1]
473 ctx = self._repo[rev]
481 ctx = self._repo[rev]
474 return ctx.flags(name)
482 return ctx.flags(name)
475
483
476
484
477 class svnsubrepo(abstractsubrepo):
485 class svnsubrepo(abstractsubrepo):
478 def __init__(self, ctx, path, state):
486 def __init__(self, ctx, path, state):
479 self._path = path
487 self._path = path
480 self._state = state
488 self._state = state
481 self._ctx = ctx
489 self._ctx = ctx
482 self._ui = ctx._repo.ui
490 self._ui = ctx._repo.ui
483
491
484 def _svncommand(self, commands, filename=''):
492 def _svncommand(self, commands, filename=''):
485 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
493 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
486 cmd = ['svn'] + commands + [path]
494 cmd = ['svn'] + commands + [path]
487 env = dict(os.environ)
495 env = dict(os.environ)
488 # Avoid localized output, preserve current locale for everything else.
496 # Avoid localized output, preserve current locale for everything else.
489 env['LC_MESSAGES'] = 'C'
497 env['LC_MESSAGES'] = 'C'
490 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
498 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
491 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
499 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
492 universal_newlines=True, env=env)
500 universal_newlines=True, env=env)
493 stdout, stderr = p.communicate()
501 stdout, stderr = p.communicate()
494 stderr = stderr.strip()
502 stderr = stderr.strip()
495 if stderr:
503 if stderr:
496 raise util.Abort(stderr)
504 raise util.Abort(stderr)
497 return stdout
505 return stdout
498
506
499 def _wcrev(self):
507 def _wcrev(self):
500 output = self._svncommand(['info', '--xml'])
508 output = self._svncommand(['info', '--xml'])
501 doc = xml.dom.minidom.parseString(output)
509 doc = xml.dom.minidom.parseString(output)
502 entries = doc.getElementsByTagName('entry')
510 entries = doc.getElementsByTagName('entry')
503 if not entries:
511 if not entries:
504 return '0'
512 return '0'
505 return str(entries[0].getAttribute('revision')) or '0'
513 return str(entries[0].getAttribute('revision')) or '0'
506
514
507 def _wcchanged(self):
515 def _wcchanged(self):
508 """Return (changes, extchanges) where changes is True
516 """Return (changes, extchanges) where changes is True
509 if the working directory was changed, and extchanges is
517 if the working directory was changed, and extchanges is
510 True if any of these changes concern an external entry.
518 True if any of these changes concern an external entry.
511 """
519 """
512 output = self._svncommand(['status', '--xml'])
520 output = self._svncommand(['status', '--xml'])
513 externals, changes = [], []
521 externals, changes = [], []
514 doc = xml.dom.minidom.parseString(output)
522 doc = xml.dom.minidom.parseString(output)
515 for e in doc.getElementsByTagName('entry'):
523 for e in doc.getElementsByTagName('entry'):
516 s = e.getElementsByTagName('wc-status')
524 s = e.getElementsByTagName('wc-status')
517 if not s:
525 if not s:
518 continue
526 continue
519 item = s[0].getAttribute('item')
527 item = s[0].getAttribute('item')
520 props = s[0].getAttribute('props')
528 props = s[0].getAttribute('props')
521 path = e.getAttribute('path')
529 path = e.getAttribute('path')
522 if item == 'external':
530 if item == 'external':
523 externals.append(path)
531 externals.append(path)
524 if (item not in ('', 'normal', 'unversioned', 'external')
532 if (item not in ('', 'normal', 'unversioned', 'external')
525 or props not in ('', 'none')):
533 or props not in ('', 'none')):
526 changes.append(path)
534 changes.append(path)
527 for path in changes:
535 for path in changes:
528 for ext in externals:
536 for ext in externals:
529 if path == ext or path.startswith(ext + os.sep):
537 if path == ext or path.startswith(ext + os.sep):
530 return True, True
538 return True, True
531 return bool(changes), False
539 return bool(changes), False
532
540
533 def dirty(self):
541 def dirty(self):
534 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
542 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
535 return False
543 return False
536 return True
544 return True
537
545
538 def commit(self, text, user, date):
546 def commit(self, text, user, date):
539 # user and date are out of our hands since svn is centralized
547 # user and date are out of our hands since svn is centralized
540 changed, extchanged = self._wcchanged()
548 changed, extchanged = self._wcchanged()
541 if not changed:
549 if not changed:
542 return self._wcrev()
550 return self._wcrev()
543 if extchanged:
551 if extchanged:
544 # Do not try to commit externals
552 # Do not try to commit externals
545 raise util.Abort(_('cannot commit svn externals'))
553 raise util.Abort(_('cannot commit svn externals'))
546 commitinfo = self._svncommand(['commit', '-m', text])
554 commitinfo = self._svncommand(['commit', '-m', text])
547 self._ui.status(commitinfo)
555 self._ui.status(commitinfo)
548 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
556 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
549 if not newrev:
557 if not newrev:
550 raise util.Abort(commitinfo.splitlines()[-1])
558 raise util.Abort(commitinfo.splitlines()[-1])
551 newrev = newrev.groups()[0]
559 newrev = newrev.groups()[0]
552 self._ui.status(self._svncommand(['update', '-r', newrev]))
560 self._ui.status(self._svncommand(['update', '-r', newrev]))
553 return newrev
561 return newrev
554
562
555 def remove(self):
563 def remove(self):
556 if self.dirty():
564 if self.dirty():
557 self._ui.warn(_('not removing repo %s because '
565 self._ui.warn(_('not removing repo %s because '
558 'it has changes.\n' % self._path))
566 'it has changes.\n' % self._path))
559 return
567 return
560 self._ui.note(_('removing subrepo %s\n') % self._path)
568 self._ui.note(_('removing subrepo %s\n') % self._path)
561
569
562 def onerror(function, path, excinfo):
570 def onerror(function, path, excinfo):
563 if function is not os.remove:
571 if function is not os.remove:
564 raise
572 raise
565 # read-only files cannot be unlinked under Windows
573 # read-only files cannot be unlinked under Windows
566 s = os.stat(path)
574 s = os.stat(path)
567 if (s.st_mode & stat.S_IWRITE) != 0:
575 if (s.st_mode & stat.S_IWRITE) != 0:
568 raise
576 raise
569 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
577 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
570 os.remove(path)
578 os.remove(path)
571
579
572 path = self._ctx._repo.wjoin(self._path)
580 path = self._ctx._repo.wjoin(self._path)
573 shutil.rmtree(path, onerror=onerror)
581 shutil.rmtree(path, onerror=onerror)
574 try:
582 try:
575 os.removedirs(os.path.dirname(path))
583 os.removedirs(os.path.dirname(path))
576 except OSError:
584 except OSError:
577 pass
585 pass
578
586
579 def get(self, state):
587 def get(self, state):
580 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
588 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
581 if not re.search('Checked out revision [0-9]+.', status):
589 if not re.search('Checked out revision [0-9]+.', status):
582 raise util.Abort(status.splitlines()[-1])
590 raise util.Abort(status.splitlines()[-1])
583 self._ui.status(status)
591 self._ui.status(status)
584
592
585 def merge(self, state):
593 def merge(self, state):
586 old = int(self._state[1])
594 old = int(self._state[1])
587 new = int(state[1])
595 new = int(state[1])
588 if new > old:
596 if new > old:
589 self.get(state)
597 self.get(state)
590
598
591 def push(self, force):
599 def push(self, force):
592 # push is a no-op for SVN
600 # push is a no-op for SVN
593 return True
601 return True
594
602
595 def files(self):
603 def files(self):
596 output = self._svncommand(['list'])
604 output = self._svncommand(['list'])
597 # This works because svn forbids \n in filenames.
605 # This works because svn forbids \n in filenames.
598 return output.splitlines()
606 return output.splitlines()
599
607
600 def filedata(self, name):
608 def filedata(self, name):
601 return self._svncommand(['cat'], name)
609 return self._svncommand(['cat'], name)
602
610
603
611
604 class gitsubrepo(abstractsubrepo):
612 class gitsubrepo(abstractsubrepo):
605 def __init__(self, ctx, path, state):
613 def __init__(self, ctx, path, state):
606 # TODO add git version check.
614 # TODO add git version check.
607 self._state = state
615 self._state = state
608 self._ctx = ctx
616 self._ctx = ctx
609 self._relpath = path
617 self._relpath = path
610 self._path = ctx._repo.wjoin(path)
618 self._path = ctx._repo.wjoin(path)
611 self._ui = ctx._repo.ui
619 self._ui = ctx._repo.ui
612
620
613 def _gitcommand(self, commands, env=None, stream=False):
621 def _gitcommand(self, commands, env=None, stream=False):
614 return self._gitdir(commands, env=env, stream=stream)[0]
622 return self._gitdir(commands, env=env, stream=stream)[0]
615
623
616 def _gitdir(self, commands, env=None, stream=False):
624 def _gitdir(self, commands, env=None, stream=False):
617 return self._gitnodir(commands, env=env, stream=stream, cwd=self._path)
625 return self._gitnodir(commands, env=env, stream=stream, cwd=self._path)
618
626
619 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
627 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
620 """Calls the git command
628 """Calls the git command
621
629
622 The methods tries to call the git command. versions previor to 1.6.0
630 The methods tries to call the git command. versions previor to 1.6.0
623 are not supported and very probably fail.
631 are not supported and very probably fail.
624 """
632 """
625 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
633 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
626 # unless ui.quiet is set, print git's stderr,
634 # unless ui.quiet is set, print git's stderr,
627 # which is mostly progress and useful info
635 # which is mostly progress and useful info
628 errpipe = None
636 errpipe = None
629 if self._ui.quiet:
637 if self._ui.quiet:
630 errpipe = open(os.devnull, 'w')
638 errpipe = open(os.devnull, 'w')
631 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
639 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
632 close_fds=util.closefds,
640 close_fds=util.closefds,
633 stdout=subprocess.PIPE, stderr=errpipe)
641 stdout=subprocess.PIPE, stderr=errpipe)
634 if stream:
642 if stream:
635 return p.stdout, None
643 return p.stdout, None
636
644
637 retdata = p.stdout.read().strip()
645 retdata = p.stdout.read().strip()
638 # wait for the child to exit to avoid race condition.
646 # wait for the child to exit to avoid race condition.
639 p.wait()
647 p.wait()
640
648
641 if p.returncode != 0 and p.returncode != 1:
649 if p.returncode != 0 and p.returncode != 1:
642 # there are certain error codes that are ok
650 # there are certain error codes that are ok
643 command = commands[0]
651 command = commands[0]
644 if command == 'cat-file':
652 if command == 'cat-file':
645 return retdata, p.returncode
653 return retdata, p.returncode
646 # for all others, abort
654 # for all others, abort
647 raise util.Abort('git %s error %d in %s' %
655 raise util.Abort('git %s error %d in %s' %
648 (command, p.returncode, self._relpath))
656 (command, p.returncode, self._relpath))
649
657
650 return retdata, p.returncode
658 return retdata, p.returncode
651
659
652 def _gitstate(self):
660 def _gitstate(self):
653 return self._gitcommand(['rev-parse', 'HEAD'])
661 return self._gitcommand(['rev-parse', 'HEAD'])
654
662
655 def _githavelocally(self, revision):
663 def _githavelocally(self, revision):
656 out, code = self._gitdir(['cat-file', '-e', revision])
664 out, code = self._gitdir(['cat-file', '-e', revision])
657 return code == 0
665 return code == 0
658
666
659 def _gitisancestor(self, r1, r2):
667 def _gitisancestor(self, r1, r2):
660 base = self._gitcommand(['merge-base', r1, r2])
668 base = self._gitcommand(['merge-base', r1, r2])
661 return base == r1
669 return base == r1
662
670
663 def _gitbranchmap(self):
671 def _gitbranchmap(self):
664 '''returns 3 things:
672 '''returns 3 things:
665 the current branch,
673 the current branch,
666 a map from git branch to revision
674 a map from git branch to revision
667 a map from revision to branches'''
675 a map from revision to branches'''
668 branch2rev = {}
676 branch2rev = {}
669 rev2branch = {}
677 rev2branch = {}
670 current = None
678 current = None
671 out = self._gitcommand(['branch', '-a', '--no-color',
679 out = self._gitcommand(['branch', '-a', '--no-color',
672 '--verbose', '--no-abbrev'])
680 '--verbose', '--no-abbrev'])
673 for line in out.split('\n'):
681 for line in out.split('\n'):
674 if line[2:].startswith('(no branch)'):
682 if line[2:].startswith('(no branch)'):
675 continue
683 continue
676 branch, revision = line[2:].split()[:2]
684 branch, revision = line[2:].split()[:2]
677 if revision == '->' or branch.endswith('/HEAD'):
685 if revision == '->' or branch.endswith('/HEAD'):
678 continue # ignore remote/HEAD redirects
686 continue # ignore remote/HEAD redirects
679 if '/' in branch and not branch.startswith('remotes/'):
687 if '/' in branch and not branch.startswith('remotes/'):
680 # old git compatability
688 # old git compatability
681 branch = 'remotes/' + branch
689 branch = 'remotes/' + branch
682 if line[0] == '*':
690 if line[0] == '*':
683 current = branch
691 current = branch
684 branch2rev[branch] = revision
692 branch2rev[branch] = revision
685 rev2branch.setdefault(revision, []).append(branch)
693 rev2branch.setdefault(revision, []).append(branch)
686 return current, branch2rev, rev2branch
694 return current, branch2rev, rev2branch
687
695
688 def _gittracking(self, branches):
696 def _gittracking(self, branches):
689 'return map of remote branch to local tracking branch'
697 'return map of remote branch to local tracking branch'
690 # assumes no more than one local tracking branch for each remote
698 # assumes no more than one local tracking branch for each remote
691 tracking = {}
699 tracking = {}
692 for b in branches:
700 for b in branches:
693 if b.startswith('remotes/'):
701 if b.startswith('remotes/'):
694 continue
702 continue
695 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
703 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
696 if remote:
704 if remote:
697 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
705 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
698 tracking['remotes/%s/%s' % (remote, ref.split('/')[-1])] = b
706 tracking['remotes/%s/%s' % (remote, ref.split('/')[-1])] = b
699 return tracking
707 return tracking
700
708
701 def _fetch(self, source, revision):
709 def _fetch(self, source, revision):
702 if not os.path.exists('%s/.git' % self._path):
710 if not os.path.exists('%s/.git' % self._path):
703 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
711 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
704 self._gitnodir(['clone', source, self._path])
712 self._gitnodir(['clone', source, self._path])
705 if self._githavelocally(revision):
713 if self._githavelocally(revision):
706 return
714 return
707 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
715 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
708 # first try from origin
716 # first try from origin
709 self._gitcommand(['fetch'])
717 self._gitcommand(['fetch'])
710 if self._githavelocally(revision):
718 if self._githavelocally(revision):
711 return
719 return
712 # then try from known subrepo source
720 # then try from known subrepo source
713 self._gitcommand(['fetch', source])
721 self._gitcommand(['fetch', source])
714 if not self._githavelocally(revision):
722 if not self._githavelocally(revision):
715 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
723 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
716 (revision, self._path))
724 (revision, self._path))
717
725
718 def dirty(self):
726 def dirty(self):
719 if self._state[1] != self._gitstate(): # version checked out changed?
727 if self._state[1] != self._gitstate(): # version checked out changed?
720 return True
728 return True
721 # check for staged changes or modified files; ignore untracked files
729 # check for staged changes or modified files; ignore untracked files
722 status = self._gitcommand(['status'])
730 status = self._gitcommand(['status'])
723 return ('\n# Changed but not updated:' in status or
731 return ('\n# Changed but not updated:' in status or
724 '\n# Changes to be committed:' in status)
732 '\n# Changes to be committed:' in status)
725
733
726 def get(self, state):
734 def get(self, state):
727 source, revision, kind = state
735 source, revision, kind = state
728 self._fetch(source, revision)
736 self._fetch(source, revision)
729 # if the repo was set to be bare, unbare it
737 # if the repo was set to be bare, unbare it
730 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
738 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
731 self._gitcommand(['config', 'core.bare', 'false'])
739 self._gitcommand(['config', 'core.bare', 'false'])
732 if self._gitstate() == revision:
740 if self._gitstate() == revision:
733 self._gitcommand(['reset', '--hard', 'HEAD'])
741 self._gitcommand(['reset', '--hard', 'HEAD'])
734 return
742 return
735 elif self._gitstate() == revision:
743 elif self._gitstate() == revision:
736 return
744 return
737 current, branch2rev, rev2branch = self._gitbranchmap()
745 current, branch2rev, rev2branch = self._gitbranchmap()
738
746
739 def rawcheckout():
747 def rawcheckout():
740 # no branch to checkout, check it out with no branch
748 # no branch to checkout, check it out with no branch
741 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
749 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
742 self._relpath)
750 self._relpath)
743 self._ui.warn(_('check out a git branch if you intend '
751 self._ui.warn(_('check out a git branch if you intend '
744 'to make changes\n'))
752 'to make changes\n'))
745 self._gitcommand(['checkout', '-q', revision])
753 self._gitcommand(['checkout', '-q', revision])
746
754
747 if revision not in rev2branch:
755 if revision not in rev2branch:
748 rawcheckout()
756 rawcheckout()
749 return
757 return
750 branches = rev2branch[revision]
758 branches = rev2branch[revision]
751 firstlocalbranch = None
759 firstlocalbranch = None
752 for b in branches:
760 for b in branches:
753 if b == 'master':
761 if b == 'master':
754 # master trumps all other branches
762 # master trumps all other branches
755 self._gitcommand(['checkout', 'master'])
763 self._gitcommand(['checkout', 'master'])
756 return
764 return
757 if not firstlocalbranch and not b.startswith('remotes/'):
765 if not firstlocalbranch and not b.startswith('remotes/'):
758 firstlocalbranch = b
766 firstlocalbranch = b
759 if firstlocalbranch:
767 if firstlocalbranch:
760 self._gitcommand(['checkout', firstlocalbranch])
768 self._gitcommand(['checkout', firstlocalbranch])
761 return
769 return
762
770
763 tracking = self._gittracking(branch2rev.keys())
771 tracking = self._gittracking(branch2rev.keys())
764 # choose a remote branch already tracked if possible
772 # choose a remote branch already tracked if possible
765 remote = branches[0]
773 remote = branches[0]
766 if remote not in tracking:
774 if remote not in tracking:
767 for b in branches:
775 for b in branches:
768 if b in tracking:
776 if b in tracking:
769 remote = b
777 remote = b
770 break
778 break
771
779
772 if remote not in tracking:
780 if remote not in tracking:
773 # create a new local tracking branch
781 # create a new local tracking branch
774 local = remote.split('/')[-1]
782 local = remote.split('/')[-1]
775 self._gitcommand(['checkout', '-b', local, remote])
783 self._gitcommand(['checkout', '-b', local, remote])
776 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
784 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
777 # When updating to a tracked remote branch,
785 # When updating to a tracked remote branch,
778 # if the local tracking branch is downstream of it,
786 # if the local tracking branch is downstream of it,
779 # a normal `git pull` would have performed a "fast-forward merge"
787 # a normal `git pull` would have performed a "fast-forward merge"
780 # which is equivalent to updating the local branch to the remote.
788 # which is equivalent to updating the local branch to the remote.
781 # Since we are only looking at branching at update, we need to
789 # Since we are only looking at branching at update, we need to
782 # detect this situation and perform this action lazily.
790 # detect this situation and perform this action lazily.
783 if tracking[remote] != current:
791 if tracking[remote] != current:
784 self._gitcommand(['checkout', tracking[remote]])
792 self._gitcommand(['checkout', tracking[remote]])
785 self._gitcommand(['merge', '--ff', remote])
793 self._gitcommand(['merge', '--ff', remote])
786 else:
794 else:
787 # a real merge would be required, just checkout the revision
795 # a real merge would be required, just checkout the revision
788 rawcheckout()
796 rawcheckout()
789
797
790 def commit(self, text, user, date):
798 def commit(self, text, user, date):
791 cmd = ['commit', '-a', '-m', text]
799 cmd = ['commit', '-a', '-m', text]
792 env = os.environ.copy()
800 env = os.environ.copy()
793 if user:
801 if user:
794 cmd += ['--author', user]
802 cmd += ['--author', user]
795 if date:
803 if date:
796 # git's date parser silently ignores when seconds < 1e9
804 # git's date parser silently ignores when seconds < 1e9
797 # convert to ISO8601
805 # convert to ISO8601
798 env['GIT_AUTHOR_DATE'] = util.datestr(date,
806 env['GIT_AUTHOR_DATE'] = util.datestr(date,
799 '%Y-%m-%dT%H:%M:%S %1%2')
807 '%Y-%m-%dT%H:%M:%S %1%2')
800 self._gitcommand(cmd, env=env)
808 self._gitcommand(cmd, env=env)
801 # make sure commit works otherwise HEAD might not exist under certain
809 # make sure commit works otherwise HEAD might not exist under certain
802 # circumstances
810 # circumstances
803 return self._gitstate()
811 return self._gitstate()
804
812
805 def merge(self, state):
813 def merge(self, state):
806 source, revision, kind = state
814 source, revision, kind = state
807 self._fetch(source, revision)
815 self._fetch(source, revision)
808 base = self._gitcommand(['merge-base', revision, self._state[1]])
816 base = self._gitcommand(['merge-base', revision, self._state[1]])
809 if base == revision:
817 if base == revision:
810 self.get(state) # fast forward merge
818 self.get(state) # fast forward merge
811 elif base != self._state[1]:
819 elif base != self._state[1]:
812 self._gitcommand(['merge', '--no-commit', revision])
820 self._gitcommand(['merge', '--no-commit', revision])
813
821
814 def push(self, force):
822 def push(self, force):
815 # if a branch in origin contains the revision, nothing to do
823 # if a branch in origin contains the revision, nothing to do
816 current, branch2rev, rev2branch = self._gitbranchmap()
824 current, branch2rev, rev2branch = self._gitbranchmap()
817 if self._state[1] in rev2branch:
825 if self._state[1] in rev2branch:
818 for b in rev2branch[self._state[1]]:
826 for b in rev2branch[self._state[1]]:
819 if b.startswith('remotes/origin/'):
827 if b.startswith('remotes/origin/'):
820 return True
828 return True
821 for b, revision in branch2rev.iteritems():
829 for b, revision in branch2rev.iteritems():
822 if b.startswith('remotes/origin/'):
830 if b.startswith('remotes/origin/'):
823 if self._gitisancestor(self._state[1], revision):
831 if self._gitisancestor(self._state[1], revision):
824 return True
832 return True
825 # otherwise, try to push the currently checked out branch
833 # otherwise, try to push the currently checked out branch
826 cmd = ['push']
834 cmd = ['push']
827 if force:
835 if force:
828 cmd.append('--force')
836 cmd.append('--force')
829 if current:
837 if current:
830 # determine if the current branch is even useful
838 # determine if the current branch is even useful
831 if not self._gitisancestor(self._state[1], current):
839 if not self._gitisancestor(self._state[1], current):
832 self._ui.warn(_('unrelated git branch checked out '
840 self._ui.warn(_('unrelated git branch checked out '
833 'in subrepo %s\n') % self._relpath)
841 'in subrepo %s\n') % self._relpath)
834 return False
842 return False
835 self._ui.status(_('pushing branch %s of subrepo %s\n') %
843 self._ui.status(_('pushing branch %s of subrepo %s\n') %
836 (current, self._relpath))
844 (current, self._relpath))
837 self._gitcommand(cmd + ['origin', current])
845 self._gitcommand(cmd + ['origin', current])
838 return True
846 return True
839 else:
847 else:
840 self._ui.warn(_('no branch checked out in subrepo %s\n'
848 self._ui.warn(_('no branch checked out in subrepo %s\n'
841 'cannot push revision %s') %
849 'cannot push revision %s') %
842 (self._relpath, self._state[1]))
850 (self._relpath, self._state[1]))
843 return False
851 return False
844
852
845 def remove(self):
853 def remove(self):
846 if self.dirty():
854 if self.dirty():
847 self._ui.warn(_('not removing repo %s because '
855 self._ui.warn(_('not removing repo %s because '
848 'it has changes.\n') % self._path)
856 'it has changes.\n') % self._path)
849 return
857 return
850 # we can't fully delete the repository as it may contain
858 # we can't fully delete the repository as it may contain
851 # local-only history
859 # local-only history
852 self._ui.note(_('removing subrepo %s\n') % self._path)
860 self._ui.note(_('removing subrepo %s\n') % self._path)
853 self._gitcommand(['config', 'core.bare', 'true'])
861 self._gitcommand(['config', 'core.bare', 'true'])
854 for f in os.listdir(self._path):
862 for f in os.listdir(self._path):
855 if f == '.git':
863 if f == '.git':
856 continue
864 continue
857 path = os.path.join(self._path, f)
865 path = os.path.join(self._path, f)
858 if os.path.isdir(path) and not os.path.islink(path):
866 if os.path.isdir(path) and not os.path.islink(path):
859 shutil.rmtree(path)
867 shutil.rmtree(path)
860 else:
868 else:
861 os.remove(path)
869 os.remove(path)
862
870
863 def archive(self, archiver, prefix):
871 def archive(self, ui, archiver, prefix):
864 source, revision = self._state
872 source, revision = self._state
865 self._fetch(source, revision)
873 self._fetch(source, revision)
866
874
867 # Parse git's native archive command.
875 # Parse git's native archive command.
868 # This should be much faster than manually traversing the trees
876 # This should be much faster than manually traversing the trees
869 # and objects with many subprocess calls.
877 # and objects with many subprocess calls.
870 tarstream = self._gitcommand(['archive', revision], stream=True)
878 tarstream = self._gitcommand(['archive', revision], stream=True)
871 tar = tarfile.open(fileobj=tarstream, mode='r|')
879 tar = tarfile.open(fileobj=tarstream, mode='r|')
872 for info in tar:
880 relpath = subrelpath(self)
881 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
882 for i, info in enumerate(tar):
873 archiver.addfile(os.path.join(prefix, self._relpath, info.name),
883 archiver.addfile(os.path.join(prefix, self._relpath, info.name),
874 info.mode, info.issym(),
884 info.mode, info.issym(),
875 tar.extractfile(info).read())
885 tar.extractfile(info).read())
886 ui.progress(_('archiving (%s)') % relpath, i + 1,
887 unit=_('files'))
888 ui.progress(_('archiving (%s)') % relpath, None)
889
876
890
877 types = {
891 types = {
878 'hg': hgsubrepo,
892 'hg': hgsubrepo,
879 'svn': svnsubrepo,
893 'svn': svnsubrepo,
880 'git': gitsubrepo,
894 'git': gitsubrepo,
881 }
895 }
@@ -1,348 +1,414 b''
1 Create test repository:
1 Create test repository:
2
2
3 $ hg init repo
3 $ hg init repo
4 $ cd repo
4 $ cd repo
5 $ echo x1 > x.txt
5 $ echo x1 > x.txt
6
6
7 $ hg init foo
7 $ hg init foo
8 $ cd foo
8 $ cd foo
9 $ echo y1 > y.txt
9 $ echo y1 > y.txt
10
10
11 $ hg init bar
11 $ hg init bar
12 $ cd bar
12 $ cd bar
13 $ echo z1 > z.txt
13 $ echo z1 > z.txt
14
14
15 $ cd ..
15 $ cd ..
16 $ echo 'bar = bar' > .hgsub
16 $ echo 'bar = bar' > .hgsub
17
17
18 $ cd ..
18 $ cd ..
19 $ echo 'foo = foo' > .hgsub
19 $ echo 'foo = foo' > .hgsub
20
20
21 Add files --- .hgsub files must go first to trigger subrepos:
21 Add files --- .hgsub files must go first to trigger subrepos:
22
22
23 $ hg add -S .hgsub
23 $ hg add -S .hgsub
24 $ hg add -S foo/.hgsub
24 $ hg add -S foo/.hgsub
25 $ hg add -S foo/bar
25 $ hg add -S foo/bar
26 adding foo/bar/z.txt
26 adding foo/bar/z.txt
27 $ hg add -S
27 $ hg add -S
28 adding x.txt
28 adding x.txt
29 adding foo/y.txt
29 adding foo/y.txt
30
30
31 Test recursive status without committing anything:
31 Test recursive status without committing anything:
32
32
33 $ hg status -S
33 $ hg status -S
34 A .hgsub
34 A .hgsub
35 A foo/.hgsub
35 A foo/.hgsub
36 A foo/bar/z.txt
36 A foo/bar/z.txt
37 A foo/y.txt
37 A foo/y.txt
38 A x.txt
38 A x.txt
39
39
40 Test recursive diff without committing anything:
40 Test recursive diff without committing anything:
41
41
42 $ hg diff --nodates -S foo
42 $ hg diff --nodates -S foo
43 diff -r 000000000000 foo/.hgsub
43 diff -r 000000000000 foo/.hgsub
44 --- /dev/null
44 --- /dev/null
45 +++ b/foo/.hgsub
45 +++ b/foo/.hgsub
46 @@ -0,0 +1,1 @@
46 @@ -0,0 +1,1 @@
47 +bar = bar
47 +bar = bar
48 diff -r 000000000000 foo/y.txt
48 diff -r 000000000000 foo/y.txt
49 --- /dev/null
49 --- /dev/null
50 +++ b/foo/y.txt
50 +++ b/foo/y.txt
51 @@ -0,0 +1,1 @@
51 @@ -0,0 +1,1 @@
52 +y1
52 +y1
53 diff -r 000000000000 foo/bar/z.txt
53 diff -r 000000000000 foo/bar/z.txt
54 --- /dev/null
54 --- /dev/null
55 +++ b/foo/bar/z.txt
55 +++ b/foo/bar/z.txt
56 @@ -0,0 +1,1 @@
56 @@ -0,0 +1,1 @@
57 +z1
57 +z1
58
58
59 Commits:
59 Commits:
60
60
61 $ hg commit -m 0-0-0
61 $ hg commit -m 0-0-0
62 committing subrepository foo
62 committing subrepository foo
63 committing subrepository foo/bar
63 committing subrepository foo/bar
64
64
65 $ cd foo
65 $ cd foo
66 $ echo y2 >> y.txt
66 $ echo y2 >> y.txt
67 $ hg commit -m 0-1-0
67 $ hg commit -m 0-1-0
68
68
69 $ cd bar
69 $ cd bar
70 $ echo z2 >> z.txt
70 $ echo z2 >> z.txt
71 $ hg commit -m 0-1-1
71 $ hg commit -m 0-1-1
72
72
73 $ cd ..
73 $ cd ..
74 $ hg commit -m 0-2-1
74 $ hg commit -m 0-2-1
75 committing subrepository bar
75 committing subrepository bar
76
76
77 $ cd ..
77 $ cd ..
78 $ hg commit -m 1-2-1
78 $ hg commit -m 1-2-1
79 committing subrepository foo
79 committing subrepository foo
80
80
81 Change working directory:
81 Change working directory:
82
82
83 $ echo y3 >> foo/y.txt
83 $ echo y3 >> foo/y.txt
84 $ echo z3 >> foo/bar/z.txt
84 $ echo z3 >> foo/bar/z.txt
85 $ hg status -S
85 $ hg status -S
86 M foo/bar/z.txt
86 M foo/bar/z.txt
87 M foo/y.txt
87 M foo/y.txt
88 $ hg diff --nodates -S
88 $ hg diff --nodates -S
89 diff -r d254738c5f5e foo/y.txt
89 diff -r d254738c5f5e foo/y.txt
90 --- a/foo/y.txt
90 --- a/foo/y.txt
91 +++ b/foo/y.txt
91 +++ b/foo/y.txt
92 @@ -1,2 +1,3 @@
92 @@ -1,2 +1,3 @@
93 y1
93 y1
94 y2
94 y2
95 +y3
95 +y3
96 diff -r 9647f22de499 foo/bar/z.txt
96 diff -r 9647f22de499 foo/bar/z.txt
97 --- a/foo/bar/z.txt
97 --- a/foo/bar/z.txt
98 +++ b/foo/bar/z.txt
98 +++ b/foo/bar/z.txt
99 @@ -1,2 +1,3 @@
99 @@ -1,2 +1,3 @@
100 z1
100 z1
101 z2
101 z2
102 +z3
102 +z3
103
103
104 Status call crossing repository boundaries:
104 Status call crossing repository boundaries:
105
105
106 $ hg status -S foo/bar/z.txt
106 $ hg status -S foo/bar/z.txt
107 M foo/bar/z.txt
107 M foo/bar/z.txt
108 $ hg status -S -I 'foo/?.txt'
108 $ hg status -S -I 'foo/?.txt'
109 M foo/y.txt
109 M foo/y.txt
110 $ hg status -S -I '**/?.txt'
110 $ hg status -S -I '**/?.txt'
111 M foo/bar/z.txt
111 M foo/bar/z.txt
112 M foo/y.txt
112 M foo/y.txt
113 $ hg diff --nodates -S -I '**/?.txt'
113 $ hg diff --nodates -S -I '**/?.txt'
114 diff -r d254738c5f5e foo/y.txt
114 diff -r d254738c5f5e foo/y.txt
115 --- a/foo/y.txt
115 --- a/foo/y.txt
116 +++ b/foo/y.txt
116 +++ b/foo/y.txt
117 @@ -1,2 +1,3 @@
117 @@ -1,2 +1,3 @@
118 y1
118 y1
119 y2
119 y2
120 +y3
120 +y3
121 diff -r 9647f22de499 foo/bar/z.txt
121 diff -r 9647f22de499 foo/bar/z.txt
122 --- a/foo/bar/z.txt
122 --- a/foo/bar/z.txt
123 +++ b/foo/bar/z.txt
123 +++ b/foo/bar/z.txt
124 @@ -1,2 +1,3 @@
124 @@ -1,2 +1,3 @@
125 z1
125 z1
126 z2
126 z2
127 +z3
127 +z3
128
128
129 Status from within a subdirectory:
129 Status from within a subdirectory:
130
130
131 $ mkdir dir
131 $ mkdir dir
132 $ cd dir
132 $ cd dir
133 $ echo a1 > a.txt
133 $ echo a1 > a.txt
134 $ hg status -S
134 $ hg status -S
135 M foo/bar/z.txt
135 M foo/bar/z.txt
136 M foo/y.txt
136 M foo/y.txt
137 ? dir/a.txt
137 ? dir/a.txt
138 $ hg diff --nodates -S
138 $ hg diff --nodates -S
139 diff -r d254738c5f5e foo/y.txt
139 diff -r d254738c5f5e foo/y.txt
140 --- a/foo/y.txt
140 --- a/foo/y.txt
141 +++ b/foo/y.txt
141 +++ b/foo/y.txt
142 @@ -1,2 +1,3 @@
142 @@ -1,2 +1,3 @@
143 y1
143 y1
144 y2
144 y2
145 +y3
145 +y3
146 diff -r 9647f22de499 foo/bar/z.txt
146 diff -r 9647f22de499 foo/bar/z.txt
147 --- a/foo/bar/z.txt
147 --- a/foo/bar/z.txt
148 +++ b/foo/bar/z.txt
148 +++ b/foo/bar/z.txt
149 @@ -1,2 +1,3 @@
149 @@ -1,2 +1,3 @@
150 z1
150 z1
151 z2
151 z2
152 +z3
152 +z3
153
153
154 Status with relative path:
154 Status with relative path:
155
155
156 $ hg status -S ..
156 $ hg status -S ..
157 M ../foo/bar/z.txt
157 M ../foo/bar/z.txt
158 M ../foo/y.txt
158 M ../foo/y.txt
159 ? a.txt
159 ? a.txt
160 $ hg diff --nodates -S ..
160 $ hg diff --nodates -S ..
161 diff -r d254738c5f5e foo/y.txt
161 diff -r d254738c5f5e foo/y.txt
162 --- a/foo/y.txt
162 --- a/foo/y.txt
163 +++ b/foo/y.txt
163 +++ b/foo/y.txt
164 @@ -1,2 +1,3 @@
164 @@ -1,2 +1,3 @@
165 y1
165 y1
166 y2
166 y2
167 +y3
167 +y3
168 diff -r 9647f22de499 foo/bar/z.txt
168 diff -r 9647f22de499 foo/bar/z.txt
169 --- a/foo/bar/z.txt
169 --- a/foo/bar/z.txt
170 +++ b/foo/bar/z.txt
170 +++ b/foo/bar/z.txt
171 @@ -1,2 +1,3 @@
171 @@ -1,2 +1,3 @@
172 z1
172 z1
173 z2
173 z2
174 +z3
174 +z3
175 $ cd ..
175 $ cd ..
176
176
177 Cleanup and final commit:
177 Cleanup and final commit:
178
178
179 $ rm -r dir
179 $ rm -r dir
180 $ hg commit -m 2-3-2
180 $ hg commit -m 2-3-2
181 committing subrepository foo
181 committing subrepository foo
182 committing subrepository foo/bar
182 committing subrepository foo/bar
183
183
184 Log with the relationships between repo and its subrepo:
184 Log with the relationships between repo and its subrepo:
185
185
186 $ hg log --template '{rev}:{node|short} {desc}\n'
186 $ hg log --template '{rev}:{node|short} {desc}\n'
187 2:1326fa26d0c0 2-3-2
187 2:1326fa26d0c0 2-3-2
188 1:4b3c9ff4f66b 1-2-1
188 1:4b3c9ff4f66b 1-2-1
189 0:23376cbba0d8 0-0-0
189 0:23376cbba0d8 0-0-0
190
190
191 $ hg -R foo log --template '{rev}:{node|short} {desc}\n'
191 $ hg -R foo log --template '{rev}:{node|short} {desc}\n'
192 3:65903cebad86 2-3-2
192 3:65903cebad86 2-3-2
193 2:d254738c5f5e 0-2-1
193 2:d254738c5f5e 0-2-1
194 1:8629ce7dcc39 0-1-0
194 1:8629ce7dcc39 0-1-0
195 0:af048e97ade2 0-0-0
195 0:af048e97ade2 0-0-0
196
196
197 $ hg -R foo/bar log --template '{rev}:{node|short} {desc}\n'
197 $ hg -R foo/bar log --template '{rev}:{node|short} {desc}\n'
198 2:31ecbdafd357 2-3-2
198 2:31ecbdafd357 2-3-2
199 1:9647f22de499 0-1-1
199 1:9647f22de499 0-1-1
200 0:4904098473f9 0-0-0
200 0:4904098473f9 0-0-0
201
201
202 Status between revisions:
202 Status between revisions:
203
203
204 $ hg status -S
204 $ hg status -S
205 $ hg status -S --rev 0:1
205 $ hg status -S --rev 0:1
206 M .hgsubstate
206 M .hgsubstate
207 M foo/.hgsubstate
207 M foo/.hgsubstate
208 M foo/bar/z.txt
208 M foo/bar/z.txt
209 M foo/y.txt
209 M foo/y.txt
210 $ hg diff --nodates -S -I '**/?.txt' --rev 0:1
210 $ hg diff --nodates -S -I '**/?.txt' --rev 0:1
211 diff -r af048e97ade2 -r d254738c5f5e foo/y.txt
211 diff -r af048e97ade2 -r d254738c5f5e foo/y.txt
212 --- a/foo/y.txt
212 --- a/foo/y.txt
213 +++ b/foo/y.txt
213 +++ b/foo/y.txt
214 @@ -1,1 +1,2 @@
214 @@ -1,1 +1,2 @@
215 y1
215 y1
216 +y2
216 +y2
217 diff -r 4904098473f9 -r 9647f22de499 foo/bar/z.txt
217 diff -r 4904098473f9 -r 9647f22de499 foo/bar/z.txt
218 --- a/foo/bar/z.txt
218 --- a/foo/bar/z.txt
219 +++ b/foo/bar/z.txt
219 +++ b/foo/bar/z.txt
220 @@ -1,1 +1,2 @@
220 @@ -1,1 +1,2 @@
221 z1
221 z1
222 +z2
222 +z2
223
223
224 Test archiving to a directory tree:
224 Enable progress extension for archive tests:
225
226 $ cp $HGRCPATH $HGRCPATH.no-progress
227 $ cat >> $HGRCPATH <<EOF
228 > [extensions]
229 > progress =
230 > [progress]
231 > assume-tty = 1
232 > delay = 0
233 > refresh = 0
234 > width = 60
235 > EOF
236
237 Test archiving to a directory tree (the doubled lines in the output
238 only show up in the test output, not in real usage):
239
240 $ hg archive --subrepos ../archive 2>&1 | $TESTDIR/filtercr.py
225
241
226 $ hg archive --subrepos ../archive
242 archiving [ ] 0/3
243 archiving [ ] 0/3
244 archiving [=============> ] 1/3
245 archiving [=============> ] 1/3
246 archiving [===========================> ] 2/3
247 archiving [===========================> ] 2/3
248 archiving [==========================================>] 3/3
249 archiving [==========================================>] 3/3
250
251 archiving (foo) [ ] 0/3
252 archiving (foo) [ ] 0/3
253 archiving (foo) [===========> ] 1/3
254 archiving (foo) [===========> ] 1/3
255 archiving (foo) [=======================> ] 2/3
256 archiving (foo) [=======================> ] 2/3
257 archiving (foo) [====================================>] 3/3
258 archiving (foo) [====================================>] 3/3
259
260 archiving (foo/bar) [ ] 0/1
261 archiving (foo/bar) [ ] 0/1
262 archiving (foo/bar) [================================>] 1/1
263 archiving (foo/bar) [================================>] 1/1
264 \r (esc)
227 $ find ../archive | sort
265 $ find ../archive | sort
228 ../archive
266 ../archive
229 ../archive/.hg_archival.txt
267 ../archive/.hg_archival.txt
230 ../archive/.hgsub
268 ../archive/.hgsub
231 ../archive/.hgsubstate
269 ../archive/.hgsubstate
232 ../archive/foo
270 ../archive/foo
233 ../archive/foo/.hgsub
271 ../archive/foo/.hgsub
234 ../archive/foo/.hgsubstate
272 ../archive/foo/.hgsubstate
235 ../archive/foo/bar
273 ../archive/foo/bar
236 ../archive/foo/bar/z.txt
274 ../archive/foo/bar/z.txt
237 ../archive/foo/y.txt
275 ../archive/foo/y.txt
238 ../archive/x.txt
276 ../archive/x.txt
239
277
240 Test archiving to zip file (unzip output is unstable):
278 Test archiving to zip file (unzip output is unstable):
241
279
242 $ hg archive --subrepos ../archive.zip
280 $ hg archive --subrepos ../archive.zip 2>&1 | $TESTDIR/filtercr.py
281
282 archiving [ ] 0/3
283 archiving [ ] 0/3
284 archiving [=============> ] 1/3
285 archiving [=============> ] 1/3
286 archiving [===========================> ] 2/3
287 archiving [===========================> ] 2/3
288 archiving [==========================================>] 3/3
289 archiving [==========================================>] 3/3
290
291 archiving (foo) [ ] 0/3
292 archiving (foo) [ ] 0/3
293 archiving (foo) [===========> ] 1/3
294 archiving (foo) [===========> ] 1/3
295 archiving (foo) [=======================> ] 2/3
296 archiving (foo) [=======================> ] 2/3
297 archiving (foo) [====================================>] 3/3
298 archiving (foo) [====================================>] 3/3
299
300 archiving (foo/bar) [ ] 0/1
301 archiving (foo/bar) [ ] 0/1
302 archiving (foo/bar) [================================>] 1/1
303 archiving (foo/bar) [================================>] 1/1
304 \r (esc)
305
306 Disable progress extension and cleanup:
307
308 $ mv $HGRCPATH.no-progress $HGRCPATH
243
309
244 Clone and test outgoing:
310 Clone and test outgoing:
245
311
246 $ cd ..
312 $ cd ..
247 $ hg clone repo repo2
313 $ hg clone repo repo2
248 updating to branch default
314 updating to branch default
249 pulling subrepo foo from $TESTTMP/repo/foo
315 pulling subrepo foo from $TESTTMP/repo/foo
250 requesting all changes
316 requesting all changes
251 adding changesets
317 adding changesets
252 adding manifests
318 adding manifests
253 adding file changes
319 adding file changes
254 added 4 changesets with 7 changes to 3 files
320 added 4 changesets with 7 changes to 3 files
255 pulling subrepo foo/bar from $TESTTMP/repo/foo/bar
321 pulling subrepo foo/bar from $TESTTMP/repo/foo/bar
256 requesting all changes
322 requesting all changes
257 adding changesets
323 adding changesets
258 adding manifests
324 adding manifests
259 adding file changes
325 adding file changes
260 added 3 changesets with 3 changes to 1 files
326 added 3 changesets with 3 changes to 1 files
261 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
327 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
262 $ cd repo2
328 $ cd repo2
263 $ hg outgoing -S
329 $ hg outgoing -S
264 comparing with $TESTTMP/repo
330 comparing with $TESTTMP/repo
265 searching for changes
331 searching for changes
266 no changes found
332 no changes found
267 comparing with $TESTTMP/repo/foo
333 comparing with $TESTTMP/repo/foo
268 searching for changes
334 searching for changes
269 no changes found
335 no changes found
270 comparing with $TESTTMP/repo/foo/bar
336 comparing with $TESTTMP/repo/foo/bar
271 searching for changes
337 searching for changes
272 no changes found
338 no changes found
273 [1]
339 [1]
274
340
275 Make nested change:
341 Make nested change:
276
342
277 $ echo y4 >> foo/y.txt
343 $ echo y4 >> foo/y.txt
278 $ hg diff --nodates -S
344 $ hg diff --nodates -S
279 diff -r 65903cebad86 foo/y.txt
345 diff -r 65903cebad86 foo/y.txt
280 --- a/foo/y.txt
346 --- a/foo/y.txt
281 +++ b/foo/y.txt
347 +++ b/foo/y.txt
282 @@ -1,3 +1,4 @@
348 @@ -1,3 +1,4 @@
283 y1
349 y1
284 y2
350 y2
285 y3
351 y3
286 +y4
352 +y4
287 $ hg commit -m 3-4-2
353 $ hg commit -m 3-4-2
288 committing subrepository foo
354 committing subrepository foo
289 $ hg outgoing -S
355 $ hg outgoing -S
290 comparing with $TESTTMP/repo
356 comparing with $TESTTMP/repo
291 searching for changes
357 searching for changes
292 changeset: 3:2655b8ecc4ee
358 changeset: 3:2655b8ecc4ee
293 tag: tip
359 tag: tip
294 user: test
360 user: test
295 date: Thu Jan 01 00:00:00 1970 +0000
361 date: Thu Jan 01 00:00:00 1970 +0000
296 summary: 3-4-2
362 summary: 3-4-2
297
363
298 comparing with $TESTTMP/repo/foo
364 comparing with $TESTTMP/repo/foo
299 searching for changes
365 searching for changes
300 changeset: 4:e96193d6cb36
366 changeset: 4:e96193d6cb36
301 tag: tip
367 tag: tip
302 user: test
368 user: test
303 date: Thu Jan 01 00:00:00 1970 +0000
369 date: Thu Jan 01 00:00:00 1970 +0000
304 summary: 3-4-2
370 summary: 3-4-2
305
371
306 comparing with $TESTTMP/repo/foo/bar
372 comparing with $TESTTMP/repo/foo/bar
307 searching for changes
373 searching for changes
308 no changes found
374 no changes found
309
375
310
376
311 Switch to original repo and setup default path:
377 Switch to original repo and setup default path:
312
378
313 $ cd ../repo
379 $ cd ../repo
314 $ echo '[paths]' >> .hg/hgrc
380 $ echo '[paths]' >> .hg/hgrc
315 $ echo 'default = ../repo2' >> .hg/hgrc
381 $ echo 'default = ../repo2' >> .hg/hgrc
316
382
317 Test incoming:
383 Test incoming:
318
384
319 $ hg incoming -S
385 $ hg incoming -S
320 comparing with $TESTTMP/repo2
386 comparing with $TESTTMP/repo2
321 searching for changes
387 searching for changes
322 changeset: 3:2655b8ecc4ee
388 changeset: 3:2655b8ecc4ee
323 tag: tip
389 tag: tip
324 user: test
390 user: test
325 date: Thu Jan 01 00:00:00 1970 +0000
391 date: Thu Jan 01 00:00:00 1970 +0000
326 summary: 3-4-2
392 summary: 3-4-2
327
393
328 comparing with $TESTTMP/repo2/foo
394 comparing with $TESTTMP/repo2/foo
329 searching for changes
395 searching for changes
330 changeset: 4:e96193d6cb36
396 changeset: 4:e96193d6cb36
331 tag: tip
397 tag: tip
332 user: test
398 user: test
333 date: Thu Jan 01 00:00:00 1970 +0000
399 date: Thu Jan 01 00:00:00 1970 +0000
334 summary: 3-4-2
400 summary: 3-4-2
335
401
336 comparing with $TESTTMP/repo2/foo/bar
402 comparing with $TESTTMP/repo2/foo/bar
337 searching for changes
403 searching for changes
338 no changes found
404 no changes found
339
405
340 $ hg incoming -S --bundle incoming.hg
406 $ hg incoming -S --bundle incoming.hg
341 abort: cannot combine --bundle and --subrepos
407 abort: cannot combine --bundle and --subrepos
342 [255]
408 [255]
343
409
344 Test missing subrepo:
410 Test missing subrepo:
345
411
346 $ rm -r foo
412 $ rm -r foo
347 $ hg status -S
413 $ hg status -S
348 warning: error "unknown revision '65903cebad86f1a84bd4f1134f62fa7dcb7a1c98'" in subrepository "foo"
414 warning: error "unknown revision '65903cebad86f1a84bd4f1134f62fa7dcb7a1c98'" in subrepository "foo"
General Comments 0
You need to be logged in to leave comments. Login now