##// END OF EJS Templates
Merge with stable
Patrick Mezard -
r13333:60792fa3 merge default
parent child Browse files
Show More
@@ -1,159 +1,159 b''
1 # win32mbcs.py -- MBCS filename support for Mercurial
1 # win32mbcs.py -- MBCS filename support for Mercurial
2 #
2 #
3 # Copyright (c) 2008 Shun-ichi Goto <shunichi.goto@gmail.com>
3 # Copyright (c) 2008 Shun-ichi Goto <shunichi.goto@gmail.com>
4 #
4 #
5 # Version: 0.3
5 # Version: 0.3
6 # Author: Shun-ichi Goto <shunichi.goto@gmail.com>
6 # Author: Shun-ichi Goto <shunichi.goto@gmail.com>
7 #
7 #
8 # This software may be used and distributed according to the terms of the
8 # This software may be used and distributed according to the terms of the
9 # GNU General Public License version 2 or any later version.
9 # GNU General Public License version 2 or any later version.
10 #
10 #
11
11
12 '''allow the use of MBCS paths with problematic encodings
12 '''allow the use of MBCS paths with problematic encodings
13
13
14 Some MBCS encodings are not good for some path operations (i.e.
14 Some MBCS encodings are not good for some path operations (i.e.
15 splitting path, case conversion, etc.) with its encoded bytes. We call
15 splitting path, case conversion, etc.) with its encoded bytes. We call
16 such a encoding (i.e. shift_jis and big5) as "problematic encoding".
16 such a encoding (i.e. shift_jis and big5) as "problematic encoding".
17 This extension can be used to fix the issue with those encodings by
17 This extension can be used to fix the issue with those encodings by
18 wrapping some functions to convert to Unicode string before path
18 wrapping some functions to convert to Unicode string before path
19 operation.
19 operation.
20
20
21 This extension is useful for:
21 This extension is useful for:
22
22
23 - Japanese Windows users using shift_jis encoding.
23 - Japanese Windows users using shift_jis encoding.
24 - Chinese Windows users using big5 encoding.
24 - Chinese Windows users using big5 encoding.
25 - All users who use a repository with one of problematic encodings on
25 - All users who use a repository with one of problematic encodings on
26 case-insensitive file system.
26 case-insensitive file system.
27
27
28 This extension is not needed for:
28 This extension is not needed for:
29
29
30 - Any user who use only ASCII chars in path.
30 - Any user who use only ASCII chars in path.
31 - Any user who do not use any of problematic encodings.
31 - Any user who do not use any of problematic encodings.
32
32
33 Note that there are some limitations on using this extension:
33 Note that there are some limitations on using this extension:
34
34
35 - You should use single encoding in one repository.
35 - You should use single encoding in one repository.
36 - If the repository path ends with 0x5c, .hg/hgrc cannot be read.
36 - If the repository path ends with 0x5c, .hg/hgrc cannot be read.
37 - win32mbcs is not compatible with fixutf8 extention.
37 - win32mbcs is not compatible with fixutf8 extension.
38
38
39 By default, win32mbcs uses encoding.encoding decided by Mercurial.
39 By default, win32mbcs uses encoding.encoding decided by Mercurial.
40 You can specify the encoding by config option::
40 You can specify the encoding by config option::
41
41
42 [win32mbcs]
42 [win32mbcs]
43 encoding = sjis
43 encoding = sjis
44
44
45 It is useful for the users who want to commit with UTF-8 log message.
45 It is useful for the users who want to commit with UTF-8 log message.
46 '''
46 '''
47
47
48 import os, sys
48 import os, sys
49 from mercurial.i18n import _
49 from mercurial.i18n import _
50 from mercurial import util, encoding
50 from mercurial import util, encoding
51
51
52 _encoding = None # see extsetup
52 _encoding = None # see extsetup
53
53
54 def decode(arg):
54 def decode(arg):
55 if isinstance(arg, str):
55 if isinstance(arg, str):
56 uarg = arg.decode(_encoding)
56 uarg = arg.decode(_encoding)
57 if arg == uarg.encode(_encoding):
57 if arg == uarg.encode(_encoding):
58 return uarg
58 return uarg
59 raise UnicodeError("Not local encoding")
59 raise UnicodeError("Not local encoding")
60 elif isinstance(arg, tuple):
60 elif isinstance(arg, tuple):
61 return tuple(map(decode, arg))
61 return tuple(map(decode, arg))
62 elif isinstance(arg, list):
62 elif isinstance(arg, list):
63 return map(decode, arg)
63 return map(decode, arg)
64 elif isinstance(arg, dict):
64 elif isinstance(arg, dict):
65 for k, v in arg.items():
65 for k, v in arg.items():
66 arg[k] = decode(v)
66 arg[k] = decode(v)
67 return arg
67 return arg
68
68
69 def encode(arg):
69 def encode(arg):
70 if isinstance(arg, unicode):
70 if isinstance(arg, unicode):
71 return arg.encode(_encoding)
71 return arg.encode(_encoding)
72 elif isinstance(arg, tuple):
72 elif isinstance(arg, tuple):
73 return tuple(map(encode, arg))
73 return tuple(map(encode, arg))
74 elif isinstance(arg, list):
74 elif isinstance(arg, list):
75 return map(encode, arg)
75 return map(encode, arg)
76 elif isinstance(arg, dict):
76 elif isinstance(arg, dict):
77 for k, v in arg.items():
77 for k, v in arg.items():
78 arg[k] = encode(v)
78 arg[k] = encode(v)
79 return arg
79 return arg
80
80
81 def appendsep(s):
81 def appendsep(s):
82 # ensure the path ends with os.sep, appending it if necessary.
82 # ensure the path ends with os.sep, appending it if necessary.
83 try:
83 try:
84 us = decode(s)
84 us = decode(s)
85 except UnicodeError:
85 except UnicodeError:
86 us = s
86 us = s
87 if us and us[-1] not in ':/\\':
87 if us and us[-1] not in ':/\\':
88 s += os.sep
88 s += os.sep
89 return s
89 return s
90
90
91 def wrapper(func, args, kwds):
91 def wrapper(func, args, kwds):
92 # check argument is unicode, then call original
92 # check argument is unicode, then call original
93 for arg in args:
93 for arg in args:
94 if isinstance(arg, unicode):
94 if isinstance(arg, unicode):
95 return func(*args, **kwds)
95 return func(*args, **kwds)
96
96
97 try:
97 try:
98 # convert arguments to unicode, call func, then convert back
98 # convert arguments to unicode, call func, then convert back
99 return encode(func(*decode(args), **decode(kwds)))
99 return encode(func(*decode(args), **decode(kwds)))
100 except UnicodeError:
100 except UnicodeError:
101 raise util.Abort(_("[win32mbcs] filename conversion failed with"
101 raise util.Abort(_("[win32mbcs] filename conversion failed with"
102 " %s encoding\n") % (_encoding))
102 " %s encoding\n") % (_encoding))
103
103
104 def wrapperforlistdir(func, args, kwds):
104 def wrapperforlistdir(func, args, kwds):
105 # Ensure 'path' argument ends with os.sep to avoids
105 # Ensure 'path' argument ends with os.sep to avoids
106 # misinterpreting last 0x5c of MBCS 2nd byte as path separator.
106 # misinterpreting last 0x5c of MBCS 2nd byte as path separator.
107 if args:
107 if args:
108 args = list(args)
108 args = list(args)
109 args[0] = appendsep(args[0])
109 args[0] = appendsep(args[0])
110 if 'path' in kwds:
110 if 'path' in kwds:
111 kwds['path'] = appendsep(kwds['path'])
111 kwds['path'] = appendsep(kwds['path'])
112 return func(*args, **kwds)
112 return func(*args, **kwds)
113
113
114 def wrapname(name, wrapper):
114 def wrapname(name, wrapper):
115 module, name = name.rsplit('.', 1)
115 module, name = name.rsplit('.', 1)
116 module = sys.modules[module]
116 module = sys.modules[module]
117 func = getattr(module, name)
117 func = getattr(module, name)
118 def f(*args, **kwds):
118 def f(*args, **kwds):
119 return wrapper(func, args, kwds)
119 return wrapper(func, args, kwds)
120 try:
120 try:
121 f.__name__ = func.__name__ # fail with python23
121 f.__name__ = func.__name__ # fail with python23
122 except Exception:
122 except Exception:
123 pass
123 pass
124 setattr(module, name, f)
124 setattr(module, name, f)
125
125
126 # List of functions to be wrapped.
126 # List of functions to be wrapped.
127 # NOTE: os.path.dirname() and os.path.basename() are safe because
127 # NOTE: os.path.dirname() and os.path.basename() are safe because
128 # they use result of os.path.split()
128 # they use result of os.path.split()
129 funcs = '''os.path.join os.path.split os.path.splitext
129 funcs = '''os.path.join os.path.split os.path.splitext
130 os.path.splitunc os.path.normpath os.path.normcase os.makedirs
130 os.path.splitunc os.path.normpath os.path.normcase os.makedirs
131 mercurial.util.endswithsep mercurial.util.splitpath mercurial.util.checkcase
131 mercurial.util.endswithsep mercurial.util.splitpath mercurial.util.checkcase
132 mercurial.util.fspath mercurial.util.pconvert mercurial.util.normpath'''
132 mercurial.util.fspath mercurial.util.pconvert mercurial.util.normpath'''
133
133
134 # codec and alias names of sjis and big5 to be faked.
134 # codec and alias names of sjis and big5 to be faked.
135 problematic_encodings = '''big5 big5-tw csbig5 big5hkscs big5-hkscs
135 problematic_encodings = '''big5 big5-tw csbig5 big5hkscs big5-hkscs
136 hkscs cp932 932 ms932 mskanji ms-kanji shift_jis csshiftjis shiftjis
136 hkscs cp932 932 ms932 mskanji ms-kanji shift_jis csshiftjis shiftjis
137 sjis s_jis shift_jis_2004 shiftjis2004 sjis_2004 sjis2004
137 sjis s_jis shift_jis_2004 shiftjis2004 sjis_2004 sjis2004
138 shift_jisx0213 shiftjisx0213 sjisx0213 s_jisx0213 950 cp950 ms950 '''
138 shift_jisx0213 shiftjisx0213 sjisx0213 s_jisx0213 950 cp950 ms950 '''
139
139
140 def extsetup(ui):
140 def extsetup(ui):
141 # TODO: decide use of config section for this extension
141 # TODO: decide use of config section for this extension
142 if not os.path.supports_unicode_filenames:
142 if not os.path.supports_unicode_filenames:
143 ui.warn(_("[win32mbcs] cannot activate on this platform.\n"))
143 ui.warn(_("[win32mbcs] cannot activate on this platform.\n"))
144 return
144 return
145 # determine encoding for filename
145 # determine encoding for filename
146 global _encoding
146 global _encoding
147 _encoding = ui.config('win32mbcs', 'encoding', encoding.encoding)
147 _encoding = ui.config('win32mbcs', 'encoding', encoding.encoding)
148 # fake is only for relevant environment.
148 # fake is only for relevant environment.
149 if _encoding.lower() in problematic_encodings.split():
149 if _encoding.lower() in problematic_encodings.split():
150 for f in funcs.split():
150 for f in funcs.split():
151 wrapname(f, wrapper)
151 wrapname(f, wrapper)
152 wrapname("mercurial.osutil.listdir", wrapperforlistdir)
152 wrapname("mercurial.osutil.listdir", wrapperforlistdir)
153 # Check sys.args manually instead of using ui.debug() because
153 # Check sys.args manually instead of using ui.debug() because
154 # command line options is not yet applied when
154 # command line options is not yet applied when
155 # extensions.loadall() is called.
155 # extensions.loadall() is called.
156 if '--debug' in sys.argv:
156 if '--debug' in sys.argv:
157 ui.write("[win32mbcs] activated with encoding: %s\n"
157 ui.write("[win32mbcs] activated with encoding: %s\n"
158 % _encoding)
158 % _encoding)
159
159
@@ -1,961 +1,961 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, overwrite):
85 def submerge(repo, wctx, mctx, actx, overwrite):
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, overwrite)
117 wctx.sub(s).get(r, overwrite)
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, overwrite)
126 wctx.sub(s).get(r, overwrite)
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, overwrite)
130 wctx.sub(s).get(r, overwrite)
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 hasattr(sub, '_relpath'):
175 if hasattr(sub, '_relpath'):
176 return sub._relpath
176 return sub._relpath
177 if not hasattr(sub, '_repo'):
177 if not hasattr(sub, '_repo'):
178 return sub._path
178 return sub._path
179 return reporelpath(sub._repo)
179 return reporelpath(sub._repo)
180
180
181 def _abssource(repo, push=False, abort=True):
181 def _abssource(repo, push=False, abort=True):
182 """return pull/push path of repo - either based on parent repo .hgsub info
182 """return pull/push path of repo - either based on parent repo .hgsub info
183 or on the top repo config. Abort or return None if no source found."""
183 or on the top repo config. Abort or return None if no source found."""
184 if hasattr(repo, '_subparent'):
184 if hasattr(repo, '_subparent'):
185 source = repo._subsource
185 source = repo._subsource
186 if source.startswith('/') or '://' in source:
186 if source.startswith('/') or '://' in source:
187 return source
187 return source
188 parent = _abssource(repo._subparent, push, abort=False)
188 parent = _abssource(repo._subparent, push, abort=False)
189 if parent:
189 if parent:
190 if '://' in parent:
190 if '://' in parent:
191 if parent[-1] == '/':
191 if parent[-1] == '/':
192 parent = parent[:-1]
192 parent = parent[:-1]
193 r = urlparse.urlparse(parent + '/' + source)
193 r = urlparse.urlparse(parent + '/' + source)
194 r = urlparse.urlunparse((r[0], r[1],
194 r = urlparse.urlunparse((r[0], r[1],
195 posixpath.normpath(r[2]),
195 posixpath.normpath(r[2]),
196 r[3], r[4], r[5]))
196 r[3], r[4], r[5]))
197 return r
197 return r
198 else: # plain file system path
198 else: # plain file system path
199 return posixpath.normpath(os.path.join(parent, repo._subsource))
199 return posixpath.normpath(os.path.join(parent, repo._subsource))
200 else: # recursion reached top repo
200 else: # recursion reached top repo
201 if hasattr(repo, '_subtoppath'):
201 if hasattr(repo, '_subtoppath'):
202 return repo._subtoppath
202 return repo._subtoppath
203 if push and repo.ui.config('paths', 'default-push'):
203 if push and repo.ui.config('paths', 'default-push'):
204 return repo.ui.config('paths', 'default-push')
204 return repo.ui.config('paths', 'default-push')
205 if repo.ui.config('paths', 'default'):
205 if repo.ui.config('paths', 'default'):
206 return repo.ui.config('paths', 'default')
206 return repo.ui.config('paths', 'default')
207 if abort:
207 if abort:
208 raise util.Abort(_("default path for subrepository %s not found") %
208 raise util.Abort(_("default path for subrepository %s not found") %
209 reporelpath(repo))
209 reporelpath(repo))
210
210
211 def itersubrepos(ctx1, ctx2):
211 def itersubrepos(ctx1, ctx2):
212 """find subrepos in ctx1 or ctx2"""
212 """find subrepos in ctx1 or ctx2"""
213 # Create a (subpath, ctx) mapping where we prefer subpaths from
213 # Create a (subpath, ctx) mapping where we prefer subpaths from
214 # ctx1. The subpaths from ctx2 are important when the .hgsub file
214 # ctx1. The subpaths from ctx2 are important when the .hgsub file
215 # has been modified (in ctx2) but not yet committed (in ctx1).
215 # has been modified (in ctx2) but not yet committed (in ctx1).
216 subpaths = dict.fromkeys(ctx2.substate, ctx2)
216 subpaths = dict.fromkeys(ctx2.substate, ctx2)
217 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
217 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
218 for subpath, ctx in sorted(subpaths.iteritems()):
218 for subpath, ctx in sorted(subpaths.iteritems()):
219 yield subpath, ctx.sub(subpath)
219 yield subpath, ctx.sub(subpath)
220
220
221 def subrepo(ctx, path):
221 def subrepo(ctx, path):
222 """return instance of the right subrepo class for subrepo in path"""
222 """return instance of the right subrepo class for subrepo in path"""
223 # subrepo inherently violates our import layering rules
223 # subrepo inherently violates our import layering rules
224 # because it wants to make repo objects from deep inside the stack
224 # because it wants to make repo objects from deep inside the stack
225 # so we manually delay the circular imports to not break
225 # so we manually delay the circular imports to not break
226 # scripts that don't use our demand-loading
226 # scripts that don't use our demand-loading
227 global hg
227 global hg
228 import hg as h
228 import hg as h
229 hg = h
229 hg = h
230
230
231 util.path_auditor(ctx._repo.root)(path)
231 util.path_auditor(ctx._repo.root)(path)
232 state = ctx.substate.get(path, nullstate)
232 state = ctx.substate.get(path, nullstate)
233 if state[2] not in types:
233 if state[2] not in types:
234 raise util.Abort(_('unknown subrepo type %s') % state[2])
234 raise util.Abort(_('unknown subrepo type %s') % state[2])
235 return types[state[2]](ctx, path, state[:2])
235 return types[state[2]](ctx, path, state[:2])
236
236
237 # subrepo classes need to implement the following abstract class:
237 # subrepo classes need to implement the following abstract class:
238
238
239 class abstractsubrepo(object):
239 class abstractsubrepo(object):
240
240
241 def dirty(self, ignoreupdate=False):
241 def dirty(self, ignoreupdate=False):
242 """returns true if the dirstate of the subrepo is dirty or does not
242 """returns true if the dirstate of the subrepo is dirty or does not
243 match current stored state. If ignoreupdate is true, only check
243 match current stored state. If ignoreupdate is true, only check
244 whether the subrepo has uncommitted changes in its dirstate.
244 whether the subrepo has uncommitted changes in its dirstate.
245 """
245 """
246 raise NotImplementedError
246 raise NotImplementedError
247
247
248 def checknested(self, path):
248 def checknested(self, path):
249 """check if path is a subrepository within this repository"""
249 """check if path is a subrepository within this repository"""
250 return False
250 return False
251
251
252 def commit(self, text, user, date):
252 def commit(self, text, user, date):
253 """commit the current changes to the subrepo with the given
253 """commit the current changes to the subrepo with the given
254 log message. Use given user and date if possible. Return the
254 log message. Use given user and date if possible. Return the
255 new state of the subrepo.
255 new state of the subrepo.
256 """
256 """
257 raise NotImplementedError
257 raise NotImplementedError
258
258
259 def remove(self):
259 def remove(self):
260 """remove the subrepo
260 """remove the subrepo
261
261
262 (should verify the dirstate is not dirty first)
262 (should verify the dirstate is not dirty first)
263 """
263 """
264 raise NotImplementedError
264 raise NotImplementedError
265
265
266 def get(self, state, overwrite=False):
266 def get(self, state, overwrite=False):
267 """run whatever commands are needed to put the subrepo into
267 """run whatever commands are needed to put the subrepo into
268 this state
268 this state
269 """
269 """
270 raise NotImplementedError
270 raise NotImplementedError
271
271
272 def merge(self, state, overwrite=False):
272 def merge(self, state, overwrite=False):
273 """merge currently-saved state with the new state."""
273 """merge currently-saved state with the new state."""
274 raise NotImplementedError
274 raise NotImplementedError
275
275
276 def push(self, force):
276 def push(self, force):
277 """perform whatever action is analogous to 'hg push'
277 """perform whatever action is analogous to 'hg push'
278
278
279 This may be a no-op on some systems.
279 This may be a no-op on some systems.
280 """
280 """
281 raise NotImplementedError
281 raise NotImplementedError
282
282
283 def add(self, ui, match, dryrun, prefix):
283 def add(self, ui, match, dryrun, prefix):
284 return []
284 return []
285
285
286 def status(self, rev2, **opts):
286 def status(self, rev2, **opts):
287 return [], [], [], [], [], [], []
287 return [], [], [], [], [], [], []
288
288
289 def diff(self, diffopts, node2, match, prefix, **opts):
289 def diff(self, diffopts, node2, match, prefix, **opts):
290 pass
290 pass
291
291
292 def outgoing(self, ui, dest, opts):
292 def outgoing(self, ui, dest, opts):
293 return 1
293 return 1
294
294
295 def incoming(self, ui, source, opts):
295 def incoming(self, ui, source, opts):
296 return 1
296 return 1
297
297
298 def files(self):
298 def files(self):
299 """return filename iterator"""
299 """return filename iterator"""
300 raise NotImplementedError
300 raise NotImplementedError
301
301
302 def filedata(self, name):
302 def filedata(self, name):
303 """return file data"""
303 """return file data"""
304 raise NotImplementedError
304 raise NotImplementedError
305
305
306 def fileflags(self, name):
306 def fileflags(self, name):
307 """return file flags"""
307 """return file flags"""
308 return ''
308 return ''
309
309
310 def archive(self, ui, archiver, prefix):
310 def archive(self, ui, archiver, prefix):
311 files = self.files()
311 files = self.files()
312 total = len(files)
312 total = len(files)
313 relpath = subrelpath(self)
313 relpath = subrelpath(self)
314 ui.progress(_('archiving (%s)') % relpath, 0,
314 ui.progress(_('archiving (%s)') % relpath, 0,
315 unit=_('files'), total=total)
315 unit=_('files'), total=total)
316 for i, name in enumerate(files):
316 for i, name in enumerate(files):
317 flags = self.fileflags(name)
317 flags = self.fileflags(name)
318 mode = 'x' in flags and 0755 or 0644
318 mode = 'x' in flags and 0755 or 0644
319 symlink = 'l' in flags
319 symlink = 'l' in flags
320 archiver.addfile(os.path.join(prefix, self._path, name),
320 archiver.addfile(os.path.join(prefix, self._path, name),
321 mode, symlink, self.filedata(name))
321 mode, symlink, self.filedata(name))
322 ui.progress(_('archiving (%s)') % relpath, i + 1,
322 ui.progress(_('archiving (%s)') % relpath, i + 1,
323 unit=_('files'), total=total)
323 unit=_('files'), total=total)
324 ui.progress(_('archiving (%s)') % relpath, None)
324 ui.progress(_('archiving (%s)') % relpath, None)
325
325
326
326
327 class hgsubrepo(abstractsubrepo):
327 class hgsubrepo(abstractsubrepo):
328 def __init__(self, ctx, path, state):
328 def __init__(self, ctx, path, state):
329 self._path = path
329 self._path = path
330 self._state = state
330 self._state = state
331 r = ctx._repo
331 r = ctx._repo
332 root = r.wjoin(path)
332 root = r.wjoin(path)
333 create = False
333 create = False
334 if not os.path.exists(os.path.join(root, '.hg')):
334 if not os.path.exists(os.path.join(root, '.hg')):
335 create = True
335 create = True
336 util.makedirs(root)
336 util.makedirs(root)
337 self._repo = hg.repository(r.ui, root, create=create)
337 self._repo = hg.repository(r.ui, root, create=create)
338 self._repo._subparent = r
338 self._repo._subparent = r
339 self._repo._subsource = state[0]
339 self._repo._subsource = state[0]
340
340
341 if create:
341 if create:
342 fp = self._repo.opener("hgrc", "w", text=True)
342 fp = self._repo.opener("hgrc", "w", text=True)
343 fp.write('[paths]\n')
343 fp.write('[paths]\n')
344
344
345 def addpathconfig(key, value):
345 def addpathconfig(key, value):
346 if value:
346 if value:
347 fp.write('%s = %s\n' % (key, value))
347 fp.write('%s = %s\n' % (key, value))
348 self._repo.ui.setconfig('paths', key, value)
348 self._repo.ui.setconfig('paths', key, value)
349
349
350 defpath = _abssource(self._repo, abort=False)
350 defpath = _abssource(self._repo, abort=False)
351 defpushpath = _abssource(self._repo, True, abort=False)
351 defpushpath = _abssource(self._repo, True, abort=False)
352 addpathconfig('default', defpath)
352 addpathconfig('default', defpath)
353 if defpath != defpushpath:
353 if defpath != defpushpath:
354 addpathconfig('default-push', defpushpath)
354 addpathconfig('default-push', defpushpath)
355 fp.close()
355 fp.close()
356
356
357 def add(self, ui, match, dryrun, prefix):
357 def add(self, ui, match, dryrun, prefix):
358 return cmdutil.add(ui, self._repo, match, dryrun, True,
358 return cmdutil.add(ui, self._repo, match, dryrun, True,
359 os.path.join(prefix, self._path))
359 os.path.join(prefix, self._path))
360
360
361 def status(self, rev2, **opts):
361 def status(self, rev2, **opts):
362 try:
362 try:
363 rev1 = self._state[1]
363 rev1 = self._state[1]
364 ctx1 = self._repo[rev1]
364 ctx1 = self._repo[rev1]
365 ctx2 = self._repo[rev2]
365 ctx2 = self._repo[rev2]
366 return self._repo.status(ctx1, ctx2, **opts)
366 return self._repo.status(ctx1, ctx2, **opts)
367 except error.RepoLookupError, inst:
367 except error.RepoLookupError, inst:
368 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
368 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
369 % (inst, subrelpath(self)))
369 % (inst, subrelpath(self)))
370 return [], [], [], [], [], [], []
370 return [], [], [], [], [], [], []
371
371
372 def diff(self, diffopts, node2, match, prefix, **opts):
372 def diff(self, diffopts, node2, match, prefix, **opts):
373 try:
373 try:
374 node1 = node.bin(self._state[1])
374 node1 = node.bin(self._state[1])
375 # We currently expect node2 to come from substate and be
375 # We currently expect node2 to come from substate and be
376 # in hex format
376 # in hex format
377 if node2 is not None:
377 if node2 is not None:
378 node2 = node.bin(node2)
378 node2 = node.bin(node2)
379 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
379 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
380 node1, node2, match,
380 node1, node2, match,
381 prefix=os.path.join(prefix, self._path),
381 prefix=os.path.join(prefix, self._path),
382 listsubrepos=True, **opts)
382 listsubrepos=True, **opts)
383 except error.RepoLookupError, inst:
383 except error.RepoLookupError, inst:
384 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
384 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
385 % (inst, subrelpath(self)))
385 % (inst, subrelpath(self)))
386
386
387 def archive(self, ui, archiver, prefix):
387 def archive(self, ui, archiver, prefix):
388 abstractsubrepo.archive(self, ui, archiver, prefix)
388 abstractsubrepo.archive(self, ui, archiver, prefix)
389
389
390 rev = self._state[1]
390 rev = self._state[1]
391 ctx = self._repo[rev]
391 ctx = self._repo[rev]
392 for subpath in ctx.substate:
392 for subpath in ctx.substate:
393 s = subrepo(ctx, subpath)
393 s = subrepo(ctx, subpath)
394 s.archive(ui, archiver, os.path.join(prefix, self._path))
394 s.archive(ui, archiver, os.path.join(prefix, self._path))
395
395
396 def dirty(self, ignoreupdate=False):
396 def dirty(self, ignoreupdate=False):
397 r = self._state[1]
397 r = self._state[1]
398 if r == '' and not ignoreupdate: # no state recorded
398 if r == '' and not ignoreupdate: # no state recorded
399 return True
399 return True
400 w = self._repo[None]
400 w = self._repo[None]
401 if w.p1() != self._repo[r] and not ignoreupdate:
401 if w.p1() != self._repo[r] and not ignoreupdate:
402 # different version checked out
402 # different version checked out
403 return True
403 return True
404 return w.dirty() # working directory changed
404 return w.dirty() # working directory changed
405
405
406 def checknested(self, path):
406 def checknested(self, path):
407 return self._repo._checknested(self._repo.wjoin(path))
407 return self._repo._checknested(self._repo.wjoin(path))
408
408
409 def commit(self, text, user, date):
409 def commit(self, text, user, date):
410 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
410 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
411 n = self._repo.commit(text, user, date)
411 n = self._repo.commit(text, user, date)
412 if not n:
412 if not n:
413 return self._repo['.'].hex() # different version checked out
413 return self._repo['.'].hex() # different version checked out
414 return node.hex(n)
414 return node.hex(n)
415
415
416 def remove(self):
416 def remove(self):
417 # we can't fully delete the repository as it may contain
417 # we can't fully delete the repository as it may contain
418 # local-only history
418 # local-only history
419 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
419 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
420 hg.clean(self._repo, node.nullid, False)
420 hg.clean(self._repo, node.nullid, False)
421
421
422 def _get(self, state):
422 def _get(self, state):
423 source, revision, kind = state
423 source, revision, kind = state
424 try:
424 try:
425 self._repo.lookup(revision)
425 self._repo.lookup(revision)
426 except error.RepoError:
426 except error.RepoError:
427 self._repo._subsource = source
427 self._repo._subsource = source
428 srcurl = _abssource(self._repo)
428 srcurl = _abssource(self._repo)
429 self._repo.ui.status(_('pulling subrepo %s from %s\n')
429 self._repo.ui.status(_('pulling subrepo %s from %s\n')
430 % (subrelpath(self), srcurl))
430 % (subrelpath(self), srcurl))
431 other = hg.repository(self._repo.ui, srcurl)
431 other = hg.repository(self._repo.ui, srcurl)
432 self._repo.pull(other)
432 self._repo.pull(other)
433
433
434 def get(self, state, overwrite=False):
434 def get(self, state, overwrite=False):
435 self._get(state)
435 self._get(state)
436 source, revision, kind = state
436 source, revision, kind = state
437 self._repo.ui.debug("getting subrepo %s\n" % self._path)
437 self._repo.ui.debug("getting subrepo %s\n" % self._path)
438 hg.clean(self._repo, revision, False)
438 hg.clean(self._repo, revision, False)
439
439
440 def merge(self, state):
440 def merge(self, state):
441 self._get(state)
441 self._get(state)
442 cur = self._repo['.']
442 cur = self._repo['.']
443 dst = self._repo[state[1]]
443 dst = self._repo[state[1]]
444 anc = dst.ancestor(cur)
444 anc = dst.ancestor(cur)
445 if anc == cur:
445 if anc == cur:
446 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
446 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
447 hg.update(self._repo, state[1])
447 hg.update(self._repo, state[1])
448 elif anc == dst:
448 elif anc == dst:
449 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
449 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
450 else:
450 else:
451 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
451 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
452 hg.merge(self._repo, state[1], remind=False)
452 hg.merge(self._repo, state[1], remind=False)
453
453
454 def push(self, force):
454 def push(self, force):
455 # push subrepos depth-first for coherent ordering
455 # push subrepos depth-first for coherent ordering
456 c = self._repo['']
456 c = self._repo['']
457 subs = c.substate # only repos that are committed
457 subs = c.substate # only repos that are committed
458 for s in sorted(subs):
458 for s in sorted(subs):
459 if not c.sub(s).push(force):
459 if not c.sub(s).push(force):
460 return False
460 return False
461
461
462 dsturl = _abssource(self._repo, True)
462 dsturl = _abssource(self._repo, True)
463 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
463 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
464 (subrelpath(self), dsturl))
464 (subrelpath(self), dsturl))
465 other = hg.repository(self._repo.ui, dsturl)
465 other = hg.repository(self._repo.ui, dsturl)
466 return self._repo.push(other, force)
466 return self._repo.push(other, force)
467
467
468 def outgoing(self, ui, dest, opts):
468 def outgoing(self, ui, dest, opts):
469 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
469 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
470
470
471 def incoming(self, ui, source, opts):
471 def incoming(self, ui, source, opts):
472 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
472 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
473
473
474 def files(self):
474 def files(self):
475 rev = self._state[1]
475 rev = self._state[1]
476 ctx = self._repo[rev]
476 ctx = self._repo[rev]
477 return ctx.manifest()
477 return ctx.manifest()
478
478
479 def filedata(self, name):
479 def filedata(self, name):
480 rev = self._state[1]
480 rev = self._state[1]
481 return self._repo[rev][name].data()
481 return self._repo[rev][name].data()
482
482
483 def fileflags(self, name):
483 def fileflags(self, name):
484 rev = self._state[1]
484 rev = self._state[1]
485 ctx = self._repo[rev]
485 ctx = self._repo[rev]
486 return ctx.flags(name)
486 return ctx.flags(name)
487
487
488
488
489 class svnsubrepo(abstractsubrepo):
489 class svnsubrepo(abstractsubrepo):
490 def __init__(self, ctx, path, state):
490 def __init__(self, ctx, path, state):
491 self._path = path
491 self._path = path
492 self._state = state
492 self._state = state
493 self._ctx = ctx
493 self._ctx = ctx
494 self._ui = ctx._repo.ui
494 self._ui = ctx._repo.ui
495
495
496 def _svncommand(self, commands, filename=''):
496 def _svncommand(self, commands, filename=''):
497 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
497 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
498 cmd = ['svn'] + commands + [path]
498 cmd = ['svn'] + commands + [path]
499 env = dict(os.environ)
499 env = dict(os.environ)
500 # Avoid localized output, preserve current locale for everything else.
500 # Avoid localized output, preserve current locale for everything else.
501 env['LC_MESSAGES'] = 'C'
501 env['LC_MESSAGES'] = 'C'
502 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
502 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
503 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
503 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
504 universal_newlines=True, env=env)
504 universal_newlines=True, env=env)
505 stdout, stderr = p.communicate()
505 stdout, stderr = p.communicate()
506 stderr = stderr.strip()
506 stderr = stderr.strip()
507 if stderr:
507 if stderr:
508 raise util.Abort(stderr)
508 raise util.Abort(stderr)
509 return stdout
509 return stdout
510
510
511 def _wcrevs(self):
511 def _wcrevs(self):
512 # Get the working directory revision as well as the last
512 # Get the working directory revision as well as the last
513 # commit revision so we can compare the subrepo state with
513 # commit revision so we can compare the subrepo state with
514 # both. We used to store the working directory one.
514 # both. We used to store the working directory one.
515 output = self._svncommand(['info', '--xml'])
515 output = self._svncommand(['info', '--xml'])
516 doc = xml.dom.minidom.parseString(output)
516 doc = xml.dom.minidom.parseString(output)
517 entries = doc.getElementsByTagName('entry')
517 entries = doc.getElementsByTagName('entry')
518 lastrev, rev = '0', '0'
518 lastrev, rev = '0', '0'
519 if entries:
519 if entries:
520 rev = str(entries[0].getAttribute('revision')) or '0'
520 rev = str(entries[0].getAttribute('revision')) or '0'
521 commits = entries[0].getElementsByTagName('commit')
521 commits = entries[0].getElementsByTagName('commit')
522 if commits:
522 if commits:
523 lastrev = str(commits[0].getAttribute('revision')) or '0'
523 lastrev = str(commits[0].getAttribute('revision')) or '0'
524 return (lastrev, rev)
524 return (lastrev, rev)
525
525
526 def _wcrev(self):
526 def _wcrev(self):
527 return self._wcrevs()[0]
527 return self._wcrevs()[0]
528
528
529 def _wcchanged(self):
529 def _wcchanged(self):
530 """Return (changes, extchanges) where changes is True
530 """Return (changes, extchanges) where changes is True
531 if the working directory was changed, and extchanges is
531 if the working directory was changed, and extchanges is
532 True if any of these changes concern an external entry.
532 True if any of these changes concern an external entry.
533 """
533 """
534 output = self._svncommand(['status', '--xml'])
534 output = self._svncommand(['status', '--xml'])
535 externals, changes = [], []
535 externals, changes = [], []
536 doc = xml.dom.minidom.parseString(output)
536 doc = xml.dom.minidom.parseString(output)
537 for e in doc.getElementsByTagName('entry'):
537 for e in doc.getElementsByTagName('entry'):
538 s = e.getElementsByTagName('wc-status')
538 s = e.getElementsByTagName('wc-status')
539 if not s:
539 if not s:
540 continue
540 continue
541 item = s[0].getAttribute('item')
541 item = s[0].getAttribute('item')
542 props = s[0].getAttribute('props')
542 props = s[0].getAttribute('props')
543 path = e.getAttribute('path')
543 path = e.getAttribute('path')
544 if item == 'external':
544 if item == 'external':
545 externals.append(path)
545 externals.append(path)
546 if (item not in ('', 'normal', 'unversioned', 'external')
546 if (item not in ('', 'normal', 'unversioned', 'external')
547 or props not in ('', 'none')):
547 or props not in ('', 'none')):
548 changes.append(path)
548 changes.append(path)
549 for path in changes:
549 for path in changes:
550 for ext in externals:
550 for ext in externals:
551 if path == ext or path.startswith(ext + os.sep):
551 if path == ext or path.startswith(ext + os.sep):
552 return True, True
552 return True, True
553 return bool(changes), False
553 return bool(changes), False
554
554
555 def dirty(self, ignoreupdate=False):
555 def dirty(self, ignoreupdate=False):
556 if not self._wcchanged()[0]:
556 if not self._wcchanged()[0]:
557 if self._state[1] in self._wcrevs() or ignoreupdate:
557 if self._state[1] in self._wcrevs() or ignoreupdate:
558 return False
558 return False
559 return True
559 return True
560
560
561 def commit(self, text, user, date):
561 def commit(self, text, user, date):
562 # user and date are out of our hands since svn is centralized
562 # user and date are out of our hands since svn is centralized
563 changed, extchanged = self._wcchanged()
563 changed, extchanged = self._wcchanged()
564 if not changed:
564 if not changed:
565 return self._wcrev()
565 return self._wcrev()
566 if extchanged:
566 if extchanged:
567 # Do not try to commit externals
567 # Do not try to commit externals
568 raise util.Abort(_('cannot commit svn externals'))
568 raise util.Abort(_('cannot commit svn externals'))
569 commitinfo = self._svncommand(['commit', '-m', text])
569 commitinfo = self._svncommand(['commit', '-m', text])
570 self._ui.status(commitinfo)
570 self._ui.status(commitinfo)
571 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
571 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
572 if not newrev:
572 if not newrev:
573 raise util.Abort(commitinfo.splitlines()[-1])
573 raise util.Abort(commitinfo.splitlines()[-1])
574 newrev = newrev.groups()[0]
574 newrev = newrev.groups()[0]
575 self._ui.status(self._svncommand(['update', '-r', newrev]))
575 self._ui.status(self._svncommand(['update', '-r', newrev]))
576 return newrev
576 return newrev
577
577
578 def remove(self):
578 def remove(self):
579 if self.dirty():
579 if self.dirty():
580 self._ui.warn(_('not removing repo %s because '
580 self._ui.warn(_('not removing repo %s because '
581 'it has changes.\n' % self._path))
581 'it has changes.\n' % self._path))
582 return
582 return
583 self._ui.note(_('removing subrepo %s\n') % self._path)
583 self._ui.note(_('removing subrepo %s\n') % self._path)
584
584
585 def onerror(function, path, excinfo):
585 def onerror(function, path, excinfo):
586 if function is not os.remove:
586 if function is not os.remove:
587 raise
587 raise
588 # read-only files cannot be unlinked under Windows
588 # read-only files cannot be unlinked under Windows
589 s = os.stat(path)
589 s = os.stat(path)
590 if (s.st_mode & stat.S_IWRITE) != 0:
590 if (s.st_mode & stat.S_IWRITE) != 0:
591 raise
591 raise
592 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
592 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
593 os.remove(path)
593 os.remove(path)
594
594
595 path = self._ctx._repo.wjoin(self._path)
595 path = self._ctx._repo.wjoin(self._path)
596 shutil.rmtree(path, onerror=onerror)
596 shutil.rmtree(path, onerror=onerror)
597 try:
597 try:
598 os.removedirs(os.path.dirname(path))
598 os.removedirs(os.path.dirname(path))
599 except OSError:
599 except OSError:
600 pass
600 pass
601
601
602 def get(self, state, overwrite=False):
602 def get(self, state, overwrite=False):
603 if overwrite:
603 if overwrite:
604 self._svncommand(['revert', '--recursive', self._path])
604 self._svncommand(['revert', '--recursive'])
605 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
605 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
606 if not re.search('Checked out revision [0-9]+.', status):
606 if not re.search('Checked out revision [0-9]+.', status):
607 raise util.Abort(status.splitlines()[-1])
607 raise util.Abort(status.splitlines()[-1])
608 self._ui.status(status)
608 self._ui.status(status)
609
609
610 def merge(self, state):
610 def merge(self, state):
611 old = int(self._state[1])
611 old = int(self._state[1])
612 new = int(state[1])
612 new = int(state[1])
613 if new > old:
613 if new > old:
614 self.get(state)
614 self.get(state)
615
615
616 def push(self, force):
616 def push(self, force):
617 # push is a no-op for SVN
617 # push is a no-op for SVN
618 return True
618 return True
619
619
620 def files(self):
620 def files(self):
621 output = self._svncommand(['list'])
621 output = self._svncommand(['list'])
622 # This works because svn forbids \n in filenames.
622 # This works because svn forbids \n in filenames.
623 return output.splitlines()
623 return output.splitlines()
624
624
625 def filedata(self, name):
625 def filedata(self, name):
626 return self._svncommand(['cat'], name)
626 return self._svncommand(['cat'], name)
627
627
628
628
629 class gitsubrepo(abstractsubrepo):
629 class gitsubrepo(abstractsubrepo):
630 def __init__(self, ctx, path, state):
630 def __init__(self, ctx, path, state):
631 # TODO add git version check.
631 # TODO add git version check.
632 self._state = state
632 self._state = state
633 self._ctx = ctx
633 self._ctx = ctx
634 self._path = path
634 self._path = path
635 self._relpath = os.path.join(reporelpath(ctx._repo), path)
635 self._relpath = os.path.join(reporelpath(ctx._repo), path)
636 self._abspath = ctx._repo.wjoin(path)
636 self._abspath = ctx._repo.wjoin(path)
637 self._ui = ctx._repo.ui
637 self._ui = ctx._repo.ui
638
638
639 def _gitcommand(self, commands, env=None, stream=False):
639 def _gitcommand(self, commands, env=None, stream=False):
640 return self._gitdir(commands, env=env, stream=stream)[0]
640 return self._gitdir(commands, env=env, stream=stream)[0]
641
641
642 def _gitdir(self, commands, env=None, stream=False):
642 def _gitdir(self, commands, env=None, stream=False):
643 return self._gitnodir(commands, env=env, stream=stream,
643 return self._gitnodir(commands, env=env, stream=stream,
644 cwd=self._abspath)
644 cwd=self._abspath)
645
645
646 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
646 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
647 """Calls the git command
647 """Calls the git command
648
648
649 The methods tries to call the git command. versions previor to 1.6.0
649 The methods tries to call the git command. versions previor to 1.6.0
650 are not supported and very probably fail.
650 are not supported and very probably fail.
651 """
651 """
652 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
652 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
653 # unless ui.quiet is set, print git's stderr,
653 # unless ui.quiet is set, print git's stderr,
654 # which is mostly progress and useful info
654 # which is mostly progress and useful info
655 errpipe = None
655 errpipe = None
656 if self._ui.quiet:
656 if self._ui.quiet:
657 errpipe = open(os.devnull, 'w')
657 errpipe = open(os.devnull, 'w')
658 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
658 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
659 close_fds=util.closefds,
659 close_fds=util.closefds,
660 stdout=subprocess.PIPE, stderr=errpipe)
660 stdout=subprocess.PIPE, stderr=errpipe)
661 if stream:
661 if stream:
662 return p.stdout, None
662 return p.stdout, None
663
663
664 retdata = p.stdout.read().strip()
664 retdata = p.stdout.read().strip()
665 # wait for the child to exit to avoid race condition.
665 # wait for the child to exit to avoid race condition.
666 p.wait()
666 p.wait()
667
667
668 if p.returncode != 0 and p.returncode != 1:
668 if p.returncode != 0 and p.returncode != 1:
669 # there are certain error codes that are ok
669 # there are certain error codes that are ok
670 command = commands[0]
670 command = commands[0]
671 if command in ('cat-file', 'symbolic-ref'):
671 if command in ('cat-file', 'symbolic-ref'):
672 return retdata, p.returncode
672 return retdata, p.returncode
673 # for all others, abort
673 # for all others, abort
674 raise util.Abort('git %s error %d in %s' %
674 raise util.Abort('git %s error %d in %s' %
675 (command, p.returncode, self._relpath))
675 (command, p.returncode, self._relpath))
676
676
677 return retdata, p.returncode
677 return retdata, p.returncode
678
678
679 def _gitstate(self):
679 def _gitstate(self):
680 return self._gitcommand(['rev-parse', 'HEAD'])
680 return self._gitcommand(['rev-parse', 'HEAD'])
681
681
682 def _gitcurrentbranch(self):
682 def _gitcurrentbranch(self):
683 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
683 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
684 if err:
684 if err:
685 current = None
685 current = None
686 return current
686 return current
687
687
688 def _githavelocally(self, revision):
688 def _githavelocally(self, revision):
689 out, code = self._gitdir(['cat-file', '-e', revision])
689 out, code = self._gitdir(['cat-file', '-e', revision])
690 return code == 0
690 return code == 0
691
691
692 def _gitisancestor(self, r1, r2):
692 def _gitisancestor(self, r1, r2):
693 base = self._gitcommand(['merge-base', r1, r2])
693 base = self._gitcommand(['merge-base', r1, r2])
694 return base == r1
694 return base == r1
695
695
696 def _gitbranchmap(self):
696 def _gitbranchmap(self):
697 '''returns 2 things:
697 '''returns 2 things:
698 a map from git branch to revision
698 a map from git branch to revision
699 a map from revision to branches'''
699 a map from revision to branches'''
700 branch2rev = {}
700 branch2rev = {}
701 rev2branch = {}
701 rev2branch = {}
702
702
703 out = self._gitcommand(['for-each-ref', '--format',
703 out = self._gitcommand(['for-each-ref', '--format',
704 '%(objectname) %(refname)'])
704 '%(objectname) %(refname)'])
705 for line in out.split('\n'):
705 for line in out.split('\n'):
706 revision, ref = line.split(' ')
706 revision, ref = line.split(' ')
707 if ref.startswith('refs/tags/'):
707 if ref.startswith('refs/tags/'):
708 continue
708 continue
709 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
709 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
710 continue # ignore remote/HEAD redirects
710 continue # ignore remote/HEAD redirects
711 branch2rev[ref] = revision
711 branch2rev[ref] = revision
712 rev2branch.setdefault(revision, []).append(ref)
712 rev2branch.setdefault(revision, []).append(ref)
713 return branch2rev, rev2branch
713 return branch2rev, rev2branch
714
714
715 def _gittracking(self, branches):
715 def _gittracking(self, branches):
716 'return map of remote branch to local tracking branch'
716 'return map of remote branch to local tracking branch'
717 # assumes no more than one local tracking branch for each remote
717 # assumes no more than one local tracking branch for each remote
718 tracking = {}
718 tracking = {}
719 for b in branches:
719 for b in branches:
720 if b.startswith('refs/remotes/'):
720 if b.startswith('refs/remotes/'):
721 continue
721 continue
722 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
722 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
723 if remote:
723 if remote:
724 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
724 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
725 tracking['refs/remotes/%s/%s' %
725 tracking['refs/remotes/%s/%s' %
726 (remote, ref.split('/', 2)[2])] = b
726 (remote, ref.split('/', 2)[2])] = b
727 return tracking
727 return tracking
728
728
729 def _fetch(self, source, revision):
729 def _fetch(self, source, revision):
730 if not os.path.exists(os.path.join(self._abspath, '.git')):
730 if not os.path.exists(os.path.join(self._abspath, '.git')):
731 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
731 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
732 self._gitnodir(['clone', source, self._abspath])
732 self._gitnodir(['clone', source, self._abspath])
733 if self._githavelocally(revision):
733 if self._githavelocally(revision):
734 return
734 return
735 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
735 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
736 # first try from origin
736 # first try from origin
737 self._gitcommand(['fetch'])
737 self._gitcommand(['fetch'])
738 if self._githavelocally(revision):
738 if self._githavelocally(revision):
739 return
739 return
740 # then try from known subrepo source
740 # then try from known subrepo source
741 self._gitcommand(['fetch', source])
741 self._gitcommand(['fetch', source])
742 if not self._githavelocally(revision):
742 if not self._githavelocally(revision):
743 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
743 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
744 (revision, self._relpath))
744 (revision, self._relpath))
745
745
746 def dirty(self, ignoreupdate=False):
746 def dirty(self, ignoreupdate=False):
747 if not ignoreupdate and self._state[1] != self._gitstate():
747 if not ignoreupdate and self._state[1] != self._gitstate():
748 # different version checked out
748 # different version checked out
749 return True
749 return True
750 # check for staged changes or modified files; ignore untracked files
750 # check for staged changes or modified files; ignore untracked files
751 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
751 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
752 return code == 1
752 return code == 1
753
753
754 def get(self, state, overwrite=False):
754 def get(self, state, overwrite=False):
755 source, revision, kind = state
755 source, revision, kind = state
756 self._fetch(source, revision)
756 self._fetch(source, revision)
757 # if the repo was set to be bare, unbare it
757 # if the repo was set to be bare, unbare it
758 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
758 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
759 self._gitcommand(['config', 'core.bare', 'false'])
759 self._gitcommand(['config', 'core.bare', 'false'])
760 if self._gitstate() == revision:
760 if self._gitstate() == revision:
761 self._gitcommand(['reset', '--hard', 'HEAD'])
761 self._gitcommand(['reset', '--hard', 'HEAD'])
762 return
762 return
763 elif self._gitstate() == revision:
763 elif self._gitstate() == revision:
764 if overwrite:
764 if overwrite:
765 # first reset the index to unmark new files for commit, because
765 # first reset the index to unmark new files for commit, because
766 # reset --hard will otherwise throw away files added for commit,
766 # reset --hard will otherwise throw away files added for commit,
767 # not just unmark them.
767 # not just unmark them.
768 self._gitcommand(['reset', 'HEAD'])
768 self._gitcommand(['reset', 'HEAD'])
769 self._gitcommand(['reset', '--hard', 'HEAD'])
769 self._gitcommand(['reset', '--hard', 'HEAD'])
770 return
770 return
771 branch2rev, rev2branch = self._gitbranchmap()
771 branch2rev, rev2branch = self._gitbranchmap()
772
772
773 def checkout(args):
773 def checkout(args):
774 cmd = ['checkout']
774 cmd = ['checkout']
775 if overwrite:
775 if overwrite:
776 # first reset the index to unmark new files for commit, because
776 # first reset the index to unmark new files for commit, because
777 # the -f option will otherwise throw away files added for
777 # the -f option will otherwise throw away files added for
778 # commit, not just unmark them.
778 # commit, not just unmark them.
779 self._gitcommand(['reset', 'HEAD'])
779 self._gitcommand(['reset', 'HEAD'])
780 cmd.append('-f')
780 cmd.append('-f')
781 self._gitcommand(cmd + args)
781 self._gitcommand(cmd + args)
782
782
783 def rawcheckout():
783 def rawcheckout():
784 # no branch to checkout, check it out with no branch
784 # no branch to checkout, check it out with no branch
785 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
785 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
786 self._relpath)
786 self._relpath)
787 self._ui.warn(_('check out a git branch if you intend '
787 self._ui.warn(_('check out a git branch if you intend '
788 'to make changes\n'))
788 'to make changes\n'))
789 checkout(['-q', revision])
789 checkout(['-q', revision])
790
790
791 if revision not in rev2branch:
791 if revision not in rev2branch:
792 rawcheckout()
792 rawcheckout()
793 return
793 return
794 branches = rev2branch[revision]
794 branches = rev2branch[revision]
795 firstlocalbranch = None
795 firstlocalbranch = None
796 for b in branches:
796 for b in branches:
797 if b == 'refs/heads/master':
797 if b == 'refs/heads/master':
798 # master trumps all other branches
798 # master trumps all other branches
799 checkout(['refs/heads/master'])
799 checkout(['refs/heads/master'])
800 return
800 return
801 if not firstlocalbranch and not b.startswith('refs/remotes/'):
801 if not firstlocalbranch and not b.startswith('refs/remotes/'):
802 firstlocalbranch = b
802 firstlocalbranch = b
803 if firstlocalbranch:
803 if firstlocalbranch:
804 checkout([firstlocalbranch])
804 checkout([firstlocalbranch])
805 return
805 return
806
806
807 tracking = self._gittracking(branch2rev.keys())
807 tracking = self._gittracking(branch2rev.keys())
808 # choose a remote branch already tracked if possible
808 # choose a remote branch already tracked if possible
809 remote = branches[0]
809 remote = branches[0]
810 if remote not in tracking:
810 if remote not in tracking:
811 for b in branches:
811 for b in branches:
812 if b in tracking:
812 if b in tracking:
813 remote = b
813 remote = b
814 break
814 break
815
815
816 if remote not in tracking:
816 if remote not in tracking:
817 # create a new local tracking branch
817 # create a new local tracking branch
818 local = remote.split('/', 2)[2]
818 local = remote.split('/', 2)[2]
819 checkout(['-b', local, remote])
819 checkout(['-b', local, remote])
820 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
820 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
821 # When updating to a tracked remote branch,
821 # When updating to a tracked remote branch,
822 # if the local tracking branch is downstream of it,
822 # if the local tracking branch is downstream of it,
823 # a normal `git pull` would have performed a "fast-forward merge"
823 # a normal `git pull` would have performed a "fast-forward merge"
824 # which is equivalent to updating the local branch to the remote.
824 # which is equivalent to updating the local branch to the remote.
825 # Since we are only looking at branching at update, we need to
825 # Since we are only looking at branching at update, we need to
826 # detect this situation and perform this action lazily.
826 # detect this situation and perform this action lazily.
827 if tracking[remote] != self._gitcurrentbranch():
827 if tracking[remote] != self._gitcurrentbranch():
828 checkout([tracking[remote]])
828 checkout([tracking[remote]])
829 self._gitcommand(['merge', '--ff', remote])
829 self._gitcommand(['merge', '--ff', remote])
830 else:
830 else:
831 # a real merge would be required, just checkout the revision
831 # a real merge would be required, just checkout the revision
832 rawcheckout()
832 rawcheckout()
833
833
834 def commit(self, text, user, date):
834 def commit(self, text, user, date):
835 cmd = ['commit', '-a', '-m', text]
835 cmd = ['commit', '-a', '-m', text]
836 env = os.environ.copy()
836 env = os.environ.copy()
837 if user:
837 if user:
838 cmd += ['--author', user]
838 cmd += ['--author', user]
839 if date:
839 if date:
840 # git's date parser silently ignores when seconds < 1e9
840 # git's date parser silently ignores when seconds < 1e9
841 # convert to ISO8601
841 # convert to ISO8601
842 env['GIT_AUTHOR_DATE'] = util.datestr(date,
842 env['GIT_AUTHOR_DATE'] = util.datestr(date,
843 '%Y-%m-%dT%H:%M:%S %1%2')
843 '%Y-%m-%dT%H:%M:%S %1%2')
844 self._gitcommand(cmd, env=env)
844 self._gitcommand(cmd, env=env)
845 # make sure commit works otherwise HEAD might not exist under certain
845 # make sure commit works otherwise HEAD might not exist under certain
846 # circumstances
846 # circumstances
847 return self._gitstate()
847 return self._gitstate()
848
848
849 def merge(self, state):
849 def merge(self, state):
850 source, revision, kind = state
850 source, revision, kind = state
851 self._fetch(source, revision)
851 self._fetch(source, revision)
852 base = self._gitcommand(['merge-base', revision, self._state[1]])
852 base = self._gitcommand(['merge-base', revision, self._state[1]])
853 if base == revision:
853 if base == revision:
854 self.get(state) # fast forward merge
854 self.get(state) # fast forward merge
855 elif base != self._state[1]:
855 elif base != self._state[1]:
856 self._gitcommand(['merge', '--no-commit', revision])
856 self._gitcommand(['merge', '--no-commit', revision])
857
857
858 def push(self, force):
858 def push(self, force):
859 # if a branch in origin contains the revision, nothing to do
859 # if a branch in origin contains the revision, nothing to do
860 branch2rev, rev2branch = self._gitbranchmap()
860 branch2rev, rev2branch = self._gitbranchmap()
861 if self._state[1] in rev2branch:
861 if self._state[1] in rev2branch:
862 for b in rev2branch[self._state[1]]:
862 for b in rev2branch[self._state[1]]:
863 if b.startswith('refs/remotes/origin/'):
863 if b.startswith('refs/remotes/origin/'):
864 return True
864 return True
865 for b, revision in branch2rev.iteritems():
865 for b, revision in branch2rev.iteritems():
866 if b.startswith('refs/remotes/origin/'):
866 if b.startswith('refs/remotes/origin/'):
867 if self._gitisancestor(self._state[1], revision):
867 if self._gitisancestor(self._state[1], revision):
868 return True
868 return True
869 # otherwise, try to push the currently checked out branch
869 # otherwise, try to push the currently checked out branch
870 cmd = ['push']
870 cmd = ['push']
871 if force:
871 if force:
872 cmd.append('--force')
872 cmd.append('--force')
873
873
874 current = self._gitcurrentbranch()
874 current = self._gitcurrentbranch()
875 if current:
875 if current:
876 # determine if the current branch is even useful
876 # determine if the current branch is even useful
877 if not self._gitisancestor(self._state[1], current):
877 if not self._gitisancestor(self._state[1], current):
878 self._ui.warn(_('unrelated git branch checked out '
878 self._ui.warn(_('unrelated git branch checked out '
879 'in subrepo %s\n') % self._relpath)
879 'in subrepo %s\n') % self._relpath)
880 return False
880 return False
881 self._ui.status(_('pushing branch %s of subrepo %s\n') %
881 self._ui.status(_('pushing branch %s of subrepo %s\n') %
882 (current.split('/', 2)[2], self._relpath))
882 (current.split('/', 2)[2], self._relpath))
883 self._gitcommand(cmd + ['origin', current])
883 self._gitcommand(cmd + ['origin', current])
884 return True
884 return True
885 else:
885 else:
886 self._ui.warn(_('no branch checked out in subrepo %s\n'
886 self._ui.warn(_('no branch checked out in subrepo %s\n'
887 'cannot push revision %s') %
887 'cannot push revision %s') %
888 (self._relpath, self._state[1]))
888 (self._relpath, self._state[1]))
889 return False
889 return False
890
890
891 def remove(self):
891 def remove(self):
892 if self.dirty():
892 if self.dirty():
893 self._ui.warn(_('not removing repo %s because '
893 self._ui.warn(_('not removing repo %s because '
894 'it has changes.\n') % self._relpath)
894 'it has changes.\n') % self._relpath)
895 return
895 return
896 # we can't fully delete the repository as it may contain
896 # we can't fully delete the repository as it may contain
897 # local-only history
897 # local-only history
898 self._ui.note(_('removing subrepo %s\n') % self._relpath)
898 self._ui.note(_('removing subrepo %s\n') % self._relpath)
899 self._gitcommand(['config', 'core.bare', 'true'])
899 self._gitcommand(['config', 'core.bare', 'true'])
900 for f in os.listdir(self._abspath):
900 for f in os.listdir(self._abspath):
901 if f == '.git':
901 if f == '.git':
902 continue
902 continue
903 path = os.path.join(self._abspath, f)
903 path = os.path.join(self._abspath, f)
904 if os.path.isdir(path) and not os.path.islink(path):
904 if os.path.isdir(path) and not os.path.islink(path):
905 shutil.rmtree(path)
905 shutil.rmtree(path)
906 else:
906 else:
907 os.remove(path)
907 os.remove(path)
908
908
909 def archive(self, ui, archiver, prefix):
909 def archive(self, ui, archiver, prefix):
910 source, revision = self._state
910 source, revision = self._state
911 self._fetch(source, revision)
911 self._fetch(source, revision)
912
912
913 # Parse git's native archive command.
913 # Parse git's native archive command.
914 # This should be much faster than manually traversing the trees
914 # This should be much faster than manually traversing the trees
915 # and objects with many subprocess calls.
915 # and objects with many subprocess calls.
916 tarstream = self._gitcommand(['archive', revision], stream=True)
916 tarstream = self._gitcommand(['archive', revision], stream=True)
917 tar = tarfile.open(fileobj=tarstream, mode='r|')
917 tar = tarfile.open(fileobj=tarstream, mode='r|')
918 relpath = subrelpath(self)
918 relpath = subrelpath(self)
919 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
919 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
920 for i, info in enumerate(tar):
920 for i, info in enumerate(tar):
921 if info.isdir():
921 if info.isdir():
922 continue
922 continue
923 if info.issym():
923 if info.issym():
924 data = info.linkname
924 data = info.linkname
925 else:
925 else:
926 data = tar.extractfile(info).read()
926 data = tar.extractfile(info).read()
927 archiver.addfile(os.path.join(prefix, self._path, info.name),
927 archiver.addfile(os.path.join(prefix, self._path, info.name),
928 info.mode, info.issym(), data)
928 info.mode, info.issym(), data)
929 ui.progress(_('archiving (%s)') % relpath, i + 1,
929 ui.progress(_('archiving (%s)') % relpath, i + 1,
930 unit=_('files'))
930 unit=_('files'))
931 ui.progress(_('archiving (%s)') % relpath, None)
931 ui.progress(_('archiving (%s)') % relpath, None)
932
932
933
933
934 def status(self, rev2, **opts):
934 def status(self, rev2, **opts):
935 rev1 = self._state[1]
935 rev1 = self._state[1]
936 modified, added, removed = [], [], []
936 modified, added, removed = [], [], []
937 if rev2:
937 if rev2:
938 command = ['diff-tree', rev1, rev2]
938 command = ['diff-tree', rev1, rev2]
939 else:
939 else:
940 command = ['diff-index', rev1]
940 command = ['diff-index', rev1]
941 out = self._gitcommand(command)
941 out = self._gitcommand(command)
942 for line in out.split('\n'):
942 for line in out.split('\n'):
943 tab = line.find('\t')
943 tab = line.find('\t')
944 if tab == -1:
944 if tab == -1:
945 continue
945 continue
946 status, f = line[tab - 1], line[tab + 1:]
946 status, f = line[tab - 1], line[tab + 1:]
947 if status == 'M':
947 if status == 'M':
948 modified.append(f)
948 modified.append(f)
949 elif status == 'A':
949 elif status == 'A':
950 added.append(f)
950 added.append(f)
951 elif status == 'D':
951 elif status == 'D':
952 removed.append(f)
952 removed.append(f)
953
953
954 deleted = unknown = ignored = clean = []
954 deleted = unknown = ignored = clean = []
955 return modified, added, removed, deleted, unknown, ignored, clean
955 return modified, added, removed, deleted, unknown, ignored, clean
956
956
957 types = {
957 types = {
958 'hg': hgsubrepo,
958 'hg': hgsubrepo,
959 'svn': svnsubrepo,
959 'svn': svnsubrepo,
960 'git': gitsubrepo,
960 'git': gitsubrepo,
961 }
961 }
@@ -1,298 +1,298 b''
1 $ "$TESTDIR/hghave" svn || exit 80
1 $ "$TESTDIR/hghave" svn || exit 80
2
2
3 $ fix_path()
3 $ fix_path()
4 > {
4 > {
5 > tr '\\' /
5 > tr '\\' /
6 > }
6 > }
7
7
8 SVN wants all paths to start with a slash. Unfortunately, Windows ones
8 SVN wants all paths to start with a slash. Unfortunately, Windows ones
9 don't. Handle that.
9 don't. Handle that.
10
10
11 $ escapedwd=`pwd | fix_path`
11 $ escapedwd=`pwd | fix_path`
12 $ expr "$escapedwd" : '\/' > /dev/null || escapedwd="/$escapedwd"
12 $ expr "$escapedwd" : '\/' > /dev/null || escapedwd="/$escapedwd"
13 $ escapedwd=`python -c "import urllib, sys; sys.stdout.write(urllib.quote(sys.argv[1]))" "$escapedwd"`
13 $ escapedwd=`python -c "import urllib, sys; sys.stdout.write(urllib.quote(sys.argv[1]))" "$escapedwd"`
14
14
15 create subversion repo
15 create subversion repo
16
16
17 $ SVNREPO="file://$escapedwd/svn-repo"
17 $ SVNREPO="file://$escapedwd/svn-repo"
18 $ WCROOT="`pwd`/svn-wc"
18 $ WCROOT="`pwd`/svn-wc"
19 $ svnadmin create svn-repo
19 $ svnadmin create svn-repo
20 $ svn co "$SVNREPO" svn-wc
20 $ svn co "$SVNREPO" svn-wc
21 Checked out revision 0.
21 Checked out revision 0.
22 $ cd svn-wc
22 $ cd svn-wc
23 $ mkdir src
23 $ mkdir src
24 $ echo alpha > src/alpha
24 $ echo alpha > src/alpha
25 $ svn add src
25 $ svn add src
26 A src
26 A src
27 A src/alpha
27 A src/alpha
28 $ mkdir externals
28 $ mkdir externals
29 $ echo other > externals/other
29 $ echo other > externals/other
30 $ svn add externals
30 $ svn add externals
31 A externals
31 A externals
32 A externals/other
32 A externals/other
33 $ svn ci -m 'Add alpha'
33 $ svn ci -m 'Add alpha'
34 Adding externals
34 Adding externals
35 Adding externals/other
35 Adding externals/other
36 Adding src
36 Adding src
37 Adding src/alpha
37 Adding src/alpha
38 Transmitting file data ..
38 Transmitting file data ..
39 Committed revision 1.
39 Committed revision 1.
40 $ svn up
40 $ svn up
41 At revision 1.
41 At revision 1.
42 $ echo "externals -r1 $SVNREPO/externals" > extdef
42 $ echo "externals -r1 $SVNREPO/externals" > extdef
43 $ svn propset -F extdef svn:externals src
43 $ svn propset -F extdef svn:externals src
44 property 'svn:externals' set on 'src'
44 property 'svn:externals' set on 'src'
45 $ svn ci -m 'Setting externals'
45 $ svn ci -m 'Setting externals'
46 Sending src
46 Sending src
47
47
48 Committed revision 2.
48 Committed revision 2.
49 $ cd ..
49 $ cd ..
50
50
51 create hg repo
51 create hg repo
52
52
53 $ mkdir sub
53 $ mkdir sub
54 $ cd sub
54 $ cd sub
55 $ hg init t
55 $ hg init t
56 $ cd t
56 $ cd t
57
57
58 first revision, no sub
58 first revision, no sub
59
59
60 $ echo a > a
60 $ echo a > a
61 $ hg ci -Am0
61 $ hg ci -Am0
62 adding a
62 adding a
63
63
64 add first svn sub with leading whitespaces
64 add first svn sub with leading whitespaces
65
65
66 $ echo "s = [svn] $SVNREPO/src" >> .hgsub
66 $ echo "s = [svn] $SVNREPO/src" >> .hgsub
67 $ echo "subdir/s = [svn] $SVNREPO/src" >> .hgsub
67 $ echo "subdir/s = [svn] $SVNREPO/src" >> .hgsub
68 $ svn co --quiet "$SVNREPO"/src s
68 $ svn co --quiet "$SVNREPO"/src s
69 $ mkdir subdir
69 $ mkdir subdir
70 $ svn co --quiet "$SVNREPO"/src subdir/s
70 $ svn co --quiet "$SVNREPO"/src subdir/s
71 $ hg add .hgsub
71 $ hg add .hgsub
72 $ hg ci -m1
72 $ hg ci -m1
73 committing subrepository s
73 committing subrepository s
74 committing subrepository subdir/s
74 committing subrepository subdir/s
75
75
76 make sure we avoid empty commits (issue2445)
76 make sure we avoid empty commits (issue2445)
77
77
78 $ hg sum
78 $ hg sum
79 parent: 1:* tip (glob)
79 parent: 1:* tip (glob)
80 1
80 1
81 branch: default
81 branch: default
82 commit: (clean)
82 commit: (clean)
83 update: (current)
83 update: (current)
84 $ hg ci -moops
84 $ hg ci -moops
85 nothing changed
85 nothing changed
86 [1]
86 [1]
87
87
88 debugsub
88 debugsub
89
89
90 $ hg debugsub
90 $ hg debugsub
91 path s
91 path s
92 source file://*/svn-repo/src (glob)
92 source file://*/svn-repo/src (glob)
93 revision 2
93 revision 2
94 path subdir/s
94 path subdir/s
95 source file://*/svn-repo/src (glob)
95 source file://*/svn-repo/src (glob)
96 revision 2
96 revision 2
97
97
98 change file in svn and hg, commit
98 change file in svn and hg, commit
99
99
100 $ echo a >> a
100 $ echo a >> a
101 $ echo alpha >> s/alpha
101 $ echo alpha >> s/alpha
102 $ hg sum
102 $ hg sum
103 parent: 1:* tip (glob)
103 parent: 1:* tip (glob)
104 1
104 1
105 branch: default
105 branch: default
106 commit: 1 modified, 1 subrepos
106 commit: 1 modified, 1 subrepos
107 update: (current)
107 update: (current)
108 $ hg commit -m 'Message!'
108 $ hg commit -m 'Message!'
109 committing subrepository s
109 committing subrepository s
110 Sending*s/alpha (glob)
110 Sending*s/alpha (glob)
111 Transmitting file data .
111 Transmitting file data .
112 Committed revision 3.
112 Committed revision 3.
113
113
114 Fetching external item into '$TESTTMP/sub/t/s/externals'
114 Fetching external item into '$TESTTMP/sub/t/s/externals'
115 External at revision 1.
115 External at revision 1.
116
116
117 At revision 3.
117 At revision 3.
118 $ hg debugsub
118 $ hg debugsub
119 path s
119 path s
120 source file://*/svn-repo/src (glob)
120 source file://*/svn-repo/src (glob)
121 revision 3
121 revision 3
122 path subdir/s
122 path subdir/s
123 source file://*/svn-repo/src (glob)
123 source file://*/svn-repo/src (glob)
124 revision 2
124 revision 2
125
125
126 add an unrelated revision in svn and update the subrepo to without
126 add an unrelated revision in svn and update the subrepo to without
127 bringing any changes.
127 bringing any changes.
128
128
129 $ svn mkdir --parents "$SVNREPO/unrelated" -m 'create unrelated'
129 $ svn mkdir --parents "$SVNREPO/unrelated" -m 'create unrelated'
130
130
131 Committed revision 4.
131 Committed revision 4.
132 $ svn up s
132 $ svn up s
133
133
134 Fetching external item into 's/externals'
134 Fetching external item into 's/externals'
135 External at revision 1.
135 External at revision 1.
136
136
137 At revision 4.
137 At revision 4.
138 $ hg sum
138 $ hg sum
139 parent: 2:* tip (glob)
139 parent: 2:* tip (glob)
140 Message!
140 Message!
141 branch: default
141 branch: default
142 commit: (clean)
142 commit: (clean)
143 update: (current)
143 update: (current)
144
144
145 $ echo a > s/a
145 $ echo a > s/a
146
146
147 should be empty despite change to s/a
147 should be empty despite change to s/a
148
148
149 $ hg st
149 $ hg st
150
150
151 add a commit from svn
151 add a commit from svn
152
152
153 $ cd "$WCROOT"/src
153 $ cd "$WCROOT"/src
154 $ svn up
154 $ svn up
155 U alpha
155 U alpha
156
156
157 Fetching external item into 'externals'
157 Fetching external item into 'externals'
158 A externals/other
158 A externals/other
159 Updated external to revision 1.
159 Updated external to revision 1.
160
160
161 Updated to revision 4.
161 Updated to revision 4.
162 $ echo xyz >> alpha
162 $ echo xyz >> alpha
163 $ svn propset svn:mime-type 'text/xml' alpha
163 $ svn propset svn:mime-type 'text/xml' alpha
164 property 'svn:mime-type' set on 'alpha'
164 property 'svn:mime-type' set on 'alpha'
165 $ svn ci -m 'amend a from svn'
165 $ svn ci -m 'amend a from svn'
166 Sending src/alpha
166 Sending src/alpha
167 Transmitting file data .
167 Transmitting file data .
168 Committed revision 5.
168 Committed revision 5.
169 $ cd ../../sub/t
169 $ cd ../../sub/t
170
170
171 this commit from hg will fail
171 this commit from hg will fail
172
172
173 $ echo zzz >> s/alpha
173 $ echo zzz >> s/alpha
174 $ hg ci -m 'amend alpha from hg'
174 $ hg ci -m 'amend alpha from hg'
175 committing subrepository s
175 committing subrepository s
176 abort: svn: Commit failed (details follow):
176 abort: svn: Commit failed (details follow):
177 svn: (Out of date)?.*/src/alpha.*(is out of date)? (re)
177 svn: (Out of date)?.*/src/alpha.*(is out of date)? (re)
178 [255]
178 [255]
179 $ svn revert -q s/alpha
179 $ svn revert -q s/alpha
180
180
181 this commit fails because of meta changes
181 this commit fails because of meta changes
182
182
183 $ svn propset svn:mime-type 'text/html' s/alpha
183 $ svn propset svn:mime-type 'text/html' s/alpha
184 property 'svn:mime-type' set on 's/alpha'
184 property 'svn:mime-type' set on 's/alpha'
185 $ hg ci -m 'amend alpha from hg'
185 $ hg ci -m 'amend alpha from hg'
186 committing subrepository s
186 committing subrepository s
187 abort: svn: Commit failed (details follow):
187 abort: svn: Commit failed (details follow):
188 svn: (Out of date)?.*/src/alpha.*(is out of date)? (re)
188 svn: (Out of date)?.*/src/alpha.*(is out of date)? (re)
189 [255]
189 [255]
190 $ svn revert -q s/alpha
190 $ svn revert -q s/alpha
191
191
192 this commit fails because of externals changes
192 this commit fails because of externals changes
193
193
194 $ echo zzz > s/externals/other
194 $ echo zzz > s/externals/other
195 $ hg ci -m 'amend externals from hg'
195 $ hg ci -m 'amend externals from hg'
196 committing subrepository s
196 committing subrepository s
197 abort: cannot commit svn externals
197 abort: cannot commit svn externals
198 [255]
198 [255]
199 $ hg diff --subrepos -r 1:2 | grep -v diff
199 $ hg diff --subrepos -r 1:2 | grep -v diff
200 --- a/.hgsubstate Thu Jan 01 00:00:00 1970 +0000
200 --- a/.hgsubstate Thu Jan 01 00:00:00 1970 +0000
201 +++ b/.hgsubstate Thu Jan 01 00:00:00 1970 +0000
201 +++ b/.hgsubstate Thu Jan 01 00:00:00 1970 +0000
202 @@ -1,2 +1,2 @@
202 @@ -1,2 +1,2 @@
203 -2 s
203 -2 s
204 +3 s
204 +3 s
205 2 subdir/s
205 2 subdir/s
206 --- a/a Thu Jan 01 00:00:00 1970 +0000
206 --- a/a Thu Jan 01 00:00:00 1970 +0000
207 +++ b/a Thu Jan 01 00:00:00 1970 +0000
207 +++ b/a Thu Jan 01 00:00:00 1970 +0000
208 @@ -1,1 +1,2 @@
208 @@ -1,1 +1,2 @@
209 a
209 a
210 +a
210 +a
211 $ svn revert -q s/externals/other
211 $ svn revert -q s/externals/other
212
212
213 this commit fails because of externals meta changes
213 this commit fails because of externals meta changes
214
214
215 $ svn propset svn:mime-type 'text/html' s/externals/other
215 $ svn propset svn:mime-type 'text/html' s/externals/other
216 property 'svn:mime-type' set on 's/externals/other'
216 property 'svn:mime-type' set on 's/externals/other'
217 $ hg ci -m 'amend externals from hg'
217 $ hg ci -m 'amend externals from hg'
218 committing subrepository s
218 committing subrepository s
219 abort: cannot commit svn externals
219 abort: cannot commit svn externals
220 [255]
220 [255]
221 $ svn revert -q s/externals/other
221 $ svn revert -q s/externals/other
222
222
223 clone
223 clone
224
224
225 $ cd ..
225 $ cd ..
226 $ hg clone t tc | fix_path
226 $ hg clone t tc | fix_path
227 updating to branch default
227 updating to branch default
228 A tc/subdir/s/alpha
228 A tc/subdir/s/alpha
229 U tc/subdir/s
229 U tc/subdir/s
230
230
231 Fetching external item into 'tc/subdir/s/externals'
231 Fetching external item into 'tc/subdir/s/externals'
232 A tc/subdir/s/externals/other
232 A tc/subdir/s/externals/other
233 Checked out external at revision 1.
233 Checked out external at revision 1.
234
234
235 Checked out revision 2.
235 Checked out revision 2.
236 A tc/s/alpha
236 A tc/s/alpha
237 U tc/s
237 U tc/s
238
238
239 Fetching external item into 'tc/s/externals'
239 Fetching external item into 'tc/s/externals'
240 A tc/s/externals/other
240 A tc/s/externals/other
241 Checked out external at revision 1.
241 Checked out external at revision 1.
242
242
243 Checked out revision 3.
243 Checked out revision 3.
244 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
244 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
245 $ cd tc
245 $ cd tc
246
246
247 debugsub in clone
247 debugsub in clone
248
248
249 $ hg debugsub
249 $ hg debugsub
250 path s
250 path s
251 source file://*/svn-repo/src (glob)
251 source file://*/svn-repo/src (glob)
252 revision 3
252 revision 3
253 path subdir/s
253 path subdir/s
254 source file://*/svn-repo/src (glob)
254 source file://*/svn-repo/src (glob)
255 revision 2
255 revision 2
256
256
257 verify subrepo is contained within the repo directory
257 verify subrepo is contained within the repo directory
258
258
259 $ python -c "import os.path; print os.path.exists('s')"
259 $ python -c "import os.path; print os.path.exists('s')"
260 True
260 True
261
261
262 update to nullrev (must delete the subrepo)
262 update to nullrev (must delete the subrepo)
263
263
264 $ hg up null
264 $ hg up null
265 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
265 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
266 $ ls
266 $ ls
267
267
268 Check hg update --clean
268 Check hg update --clean
269 $ cd $TESTTMP/sub/t
269 $ cd $TESTTMP/sub/t
270 $ cd s
270 $ cd s
271 $ echo c0 > alpha
271 $ echo c0 > alpha
272 $ echo c1 > f1
272 $ echo c1 > f1
273 $ echo c1 > f2
273 $ echo c1 > f2
274 $ svn add f1 -q
274 $ svn add f1 -q
275 $ svn status
275 $ svn status
276 ? a
276 ? a
277 X externals
277 X externals
278 ? f2
278 ? f2
279 M alpha
279 M alpha
280 A f1
280 A f1
281
281
282 Performing status on external item at 'externals'
282 Performing status on external item at 'externals'
283 $ cd ..
283 $ cd ../..
284 $ hg update -C
284 $ hg -R t update -C
285
285
286 Fetching external item into '$TESTTMP/sub/t/s/externals'
286 Fetching external item into 't/s/externals'
287 Checked out external at revision 1.
287 Checked out external at revision 1.
288
288
289 Checked out revision 3.
289 Checked out revision 3.
290 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
290 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
291 $ cd s
291 $ cd t/s
292 $ svn status
292 $ svn status
293 ? a
293 ? a
294 X externals
294 X externals
295 ? f1
295 ? f1
296 ? f2
296 ? f2
297
297
298 Performing status on external item at 'externals'
298 Performing status on external item at 'externals'
General Comments 0
You need to be logged in to leave comments. Login now