##// END OF EJS Templates
color: move git-subrepo support into the subrepo module...
Pierre-Yves David -
r31102:96d561c9 default
parent child Browse files
Show More
@@ -1,261 +1,253 b''
1 # color.py color output for Mercurial commands
1 # color.py color output for Mercurial commands
2 #
2 #
3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com>
3 # Copyright (C) 2007 Kevin Christen <kevin.christen@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 '''colorize output from some commands
8 '''colorize output from some commands
9
9
10 The color extension colorizes output from several Mercurial commands.
10 The color extension colorizes output from several Mercurial commands.
11 For example, the diff command shows additions in green and deletions
11 For example, the diff command shows additions in green and deletions
12 in red, while the status command shows modified files in magenta. Many
12 in red, while the status command shows modified files in magenta. Many
13 other commands have analogous colors. It is possible to customize
13 other commands have analogous colors. It is possible to customize
14 these colors.
14 these colors.
15
15
16 Effects
16 Effects
17 -------
17 -------
18
18
19 Other effects in addition to color, like bold and underlined text, are
19 Other effects in addition to color, like bold and underlined text, are
20 also available. By default, the terminfo database is used to find the
20 also available. By default, the terminfo database is used to find the
21 terminal codes used to change color and effect. If terminfo is not
21 terminal codes used to change color and effect. If terminfo is not
22 available, then effects are rendered with the ECMA-48 SGR control
22 available, then effects are rendered with the ECMA-48 SGR control
23 function (aka ANSI escape codes).
23 function (aka ANSI escape codes).
24
24
25 The available effects in terminfo mode are 'blink', 'bold', 'dim',
25 The available effects in terminfo mode are 'blink', 'bold', 'dim',
26 'inverse', 'invisible', 'italic', 'standout', and 'underline'; in
26 'inverse', 'invisible', 'italic', 'standout', and 'underline'; in
27 ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and
27 ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and
28 'underline'. How each is rendered depends on the terminal emulator.
28 'underline'. How each is rendered depends on the terminal emulator.
29 Some may not be available for a given terminal type, and will be
29 Some may not be available for a given terminal type, and will be
30 silently ignored.
30 silently ignored.
31
31
32 If the terminfo entry for your terminal is missing codes for an effect
32 If the terminfo entry for your terminal is missing codes for an effect
33 or has the wrong codes, you can add or override those codes in your
33 or has the wrong codes, you can add or override those codes in your
34 configuration::
34 configuration::
35
35
36 [color]
36 [color]
37 terminfo.dim = \E[2m
37 terminfo.dim = \E[2m
38
38
39 where '\E' is substituted with an escape character.
39 where '\E' is substituted with an escape character.
40
40
41 Labels
41 Labels
42 ------
42 ------
43
43
44 Text receives color effects depending on the labels that it has. Many
44 Text receives color effects depending on the labels that it has. Many
45 default Mercurial commands emit labelled text. You can also define
45 default Mercurial commands emit labelled text. You can also define
46 your own labels in templates using the label function, see :hg:`help
46 your own labels in templates using the label function, see :hg:`help
47 templates`. A single portion of text may have more than one label. In
47 templates`. A single portion of text may have more than one label. In
48 that case, effects given to the last label will override any other
48 that case, effects given to the last label will override any other
49 effects. This includes the special "none" effect, which nullifies
49 effects. This includes the special "none" effect, which nullifies
50 other effects.
50 other effects.
51
51
52 Labels are normally invisible. In order to see these labels and their
52 Labels are normally invisible. In order to see these labels and their
53 position in the text, use the global --color=debug option. The same
53 position in the text, use the global --color=debug option. The same
54 anchor text may be associated to multiple labels, e.g.
54 anchor text may be associated to multiple labels, e.g.
55
55
56 [log.changeset changeset.secret|changeset: 22611:6f0a53c8f587]
56 [log.changeset changeset.secret|changeset: 22611:6f0a53c8f587]
57
57
58 The following are the default effects for some default labels. Default
58 The following are the default effects for some default labels. Default
59 effects may be overridden from your configuration file::
59 effects may be overridden from your configuration file::
60
60
61 [color]
61 [color]
62 status.modified = blue bold underline red_background
62 status.modified = blue bold underline red_background
63 status.added = green bold
63 status.added = green bold
64 status.removed = red bold blue_background
64 status.removed = red bold blue_background
65 status.deleted = cyan bold underline
65 status.deleted = cyan bold underline
66 status.unknown = magenta bold underline
66 status.unknown = magenta bold underline
67 status.ignored = black bold
67 status.ignored = black bold
68
68
69 # 'none' turns off all effects
69 # 'none' turns off all effects
70 status.clean = none
70 status.clean = none
71 status.copied = none
71 status.copied = none
72
72
73 qseries.applied = blue bold underline
73 qseries.applied = blue bold underline
74 qseries.unapplied = black bold
74 qseries.unapplied = black bold
75 qseries.missing = red bold
75 qseries.missing = red bold
76
76
77 diff.diffline = bold
77 diff.diffline = bold
78 diff.extended = cyan bold
78 diff.extended = cyan bold
79 diff.file_a = red bold
79 diff.file_a = red bold
80 diff.file_b = green bold
80 diff.file_b = green bold
81 diff.hunk = magenta
81 diff.hunk = magenta
82 diff.deleted = red
82 diff.deleted = red
83 diff.inserted = green
83 diff.inserted = green
84 diff.changed = white
84 diff.changed = white
85 diff.tab =
85 diff.tab =
86 diff.trailingwhitespace = bold red_background
86 diff.trailingwhitespace = bold red_background
87
87
88 # Blank so it inherits the style of the surrounding label
88 # Blank so it inherits the style of the surrounding label
89 changeset.public =
89 changeset.public =
90 changeset.draft =
90 changeset.draft =
91 changeset.secret =
91 changeset.secret =
92
92
93 resolve.unresolved = red bold
93 resolve.unresolved = red bold
94 resolve.resolved = green bold
94 resolve.resolved = green bold
95
95
96 bookmarks.active = green
96 bookmarks.active = green
97
97
98 branches.active = none
98 branches.active = none
99 branches.closed = black bold
99 branches.closed = black bold
100 branches.current = green
100 branches.current = green
101 branches.inactive = none
101 branches.inactive = none
102
102
103 tags.normal = green
103 tags.normal = green
104 tags.local = black bold
104 tags.local = black bold
105
105
106 rebase.rebased = blue
106 rebase.rebased = blue
107 rebase.remaining = red bold
107 rebase.remaining = red bold
108
108
109 shelve.age = cyan
109 shelve.age = cyan
110 shelve.newest = green bold
110 shelve.newest = green bold
111 shelve.name = blue bold
111 shelve.name = blue bold
112
112
113 histedit.remaining = red bold
113 histedit.remaining = red bold
114
114
115 Custom colors
115 Custom colors
116 -------------
116 -------------
117
117
118 Because there are only eight standard colors, this module allows you
118 Because there are only eight standard colors, this module allows you
119 to define color names for other color slots which might be available
119 to define color names for other color slots which might be available
120 for your terminal type, assuming terminfo mode. For instance::
120 for your terminal type, assuming terminfo mode. For instance::
121
121
122 color.brightblue = 12
122 color.brightblue = 12
123 color.pink = 207
123 color.pink = 207
124 color.orange = 202
124 color.orange = 202
125
125
126 to set 'brightblue' to color slot 12 (useful for 16 color terminals
126 to set 'brightblue' to color slot 12 (useful for 16 color terminals
127 that have brighter colors defined in the upper eight) and, 'pink' and
127 that have brighter colors defined in the upper eight) and, 'pink' and
128 'orange' to colors in 256-color xterm's default color cube. These
128 'orange' to colors in 256-color xterm's default color cube. These
129 defined colors may then be used as any of the pre-defined eight,
129 defined colors may then be used as any of the pre-defined eight,
130 including appending '_background' to set the background to that color.
130 including appending '_background' to set the background to that color.
131
131
132 Modes
132 Modes
133 -----
133 -----
134
134
135 By default, the color extension will use ANSI mode (or win32 mode on
135 By default, the color extension will use ANSI mode (or win32 mode on
136 Windows) if it detects a terminal. To override auto mode (to enable
136 Windows) if it detects a terminal. To override auto mode (to enable
137 terminfo mode, for example), set the following configuration option::
137 terminfo mode, for example), set the following configuration option::
138
138
139 [color]
139 [color]
140 mode = terminfo
140 mode = terminfo
141
141
142 Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
142 Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
143 disable color.
143 disable color.
144
144
145 Note that on some systems, terminfo mode may cause problems when using
145 Note that on some systems, terminfo mode may cause problems when using
146 color with the pager extension and less -R. less with the -R option
146 color with the pager extension and less -R. less with the -R option
147 will only display ECMA-48 color codes, and terminfo mode may sometimes
147 will only display ECMA-48 color codes, and terminfo mode may sometimes
148 emit codes that less doesn't understand. You can work around this by
148 emit codes that less doesn't understand. You can work around this by
149 either using ansi mode (or auto mode), or by using less -r (which will
149 either using ansi mode (or auto mode), or by using less -r (which will
150 pass through all terminal control codes, not just color control
150 pass through all terminal control codes, not just color control
151 codes).
151 codes).
152
152
153 On some systems (such as MSYS in Windows), the terminal may support
153 On some systems (such as MSYS in Windows), the terminal may support
154 a different color mode than the pager (activated via the "pager"
154 a different color mode than the pager (activated via the "pager"
155 extension). It is possible to define separate modes depending on whether
155 extension). It is possible to define separate modes depending on whether
156 the pager is active::
156 the pager is active::
157
157
158 [color]
158 [color]
159 mode = auto
159 mode = auto
160 pagermode = ansi
160 pagermode = ansi
161
161
162 If ``pagermode`` is not defined, the ``mode`` will be used.
162 If ``pagermode`` is not defined, the ``mode`` will be used.
163 '''
163 '''
164
164
165 from __future__ import absolute_import
165 from __future__ import absolute_import
166
166
167 try:
167 try:
168 import curses
168 import curses
169 curses.COLOR_BLACK # force import
169 curses.COLOR_BLACK # force import
170 except ImportError:
170 except ImportError:
171 curses = None
171 curses = None
172
172
173 from mercurial.i18n import _
173 from mercurial.i18n import _
174 from mercurial import (
174 from mercurial import (
175 cmdutil,
175 cmdutil,
176 color,
176 color,
177 commands,
177 commands,
178 dispatch,
178 dispatch,
179 extensions,
179 extensions,
180 subrepo,
181 ui as uimod,
180 ui as uimod,
182 )
181 )
183
182
184 cmdtable = {}
183 cmdtable = {}
185 command = cmdutil.command(cmdtable)
184 command = cmdutil.command(cmdtable)
186 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
185 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
187 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
186 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
188 # be specifying the version(s) of Mercurial they are tested with, or
187 # be specifying the version(s) of Mercurial they are tested with, or
189 # leave the attribute unspecified.
188 # leave the attribute unspecified.
190 testedwith = 'ships-with-hg-core'
189 testedwith = 'ships-with-hg-core'
191
190
192 def uisetup(ui):
191 def uisetup(ui):
193 if ui.plain():
192 if ui.plain():
194 return
193 return
195 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
194 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
196 mode = color._modesetup(ui_, opts['color'])
195 mode = color._modesetup(ui_, opts['color'])
197 uimod.ui._colormode = mode
196 uimod.ui._colormode = mode
198 if mode and mode != 'debug':
197 if mode and mode != 'debug':
199 color.configstyles(ui_)
198 color.configstyles(ui_)
200 return orig(ui_, opts, cmd, cmdfunc)
199 return orig(ui_, opts, cmd, cmdfunc)
201 def colorgit(orig, gitsub, commands, env=None, stream=False, cwd=None):
202 if gitsub.ui._colormode and len(commands) and commands[0] == "diff":
203 # insert the argument in the front,
204 # the end of git diff arguments is used for paths
205 commands.insert(1, '--color')
206 return orig(gitsub, commands, env, stream, cwd)
207 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
200 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
208 extensions.wrapfunction(subrepo.gitsubrepo, '_gitnodir', colorgit)
209
201
210 def extsetup(ui):
202 def extsetup(ui):
211 commands.globalopts.append(
203 commands.globalopts.append(
212 ('', 'color', 'auto',
204 ('', 'color', 'auto',
213 # i18n: 'always', 'auto', 'never', and 'debug' are keywords
205 # i18n: 'always', 'auto', 'never', and 'debug' are keywords
214 # and should not be translated
206 # and should not be translated
215 _("when to colorize (boolean, always, auto, never, or debug)"),
207 _("when to colorize (boolean, always, auto, never, or debug)"),
216 _('TYPE')))
208 _('TYPE')))
217
209
218 @command('debugcolor',
210 @command('debugcolor',
219 [('', 'style', None, _('show all configured styles'))],
211 [('', 'style', None, _('show all configured styles'))],
220 'hg debugcolor')
212 'hg debugcolor')
221 def debugcolor(ui, repo, **opts):
213 def debugcolor(ui, repo, **opts):
222 """show available color, effects or style"""
214 """show available color, effects or style"""
223 ui.write(('color mode: %s\n') % ui._colormode)
215 ui.write(('color mode: %s\n') % ui._colormode)
224 if opts.get('style'):
216 if opts.get('style'):
225 return _debugdisplaystyle(ui)
217 return _debugdisplaystyle(ui)
226 else:
218 else:
227 return _debugdisplaycolor(ui)
219 return _debugdisplaycolor(ui)
228
220
229 def _debugdisplaycolor(ui):
221 def _debugdisplaycolor(ui):
230 oldstyle = color._styles.copy()
222 oldstyle = color._styles.copy()
231 try:
223 try:
232 color._styles.clear()
224 color._styles.clear()
233 for effect in color._effects.keys():
225 for effect in color._effects.keys():
234 color._styles[effect] = effect
226 color._styles[effect] = effect
235 if color._terminfo_params:
227 if color._terminfo_params:
236 for k, v in ui.configitems('color'):
228 for k, v in ui.configitems('color'):
237 if k.startswith('color.'):
229 if k.startswith('color.'):
238 color._styles[k] = k[6:]
230 color._styles[k] = k[6:]
239 elif k.startswith('terminfo.'):
231 elif k.startswith('terminfo.'):
240 color._styles[k] = k[9:]
232 color._styles[k] = k[9:]
241 ui.write(_('available colors:\n'))
233 ui.write(_('available colors:\n'))
242 # sort label with a '_' after the other to group '_background' entry.
234 # sort label with a '_' after the other to group '_background' entry.
243 items = sorted(color._styles.items(),
235 items = sorted(color._styles.items(),
244 key=lambda i: ('_' in i[0], i[0], i[1]))
236 key=lambda i: ('_' in i[0], i[0], i[1]))
245 for colorname, label in items:
237 for colorname, label in items:
246 ui.write(('%s\n') % colorname, label=label)
238 ui.write(('%s\n') % colorname, label=label)
247 finally:
239 finally:
248 color._styles.clear()
240 color._styles.clear()
249 color._styles.update(oldstyle)
241 color._styles.update(oldstyle)
250
242
251 def _debugdisplaystyle(ui):
243 def _debugdisplaystyle(ui):
252 ui.write(_('available style:\n'))
244 ui.write(_('available style:\n'))
253 width = max(len(s) for s in color._styles)
245 width = max(len(s) for s in color._styles)
254 for label, effects in sorted(color._styles.items()):
246 for label, effects in sorted(color._styles.items()):
255 ui.write('%s' % label, label=label)
247 ui.write('%s' % label, label=label)
256 if effects:
248 if effects:
257 # 50
249 # 50
258 ui.write(': ')
250 ui.write(': ')
259 ui.write(' ' * (max(0, width - len(label))))
251 ui.write(' ' * (max(0, width - len(label))))
260 ui.write(', '.join(ui.label(e, e) for e in effects.split()))
252 ui.write(', '.join(ui.label(e, e) for e in effects.split()))
261 ui.write('\n')
253 ui.write('\n')
@@ -1,1964 +1,1968 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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import copy
10 import copy
11 import errno
11 import errno
12 import hashlib
12 import hashlib
13 import os
13 import os
14 import posixpath
14 import posixpath
15 import re
15 import re
16 import stat
16 import stat
17 import subprocess
17 import subprocess
18 import sys
18 import sys
19 import tarfile
19 import tarfile
20 import xml.dom.minidom
20 import xml.dom.minidom
21
21
22
22
23 from .i18n import _
23 from .i18n import _
24 from . import (
24 from . import (
25 cmdutil,
25 cmdutil,
26 config,
26 config,
27 encoding,
27 encoding,
28 error,
28 error,
29 exchange,
29 exchange,
30 filemerge,
30 filemerge,
31 match as matchmod,
31 match as matchmod,
32 node,
32 node,
33 pathutil,
33 pathutil,
34 phases,
34 phases,
35 pycompat,
35 pycompat,
36 scmutil,
36 scmutil,
37 util,
37 util,
38 )
38 )
39
39
40 hg = None
40 hg = None
41 propertycache = util.propertycache
41 propertycache = util.propertycache
42
42
43 nullstate = ('', '', 'empty')
43 nullstate = ('', '', 'empty')
44
44
45 def _expandedabspath(path):
45 def _expandedabspath(path):
46 '''
46 '''
47 get a path or url and if it is a path expand it and return an absolute path
47 get a path or url and if it is a path expand it and return an absolute path
48 '''
48 '''
49 expandedpath = util.urllocalpath(util.expandpath(path))
49 expandedpath = util.urllocalpath(util.expandpath(path))
50 u = util.url(expandedpath)
50 u = util.url(expandedpath)
51 if not u.scheme:
51 if not u.scheme:
52 path = util.normpath(os.path.abspath(u.path))
52 path = util.normpath(os.path.abspath(u.path))
53 return path
53 return path
54
54
55 def _getstorehashcachename(remotepath):
55 def _getstorehashcachename(remotepath):
56 '''get a unique filename for the store hash cache of a remote repository'''
56 '''get a unique filename for the store hash cache of a remote repository'''
57 return hashlib.sha1(_expandedabspath(remotepath)).hexdigest()[0:12]
57 return hashlib.sha1(_expandedabspath(remotepath)).hexdigest()[0:12]
58
58
59 class SubrepoAbort(error.Abort):
59 class SubrepoAbort(error.Abort):
60 """Exception class used to avoid handling a subrepo error more than once"""
60 """Exception class used to avoid handling a subrepo error more than once"""
61 def __init__(self, *args, **kw):
61 def __init__(self, *args, **kw):
62 self.subrepo = kw.pop('subrepo', None)
62 self.subrepo = kw.pop('subrepo', None)
63 self.cause = kw.pop('cause', None)
63 self.cause = kw.pop('cause', None)
64 error.Abort.__init__(self, *args, **kw)
64 error.Abort.__init__(self, *args, **kw)
65
65
66 def annotatesubrepoerror(func):
66 def annotatesubrepoerror(func):
67 def decoratedmethod(self, *args, **kargs):
67 def decoratedmethod(self, *args, **kargs):
68 try:
68 try:
69 res = func(self, *args, **kargs)
69 res = func(self, *args, **kargs)
70 except SubrepoAbort as ex:
70 except SubrepoAbort as ex:
71 # This exception has already been handled
71 # This exception has already been handled
72 raise ex
72 raise ex
73 except error.Abort as ex:
73 except error.Abort as ex:
74 subrepo = subrelpath(self)
74 subrepo = subrelpath(self)
75 errormsg = str(ex) + ' ' + _('(in subrepo %s)') % subrepo
75 errormsg = str(ex) + ' ' + _('(in subrepo %s)') % subrepo
76 # avoid handling this exception by raising a SubrepoAbort exception
76 # avoid handling this exception by raising a SubrepoAbort exception
77 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
77 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
78 cause=sys.exc_info())
78 cause=sys.exc_info())
79 return res
79 return res
80 return decoratedmethod
80 return decoratedmethod
81
81
82 def state(ctx, ui):
82 def state(ctx, ui):
83 """return a state dict, mapping subrepo paths configured in .hgsub
83 """return a state dict, mapping subrepo paths configured in .hgsub
84 to tuple: (source from .hgsub, revision from .hgsubstate, kind
84 to tuple: (source from .hgsub, revision from .hgsubstate, kind
85 (key in types dict))
85 (key in types dict))
86 """
86 """
87 p = config.config()
87 p = config.config()
88 repo = ctx.repo()
88 repo = ctx.repo()
89 def read(f, sections=None, remap=None):
89 def read(f, sections=None, remap=None):
90 if f in ctx:
90 if f in ctx:
91 try:
91 try:
92 data = ctx[f].data()
92 data = ctx[f].data()
93 except IOError as err:
93 except IOError as err:
94 if err.errno != errno.ENOENT:
94 if err.errno != errno.ENOENT:
95 raise
95 raise
96 # handle missing subrepo spec files as removed
96 # handle missing subrepo spec files as removed
97 ui.warn(_("warning: subrepo spec file \'%s\' not found\n") %
97 ui.warn(_("warning: subrepo spec file \'%s\' not found\n") %
98 repo.pathto(f))
98 repo.pathto(f))
99 return
99 return
100 p.parse(f, data, sections, remap, read)
100 p.parse(f, data, sections, remap, read)
101 else:
101 else:
102 raise error.Abort(_("subrepo spec file \'%s\' not found") %
102 raise error.Abort(_("subrepo spec file \'%s\' not found") %
103 repo.pathto(f))
103 repo.pathto(f))
104 if '.hgsub' in ctx:
104 if '.hgsub' in ctx:
105 read('.hgsub')
105 read('.hgsub')
106
106
107 for path, src in ui.configitems('subpaths'):
107 for path, src in ui.configitems('subpaths'):
108 p.set('subpaths', path, src, ui.configsource('subpaths', path))
108 p.set('subpaths', path, src, ui.configsource('subpaths', path))
109
109
110 rev = {}
110 rev = {}
111 if '.hgsubstate' in ctx:
111 if '.hgsubstate' in ctx:
112 try:
112 try:
113 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
113 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
114 l = l.lstrip()
114 l = l.lstrip()
115 if not l:
115 if not l:
116 continue
116 continue
117 try:
117 try:
118 revision, path = l.split(" ", 1)
118 revision, path = l.split(" ", 1)
119 except ValueError:
119 except ValueError:
120 raise error.Abort(_("invalid subrepository revision "
120 raise error.Abort(_("invalid subrepository revision "
121 "specifier in \'%s\' line %d")
121 "specifier in \'%s\' line %d")
122 % (repo.pathto('.hgsubstate'), (i + 1)))
122 % (repo.pathto('.hgsubstate'), (i + 1)))
123 rev[path] = revision
123 rev[path] = revision
124 except IOError as err:
124 except IOError as err:
125 if err.errno != errno.ENOENT:
125 if err.errno != errno.ENOENT:
126 raise
126 raise
127
127
128 def remap(src):
128 def remap(src):
129 for pattern, repl in p.items('subpaths'):
129 for pattern, repl in p.items('subpaths'):
130 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
130 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
131 # does a string decode.
131 # does a string decode.
132 repl = repl.encode('string-escape')
132 repl = repl.encode('string-escape')
133 # However, we still want to allow back references to go
133 # However, we still want to allow back references to go
134 # through unharmed, so we turn r'\\1' into r'\1'. Again,
134 # through unharmed, so we turn r'\\1' into r'\1'. Again,
135 # extra escapes are needed because re.sub string decodes.
135 # extra escapes are needed because re.sub string decodes.
136 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
136 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
137 try:
137 try:
138 src = re.sub(pattern, repl, src, 1)
138 src = re.sub(pattern, repl, src, 1)
139 except re.error as e:
139 except re.error as e:
140 raise error.Abort(_("bad subrepository pattern in %s: %s")
140 raise error.Abort(_("bad subrepository pattern in %s: %s")
141 % (p.source('subpaths', pattern), e))
141 % (p.source('subpaths', pattern), e))
142 return src
142 return src
143
143
144 state = {}
144 state = {}
145 for path, src in p[''].items():
145 for path, src in p[''].items():
146 kind = 'hg'
146 kind = 'hg'
147 if src.startswith('['):
147 if src.startswith('['):
148 if ']' not in src:
148 if ']' not in src:
149 raise error.Abort(_('missing ] in subrepo source'))
149 raise error.Abort(_('missing ] in subrepo source'))
150 kind, src = src.split(']', 1)
150 kind, src = src.split(']', 1)
151 kind = kind[1:]
151 kind = kind[1:]
152 src = src.lstrip() # strip any extra whitespace after ']'
152 src = src.lstrip() # strip any extra whitespace after ']'
153
153
154 if not util.url(src).isabs():
154 if not util.url(src).isabs():
155 parent = _abssource(repo, abort=False)
155 parent = _abssource(repo, abort=False)
156 if parent:
156 if parent:
157 parent = util.url(parent)
157 parent = util.url(parent)
158 parent.path = posixpath.join(parent.path or '', src)
158 parent.path = posixpath.join(parent.path or '', src)
159 parent.path = posixpath.normpath(parent.path)
159 parent.path = posixpath.normpath(parent.path)
160 joined = str(parent)
160 joined = str(parent)
161 # Remap the full joined path and use it if it changes,
161 # Remap the full joined path and use it if it changes,
162 # else remap the original source.
162 # else remap the original source.
163 remapped = remap(joined)
163 remapped = remap(joined)
164 if remapped == joined:
164 if remapped == joined:
165 src = remap(src)
165 src = remap(src)
166 else:
166 else:
167 src = remapped
167 src = remapped
168
168
169 src = remap(src)
169 src = remap(src)
170 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
170 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
171
171
172 return state
172 return state
173
173
174 def writestate(repo, state):
174 def writestate(repo, state):
175 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
175 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
176 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)
176 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)
177 if state[s][1] != nullstate[1]]
177 if state[s][1] != nullstate[1]]
178 repo.wwrite('.hgsubstate', ''.join(lines), '')
178 repo.wwrite('.hgsubstate', ''.join(lines), '')
179
179
180 def submerge(repo, wctx, mctx, actx, overwrite, labels=None):
180 def submerge(repo, wctx, mctx, actx, overwrite, labels=None):
181 """delegated from merge.applyupdates: merging of .hgsubstate file
181 """delegated from merge.applyupdates: merging of .hgsubstate file
182 in working context, merging context and ancestor context"""
182 in working context, merging context and ancestor context"""
183 if mctx == actx: # backwards?
183 if mctx == actx: # backwards?
184 actx = wctx.p1()
184 actx = wctx.p1()
185 s1 = wctx.substate
185 s1 = wctx.substate
186 s2 = mctx.substate
186 s2 = mctx.substate
187 sa = actx.substate
187 sa = actx.substate
188 sm = {}
188 sm = {}
189
189
190 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
190 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
191
191
192 def debug(s, msg, r=""):
192 def debug(s, msg, r=""):
193 if r:
193 if r:
194 r = "%s:%s:%s" % r
194 r = "%s:%s:%s" % r
195 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
195 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
196
196
197 for s, l in sorted(s1.iteritems()):
197 for s, l in sorted(s1.iteritems()):
198 a = sa.get(s, nullstate)
198 a = sa.get(s, nullstate)
199 ld = l # local state with possible dirty flag for compares
199 ld = l # local state with possible dirty flag for compares
200 if wctx.sub(s).dirty():
200 if wctx.sub(s).dirty():
201 ld = (l[0], l[1] + "+")
201 ld = (l[0], l[1] + "+")
202 if wctx == actx: # overwrite
202 if wctx == actx: # overwrite
203 a = ld
203 a = ld
204
204
205 if s in s2:
205 if s in s2:
206 prompts = filemerge.partextras(labels)
206 prompts = filemerge.partextras(labels)
207 prompts['s'] = s
207 prompts['s'] = s
208 r = s2[s]
208 r = s2[s]
209 if ld == r or r == a: # no change or local is newer
209 if ld == r or r == a: # no change or local is newer
210 sm[s] = l
210 sm[s] = l
211 continue
211 continue
212 elif ld == a: # other side changed
212 elif ld == a: # other side changed
213 debug(s, "other changed, get", r)
213 debug(s, "other changed, get", r)
214 wctx.sub(s).get(r, overwrite)
214 wctx.sub(s).get(r, overwrite)
215 sm[s] = r
215 sm[s] = r
216 elif ld[0] != r[0]: # sources differ
216 elif ld[0] != r[0]: # sources differ
217 prompts['lo'] = l[0]
217 prompts['lo'] = l[0]
218 prompts['ro'] = r[0]
218 prompts['ro'] = r[0]
219 if repo.ui.promptchoice(
219 if repo.ui.promptchoice(
220 _(' subrepository sources for %(s)s differ\n'
220 _(' subrepository sources for %(s)s differ\n'
221 'use (l)ocal%(l)s source (%(lo)s)'
221 'use (l)ocal%(l)s source (%(lo)s)'
222 ' or (r)emote%(o)s source (%(ro)s)?'
222 ' or (r)emote%(o)s source (%(ro)s)?'
223 '$$ &Local $$ &Remote') % prompts, 0):
223 '$$ &Local $$ &Remote') % prompts, 0):
224 debug(s, "prompt changed, get", r)
224 debug(s, "prompt changed, get", r)
225 wctx.sub(s).get(r, overwrite)
225 wctx.sub(s).get(r, overwrite)
226 sm[s] = r
226 sm[s] = r
227 elif ld[1] == a[1]: # local side is unchanged
227 elif ld[1] == a[1]: # local side is unchanged
228 debug(s, "other side changed, get", r)
228 debug(s, "other side changed, get", r)
229 wctx.sub(s).get(r, overwrite)
229 wctx.sub(s).get(r, overwrite)
230 sm[s] = r
230 sm[s] = r
231 else:
231 else:
232 debug(s, "both sides changed")
232 debug(s, "both sides changed")
233 srepo = wctx.sub(s)
233 srepo = wctx.sub(s)
234 prompts['sl'] = srepo.shortid(l[1])
234 prompts['sl'] = srepo.shortid(l[1])
235 prompts['sr'] = srepo.shortid(r[1])
235 prompts['sr'] = srepo.shortid(r[1])
236 option = repo.ui.promptchoice(
236 option = repo.ui.promptchoice(
237 _(' subrepository %(s)s diverged (local revision: %(sl)s, '
237 _(' subrepository %(s)s diverged (local revision: %(sl)s, '
238 'remote revision: %(sr)s)\n'
238 'remote revision: %(sr)s)\n'
239 '(M)erge, keep (l)ocal%(l)s or keep (r)emote%(o)s?'
239 '(M)erge, keep (l)ocal%(l)s or keep (r)emote%(o)s?'
240 '$$ &Merge $$ &Local $$ &Remote')
240 '$$ &Merge $$ &Local $$ &Remote')
241 % prompts, 0)
241 % prompts, 0)
242 if option == 0:
242 if option == 0:
243 wctx.sub(s).merge(r)
243 wctx.sub(s).merge(r)
244 sm[s] = l
244 sm[s] = l
245 debug(s, "merge with", r)
245 debug(s, "merge with", r)
246 elif option == 1:
246 elif option == 1:
247 sm[s] = l
247 sm[s] = l
248 debug(s, "keep local subrepo revision", l)
248 debug(s, "keep local subrepo revision", l)
249 else:
249 else:
250 wctx.sub(s).get(r, overwrite)
250 wctx.sub(s).get(r, overwrite)
251 sm[s] = r
251 sm[s] = r
252 debug(s, "get remote subrepo revision", r)
252 debug(s, "get remote subrepo revision", r)
253 elif ld == a: # remote removed, local unchanged
253 elif ld == a: # remote removed, local unchanged
254 debug(s, "remote removed, remove")
254 debug(s, "remote removed, remove")
255 wctx.sub(s).remove()
255 wctx.sub(s).remove()
256 elif a == nullstate: # not present in remote or ancestor
256 elif a == nullstate: # not present in remote or ancestor
257 debug(s, "local added, keep")
257 debug(s, "local added, keep")
258 sm[s] = l
258 sm[s] = l
259 continue
259 continue
260 else:
260 else:
261 if repo.ui.promptchoice(
261 if repo.ui.promptchoice(
262 _(' local%(l)s changed subrepository %(s)s'
262 _(' local%(l)s changed subrepository %(s)s'
263 ' which remote%(o)s removed\n'
263 ' which remote%(o)s removed\n'
264 'use (c)hanged version or (d)elete?'
264 'use (c)hanged version or (d)elete?'
265 '$$ &Changed $$ &Delete') % prompts, 0):
265 '$$ &Changed $$ &Delete') % prompts, 0):
266 debug(s, "prompt remove")
266 debug(s, "prompt remove")
267 wctx.sub(s).remove()
267 wctx.sub(s).remove()
268
268
269 for s, r in sorted(s2.items()):
269 for s, r in sorted(s2.items()):
270 if s in s1:
270 if s in s1:
271 continue
271 continue
272 elif s not in sa:
272 elif s not in sa:
273 debug(s, "remote added, get", r)
273 debug(s, "remote added, get", r)
274 mctx.sub(s).get(r)
274 mctx.sub(s).get(r)
275 sm[s] = r
275 sm[s] = r
276 elif r != sa[s]:
276 elif r != sa[s]:
277 if repo.ui.promptchoice(
277 if repo.ui.promptchoice(
278 _(' remote%(o)s changed subrepository %(s)s'
278 _(' remote%(o)s changed subrepository %(s)s'
279 ' which local%(l)s removed\n'
279 ' which local%(l)s removed\n'
280 'use (c)hanged version or (d)elete?'
280 'use (c)hanged version or (d)elete?'
281 '$$ &Changed $$ &Delete') % prompts, 0) == 0:
281 '$$ &Changed $$ &Delete') % prompts, 0) == 0:
282 debug(s, "prompt recreate", r)
282 debug(s, "prompt recreate", r)
283 mctx.sub(s).get(r)
283 mctx.sub(s).get(r)
284 sm[s] = r
284 sm[s] = r
285
285
286 # record merged .hgsubstate
286 # record merged .hgsubstate
287 writestate(repo, sm)
287 writestate(repo, sm)
288 return sm
288 return sm
289
289
290 def _updateprompt(ui, sub, dirty, local, remote):
290 def _updateprompt(ui, sub, dirty, local, remote):
291 if dirty:
291 if dirty:
292 msg = (_(' subrepository sources for %s differ\n'
292 msg = (_(' subrepository sources for %s differ\n'
293 'use (l)ocal source (%s) or (r)emote source (%s)?'
293 'use (l)ocal source (%s) or (r)emote source (%s)?'
294 '$$ &Local $$ &Remote')
294 '$$ &Local $$ &Remote')
295 % (subrelpath(sub), local, remote))
295 % (subrelpath(sub), local, remote))
296 else:
296 else:
297 msg = (_(' subrepository sources for %s differ (in checked out '
297 msg = (_(' subrepository sources for %s differ (in checked out '
298 'version)\n'
298 'version)\n'
299 'use (l)ocal source (%s) or (r)emote source (%s)?'
299 'use (l)ocal source (%s) or (r)emote source (%s)?'
300 '$$ &Local $$ &Remote')
300 '$$ &Local $$ &Remote')
301 % (subrelpath(sub), local, remote))
301 % (subrelpath(sub), local, remote))
302 return ui.promptchoice(msg, 0)
302 return ui.promptchoice(msg, 0)
303
303
304 def reporelpath(repo):
304 def reporelpath(repo):
305 """return path to this (sub)repo as seen from outermost repo"""
305 """return path to this (sub)repo as seen from outermost repo"""
306 parent = repo
306 parent = repo
307 while util.safehasattr(parent, '_subparent'):
307 while util.safehasattr(parent, '_subparent'):
308 parent = parent._subparent
308 parent = parent._subparent
309 return repo.root[len(pathutil.normasprefix(parent.root)):]
309 return repo.root[len(pathutil.normasprefix(parent.root)):]
310
310
311 def subrelpath(sub):
311 def subrelpath(sub):
312 """return path to this subrepo as seen from outermost repo"""
312 """return path to this subrepo as seen from outermost repo"""
313 return sub._relpath
313 return sub._relpath
314
314
315 def _abssource(repo, push=False, abort=True):
315 def _abssource(repo, push=False, abort=True):
316 """return pull/push path of repo - either based on parent repo .hgsub info
316 """return pull/push path of repo - either based on parent repo .hgsub info
317 or on the top repo config. Abort or return None if no source found."""
317 or on the top repo config. Abort or return None if no source found."""
318 if util.safehasattr(repo, '_subparent'):
318 if util.safehasattr(repo, '_subparent'):
319 source = util.url(repo._subsource)
319 source = util.url(repo._subsource)
320 if source.isabs():
320 if source.isabs():
321 return str(source)
321 return str(source)
322 source.path = posixpath.normpath(source.path)
322 source.path = posixpath.normpath(source.path)
323 parent = _abssource(repo._subparent, push, abort=False)
323 parent = _abssource(repo._subparent, push, abort=False)
324 if parent:
324 if parent:
325 parent = util.url(util.pconvert(parent))
325 parent = util.url(util.pconvert(parent))
326 parent.path = posixpath.join(parent.path or '', source.path)
326 parent.path = posixpath.join(parent.path or '', source.path)
327 parent.path = posixpath.normpath(parent.path)
327 parent.path = posixpath.normpath(parent.path)
328 return str(parent)
328 return str(parent)
329 else: # recursion reached top repo
329 else: # recursion reached top repo
330 if util.safehasattr(repo, '_subtoppath'):
330 if util.safehasattr(repo, '_subtoppath'):
331 return repo._subtoppath
331 return repo._subtoppath
332 if push and repo.ui.config('paths', 'default-push'):
332 if push and repo.ui.config('paths', 'default-push'):
333 return repo.ui.config('paths', 'default-push')
333 return repo.ui.config('paths', 'default-push')
334 if repo.ui.config('paths', 'default'):
334 if repo.ui.config('paths', 'default'):
335 return repo.ui.config('paths', 'default')
335 return repo.ui.config('paths', 'default')
336 if repo.shared():
336 if repo.shared():
337 # chop off the .hg component to get the default path form
337 # chop off the .hg component to get the default path form
338 return os.path.dirname(repo.sharedpath)
338 return os.path.dirname(repo.sharedpath)
339 if abort:
339 if abort:
340 raise error.Abort(_("default path for subrepository not found"))
340 raise error.Abort(_("default path for subrepository not found"))
341
341
342 def _sanitize(ui, vfs, ignore):
342 def _sanitize(ui, vfs, ignore):
343 for dirname, dirs, names in vfs.walk():
343 for dirname, dirs, names in vfs.walk():
344 for i, d in enumerate(dirs):
344 for i, d in enumerate(dirs):
345 if d.lower() == ignore:
345 if d.lower() == ignore:
346 del dirs[i]
346 del dirs[i]
347 break
347 break
348 if vfs.basename(dirname).lower() != '.hg':
348 if vfs.basename(dirname).lower() != '.hg':
349 continue
349 continue
350 for f in names:
350 for f in names:
351 if f.lower() == 'hgrc':
351 if f.lower() == 'hgrc':
352 ui.warn(_("warning: removing potentially hostile 'hgrc' "
352 ui.warn(_("warning: removing potentially hostile 'hgrc' "
353 "in '%s'\n") % vfs.join(dirname))
353 "in '%s'\n") % vfs.join(dirname))
354 vfs.unlink(vfs.reljoin(dirname, f))
354 vfs.unlink(vfs.reljoin(dirname, f))
355
355
356 def subrepo(ctx, path, allowwdir=False, allowcreate=True):
356 def subrepo(ctx, path, allowwdir=False, allowcreate=True):
357 """return instance of the right subrepo class for subrepo in path"""
357 """return instance of the right subrepo class for subrepo in path"""
358 # subrepo inherently violates our import layering rules
358 # subrepo inherently violates our import layering rules
359 # because it wants to make repo objects from deep inside the stack
359 # because it wants to make repo objects from deep inside the stack
360 # so we manually delay the circular imports to not break
360 # so we manually delay the circular imports to not break
361 # scripts that don't use our demand-loading
361 # scripts that don't use our demand-loading
362 global hg
362 global hg
363 from . import hg as h
363 from . import hg as h
364 hg = h
364 hg = h
365
365
366 pathutil.pathauditor(ctx.repo().root)(path)
366 pathutil.pathauditor(ctx.repo().root)(path)
367 state = ctx.substate[path]
367 state = ctx.substate[path]
368 if state[2] not in types:
368 if state[2] not in types:
369 raise error.Abort(_('unknown subrepo type %s') % state[2])
369 raise error.Abort(_('unknown subrepo type %s') % state[2])
370 if allowwdir:
370 if allowwdir:
371 state = (state[0], ctx.subrev(path), state[2])
371 state = (state[0], ctx.subrev(path), state[2])
372 return types[state[2]](ctx, path, state[:2], allowcreate)
372 return types[state[2]](ctx, path, state[:2], allowcreate)
373
373
374 def nullsubrepo(ctx, path, pctx):
374 def nullsubrepo(ctx, path, pctx):
375 """return an empty subrepo in pctx for the extant subrepo in ctx"""
375 """return an empty subrepo in pctx for the extant subrepo in ctx"""
376 # subrepo inherently violates our import layering rules
376 # subrepo inherently violates our import layering rules
377 # because it wants to make repo objects from deep inside the stack
377 # because it wants to make repo objects from deep inside the stack
378 # so we manually delay the circular imports to not break
378 # so we manually delay the circular imports to not break
379 # scripts that don't use our demand-loading
379 # scripts that don't use our demand-loading
380 global hg
380 global hg
381 from . import hg as h
381 from . import hg as h
382 hg = h
382 hg = h
383
383
384 pathutil.pathauditor(ctx.repo().root)(path)
384 pathutil.pathauditor(ctx.repo().root)(path)
385 state = ctx.substate[path]
385 state = ctx.substate[path]
386 if state[2] not in types:
386 if state[2] not in types:
387 raise error.Abort(_('unknown subrepo type %s') % state[2])
387 raise error.Abort(_('unknown subrepo type %s') % state[2])
388 subrev = ''
388 subrev = ''
389 if state[2] == 'hg':
389 if state[2] == 'hg':
390 subrev = "0" * 40
390 subrev = "0" * 40
391 return types[state[2]](pctx, path, (state[0], subrev), True)
391 return types[state[2]](pctx, path, (state[0], subrev), True)
392
392
393 def newcommitphase(ui, ctx):
393 def newcommitphase(ui, ctx):
394 commitphase = phases.newcommitphase(ui)
394 commitphase = phases.newcommitphase(ui)
395 substate = getattr(ctx, "substate", None)
395 substate = getattr(ctx, "substate", None)
396 if not substate:
396 if not substate:
397 return commitphase
397 return commitphase
398 check = ui.config('phases', 'checksubrepos', 'follow')
398 check = ui.config('phases', 'checksubrepos', 'follow')
399 if check not in ('ignore', 'follow', 'abort'):
399 if check not in ('ignore', 'follow', 'abort'):
400 raise error.Abort(_('invalid phases.checksubrepos configuration: %s')
400 raise error.Abort(_('invalid phases.checksubrepos configuration: %s')
401 % (check))
401 % (check))
402 if check == 'ignore':
402 if check == 'ignore':
403 return commitphase
403 return commitphase
404 maxphase = phases.public
404 maxphase = phases.public
405 maxsub = None
405 maxsub = None
406 for s in sorted(substate):
406 for s in sorted(substate):
407 sub = ctx.sub(s)
407 sub = ctx.sub(s)
408 subphase = sub.phase(substate[s][1])
408 subphase = sub.phase(substate[s][1])
409 if maxphase < subphase:
409 if maxphase < subphase:
410 maxphase = subphase
410 maxphase = subphase
411 maxsub = s
411 maxsub = s
412 if commitphase < maxphase:
412 if commitphase < maxphase:
413 if check == 'abort':
413 if check == 'abort':
414 raise error.Abort(_("can't commit in %s phase"
414 raise error.Abort(_("can't commit in %s phase"
415 " conflicting %s from subrepository %s") %
415 " conflicting %s from subrepository %s") %
416 (phases.phasenames[commitphase],
416 (phases.phasenames[commitphase],
417 phases.phasenames[maxphase], maxsub))
417 phases.phasenames[maxphase], maxsub))
418 ui.warn(_("warning: changes are committed in"
418 ui.warn(_("warning: changes are committed in"
419 " %s phase from subrepository %s\n") %
419 " %s phase from subrepository %s\n") %
420 (phases.phasenames[maxphase], maxsub))
420 (phases.phasenames[maxphase], maxsub))
421 return maxphase
421 return maxphase
422 return commitphase
422 return commitphase
423
423
424 # subrepo classes need to implement the following abstract class:
424 # subrepo classes need to implement the following abstract class:
425
425
426 class abstractsubrepo(object):
426 class abstractsubrepo(object):
427
427
428 def __init__(self, ctx, path):
428 def __init__(self, ctx, path):
429 """Initialize abstractsubrepo part
429 """Initialize abstractsubrepo part
430
430
431 ``ctx`` is the context referring this subrepository in the
431 ``ctx`` is the context referring this subrepository in the
432 parent repository.
432 parent repository.
433
433
434 ``path`` is the path to this subrepository as seen from
434 ``path`` is the path to this subrepository as seen from
435 innermost repository.
435 innermost repository.
436 """
436 """
437 self.ui = ctx.repo().ui
437 self.ui = ctx.repo().ui
438 self._ctx = ctx
438 self._ctx = ctx
439 self._path = path
439 self._path = path
440
440
441 def storeclean(self, path):
441 def storeclean(self, path):
442 """
442 """
443 returns true if the repository has not changed since it was last
443 returns true if the repository has not changed since it was last
444 cloned from or pushed to a given repository.
444 cloned from or pushed to a given repository.
445 """
445 """
446 return False
446 return False
447
447
448 def dirty(self, ignoreupdate=False):
448 def dirty(self, ignoreupdate=False):
449 """returns true if the dirstate of the subrepo is dirty or does not
449 """returns true if the dirstate of the subrepo is dirty or does not
450 match current stored state. If ignoreupdate is true, only check
450 match current stored state. If ignoreupdate is true, only check
451 whether the subrepo has uncommitted changes in its dirstate.
451 whether the subrepo has uncommitted changes in its dirstate.
452 """
452 """
453 raise NotImplementedError
453 raise NotImplementedError
454
454
455 def dirtyreason(self, ignoreupdate=False):
455 def dirtyreason(self, ignoreupdate=False):
456 """return reason string if it is ``dirty()``
456 """return reason string if it is ``dirty()``
457
457
458 Returned string should have enough information for the message
458 Returned string should have enough information for the message
459 of exception.
459 of exception.
460
460
461 This returns None, otherwise.
461 This returns None, otherwise.
462 """
462 """
463 if self.dirty(ignoreupdate=ignoreupdate):
463 if self.dirty(ignoreupdate=ignoreupdate):
464 return _("uncommitted changes in subrepository '%s'"
464 return _("uncommitted changes in subrepository '%s'"
465 ) % subrelpath(self)
465 ) % subrelpath(self)
466
466
467 def bailifchanged(self, ignoreupdate=False, hint=None):
467 def bailifchanged(self, ignoreupdate=False, hint=None):
468 """raise Abort if subrepository is ``dirty()``
468 """raise Abort if subrepository is ``dirty()``
469 """
469 """
470 dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate)
470 dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate)
471 if dirtyreason:
471 if dirtyreason:
472 raise error.Abort(dirtyreason, hint=hint)
472 raise error.Abort(dirtyreason, hint=hint)
473
473
474 def basestate(self):
474 def basestate(self):
475 """current working directory base state, disregarding .hgsubstate
475 """current working directory base state, disregarding .hgsubstate
476 state and working directory modifications"""
476 state and working directory modifications"""
477 raise NotImplementedError
477 raise NotImplementedError
478
478
479 def checknested(self, path):
479 def checknested(self, path):
480 """check if path is a subrepository within this repository"""
480 """check if path is a subrepository within this repository"""
481 return False
481 return False
482
482
483 def commit(self, text, user, date):
483 def commit(self, text, user, date):
484 """commit the current changes to the subrepo with the given
484 """commit the current changes to the subrepo with the given
485 log message. Use given user and date if possible. Return the
485 log message. Use given user and date if possible. Return the
486 new state of the subrepo.
486 new state of the subrepo.
487 """
487 """
488 raise NotImplementedError
488 raise NotImplementedError
489
489
490 def phase(self, state):
490 def phase(self, state):
491 """returns phase of specified state in the subrepository.
491 """returns phase of specified state in the subrepository.
492 """
492 """
493 return phases.public
493 return phases.public
494
494
495 def remove(self):
495 def remove(self):
496 """remove the subrepo
496 """remove the subrepo
497
497
498 (should verify the dirstate is not dirty first)
498 (should verify the dirstate is not dirty first)
499 """
499 """
500 raise NotImplementedError
500 raise NotImplementedError
501
501
502 def get(self, state, overwrite=False):
502 def get(self, state, overwrite=False):
503 """run whatever commands are needed to put the subrepo into
503 """run whatever commands are needed to put the subrepo into
504 this state
504 this state
505 """
505 """
506 raise NotImplementedError
506 raise NotImplementedError
507
507
508 def merge(self, state):
508 def merge(self, state):
509 """merge currently-saved state with the new state."""
509 """merge currently-saved state with the new state."""
510 raise NotImplementedError
510 raise NotImplementedError
511
511
512 def push(self, opts):
512 def push(self, opts):
513 """perform whatever action is analogous to 'hg push'
513 """perform whatever action is analogous to 'hg push'
514
514
515 This may be a no-op on some systems.
515 This may be a no-op on some systems.
516 """
516 """
517 raise NotImplementedError
517 raise NotImplementedError
518
518
519 def add(self, ui, match, prefix, explicitonly, **opts):
519 def add(self, ui, match, prefix, explicitonly, **opts):
520 return []
520 return []
521
521
522 def addremove(self, matcher, prefix, opts, dry_run, similarity):
522 def addremove(self, matcher, prefix, opts, dry_run, similarity):
523 self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
523 self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
524 return 1
524 return 1
525
525
526 def cat(self, match, prefix, **opts):
526 def cat(self, match, prefix, **opts):
527 return 1
527 return 1
528
528
529 def status(self, rev2, **opts):
529 def status(self, rev2, **opts):
530 return scmutil.status([], [], [], [], [], [], [])
530 return scmutil.status([], [], [], [], [], [], [])
531
531
532 def diff(self, ui, diffopts, node2, match, prefix, **opts):
532 def diff(self, ui, diffopts, node2, match, prefix, **opts):
533 pass
533 pass
534
534
535 def outgoing(self, ui, dest, opts):
535 def outgoing(self, ui, dest, opts):
536 return 1
536 return 1
537
537
538 def incoming(self, ui, source, opts):
538 def incoming(self, ui, source, opts):
539 return 1
539 return 1
540
540
541 def files(self):
541 def files(self):
542 """return filename iterator"""
542 """return filename iterator"""
543 raise NotImplementedError
543 raise NotImplementedError
544
544
545 def filedata(self, name, decode):
545 def filedata(self, name, decode):
546 """return file data, optionally passed through repo decoders"""
546 """return file data, optionally passed through repo decoders"""
547 raise NotImplementedError
547 raise NotImplementedError
548
548
549 def fileflags(self, name):
549 def fileflags(self, name):
550 """return file flags"""
550 """return file flags"""
551 return ''
551 return ''
552
552
553 def getfileset(self, expr):
553 def getfileset(self, expr):
554 """Resolve the fileset expression for this repo"""
554 """Resolve the fileset expression for this repo"""
555 return set()
555 return set()
556
556
557 def printfiles(self, ui, m, fm, fmt, subrepos):
557 def printfiles(self, ui, m, fm, fmt, subrepos):
558 """handle the files command for this subrepo"""
558 """handle the files command for this subrepo"""
559 return 1
559 return 1
560
560
561 def archive(self, archiver, prefix, match=None, decode=True):
561 def archive(self, archiver, prefix, match=None, decode=True):
562 if match is not None:
562 if match is not None:
563 files = [f for f in self.files() if match(f)]
563 files = [f for f in self.files() if match(f)]
564 else:
564 else:
565 files = self.files()
565 files = self.files()
566 total = len(files)
566 total = len(files)
567 relpath = subrelpath(self)
567 relpath = subrelpath(self)
568 self.ui.progress(_('archiving (%s)') % relpath, 0,
568 self.ui.progress(_('archiving (%s)') % relpath, 0,
569 unit=_('files'), total=total)
569 unit=_('files'), total=total)
570 for i, name in enumerate(files):
570 for i, name in enumerate(files):
571 flags = self.fileflags(name)
571 flags = self.fileflags(name)
572 mode = 'x' in flags and 0o755 or 0o644
572 mode = 'x' in flags and 0o755 or 0o644
573 symlink = 'l' in flags
573 symlink = 'l' in flags
574 archiver.addfile(prefix + self._path + '/' + name,
574 archiver.addfile(prefix + self._path + '/' + name,
575 mode, symlink, self.filedata(name, decode))
575 mode, symlink, self.filedata(name, decode))
576 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
576 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
577 unit=_('files'), total=total)
577 unit=_('files'), total=total)
578 self.ui.progress(_('archiving (%s)') % relpath, None)
578 self.ui.progress(_('archiving (%s)') % relpath, None)
579 return total
579 return total
580
580
581 def walk(self, match):
581 def walk(self, match):
582 '''
582 '''
583 walk recursively through the directory tree, finding all files
583 walk recursively through the directory tree, finding all files
584 matched by the match function
584 matched by the match function
585 '''
585 '''
586 pass
586 pass
587
587
588 def forget(self, match, prefix):
588 def forget(self, match, prefix):
589 return ([], [])
589 return ([], [])
590
590
591 def removefiles(self, matcher, prefix, after, force, subrepos, warnings):
591 def removefiles(self, matcher, prefix, after, force, subrepos, warnings):
592 """remove the matched files from the subrepository and the filesystem,
592 """remove the matched files from the subrepository and the filesystem,
593 possibly by force and/or after the file has been removed from the
593 possibly by force and/or after the file has been removed from the
594 filesystem. Return 0 on success, 1 on any warning.
594 filesystem. Return 0 on success, 1 on any warning.
595 """
595 """
596 warnings.append(_("warning: removefiles not implemented (%s)")
596 warnings.append(_("warning: removefiles not implemented (%s)")
597 % self._path)
597 % self._path)
598 return 1
598 return 1
599
599
600 def revert(self, substate, *pats, **opts):
600 def revert(self, substate, *pats, **opts):
601 self.ui.warn(_('%s: reverting %s subrepos is unsupported\n') \
601 self.ui.warn(_('%s: reverting %s subrepos is unsupported\n') \
602 % (substate[0], substate[2]))
602 % (substate[0], substate[2]))
603 return []
603 return []
604
604
605 def shortid(self, revid):
605 def shortid(self, revid):
606 return revid
606 return revid
607
607
608 def verify(self):
608 def verify(self):
609 '''verify the integrity of the repository. Return 0 on success or
609 '''verify the integrity of the repository. Return 0 on success or
610 warning, 1 on any error.
610 warning, 1 on any error.
611 '''
611 '''
612 return 0
612 return 0
613
613
614 @propertycache
614 @propertycache
615 def wvfs(self):
615 def wvfs(self):
616 """return vfs to access the working directory of this subrepository
616 """return vfs to access the working directory of this subrepository
617 """
617 """
618 return scmutil.vfs(self._ctx.repo().wvfs.join(self._path))
618 return scmutil.vfs(self._ctx.repo().wvfs.join(self._path))
619
619
620 @propertycache
620 @propertycache
621 def _relpath(self):
621 def _relpath(self):
622 """return path to this subrepository as seen from outermost repository
622 """return path to this subrepository as seen from outermost repository
623 """
623 """
624 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
624 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
625
625
626 class hgsubrepo(abstractsubrepo):
626 class hgsubrepo(abstractsubrepo):
627 def __init__(self, ctx, path, state, allowcreate):
627 def __init__(self, ctx, path, state, allowcreate):
628 super(hgsubrepo, self).__init__(ctx, path)
628 super(hgsubrepo, self).__init__(ctx, path)
629 self._state = state
629 self._state = state
630 r = ctx.repo()
630 r = ctx.repo()
631 root = r.wjoin(path)
631 root = r.wjoin(path)
632 create = allowcreate and not r.wvfs.exists('%s/.hg' % path)
632 create = allowcreate and not r.wvfs.exists('%s/.hg' % path)
633 self._repo = hg.repository(r.baseui, root, create=create)
633 self._repo = hg.repository(r.baseui, root, create=create)
634
634
635 # Propagate the parent's --hidden option
635 # Propagate the parent's --hidden option
636 if r is r.unfiltered():
636 if r is r.unfiltered():
637 self._repo = self._repo.unfiltered()
637 self._repo = self._repo.unfiltered()
638
638
639 self.ui = self._repo.ui
639 self.ui = self._repo.ui
640 for s, k in [('ui', 'commitsubrepos')]:
640 for s, k in [('ui', 'commitsubrepos')]:
641 v = r.ui.config(s, k)
641 v = r.ui.config(s, k)
642 if v:
642 if v:
643 self.ui.setconfig(s, k, v, 'subrepo')
643 self.ui.setconfig(s, k, v, 'subrepo')
644 # internal config: ui._usedassubrepo
644 # internal config: ui._usedassubrepo
645 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
645 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
646 self._initrepo(r, state[0], create)
646 self._initrepo(r, state[0], create)
647
647
648 def storeclean(self, path):
648 def storeclean(self, path):
649 with self._repo.lock():
649 with self._repo.lock():
650 return self._storeclean(path)
650 return self._storeclean(path)
651
651
652 def _storeclean(self, path):
652 def _storeclean(self, path):
653 clean = True
653 clean = True
654 itercache = self._calcstorehash(path)
654 itercache = self._calcstorehash(path)
655 for filehash in self._readstorehashcache(path):
655 for filehash in self._readstorehashcache(path):
656 if filehash != next(itercache, None):
656 if filehash != next(itercache, None):
657 clean = False
657 clean = False
658 break
658 break
659 if clean:
659 if clean:
660 # if not empty:
660 # if not empty:
661 # the cached and current pull states have a different size
661 # the cached and current pull states have a different size
662 clean = next(itercache, None) is None
662 clean = next(itercache, None) is None
663 return clean
663 return clean
664
664
665 def _calcstorehash(self, remotepath):
665 def _calcstorehash(self, remotepath):
666 '''calculate a unique "store hash"
666 '''calculate a unique "store hash"
667
667
668 This method is used to to detect when there are changes that may
668 This method is used to to detect when there are changes that may
669 require a push to a given remote path.'''
669 require a push to a given remote path.'''
670 # sort the files that will be hashed in increasing (likely) file size
670 # sort the files that will be hashed in increasing (likely) file size
671 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
671 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
672 yield '# %s\n' % _expandedabspath(remotepath)
672 yield '# %s\n' % _expandedabspath(remotepath)
673 vfs = self._repo.vfs
673 vfs = self._repo.vfs
674 for relname in filelist:
674 for relname in filelist:
675 filehash = hashlib.sha1(vfs.tryread(relname)).hexdigest()
675 filehash = hashlib.sha1(vfs.tryread(relname)).hexdigest()
676 yield '%s = %s\n' % (relname, filehash)
676 yield '%s = %s\n' % (relname, filehash)
677
677
678 @propertycache
678 @propertycache
679 def _cachestorehashvfs(self):
679 def _cachestorehashvfs(self):
680 return scmutil.vfs(self._repo.join('cache/storehash'))
680 return scmutil.vfs(self._repo.join('cache/storehash'))
681
681
682 def _readstorehashcache(self, remotepath):
682 def _readstorehashcache(self, remotepath):
683 '''read the store hash cache for a given remote repository'''
683 '''read the store hash cache for a given remote repository'''
684 cachefile = _getstorehashcachename(remotepath)
684 cachefile = _getstorehashcachename(remotepath)
685 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
685 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
686
686
687 def _cachestorehash(self, remotepath):
687 def _cachestorehash(self, remotepath):
688 '''cache the current store hash
688 '''cache the current store hash
689
689
690 Each remote repo requires its own store hash cache, because a subrepo
690 Each remote repo requires its own store hash cache, because a subrepo
691 store may be "clean" versus a given remote repo, but not versus another
691 store may be "clean" versus a given remote repo, but not versus another
692 '''
692 '''
693 cachefile = _getstorehashcachename(remotepath)
693 cachefile = _getstorehashcachename(remotepath)
694 with self._repo.lock():
694 with self._repo.lock():
695 storehash = list(self._calcstorehash(remotepath))
695 storehash = list(self._calcstorehash(remotepath))
696 vfs = self._cachestorehashvfs
696 vfs = self._cachestorehashvfs
697 vfs.writelines(cachefile, storehash, mode='w', notindexed=True)
697 vfs.writelines(cachefile, storehash, mode='w', notindexed=True)
698
698
699 def _getctx(self):
699 def _getctx(self):
700 '''fetch the context for this subrepo revision, possibly a workingctx
700 '''fetch the context for this subrepo revision, possibly a workingctx
701 '''
701 '''
702 if self._ctx.rev() is None:
702 if self._ctx.rev() is None:
703 return self._repo[None] # workingctx if parent is workingctx
703 return self._repo[None] # workingctx if parent is workingctx
704 else:
704 else:
705 rev = self._state[1]
705 rev = self._state[1]
706 return self._repo[rev]
706 return self._repo[rev]
707
707
708 @annotatesubrepoerror
708 @annotatesubrepoerror
709 def _initrepo(self, parentrepo, source, create):
709 def _initrepo(self, parentrepo, source, create):
710 self._repo._subparent = parentrepo
710 self._repo._subparent = parentrepo
711 self._repo._subsource = source
711 self._repo._subsource = source
712
712
713 if create:
713 if create:
714 lines = ['[paths]\n']
714 lines = ['[paths]\n']
715
715
716 def addpathconfig(key, value):
716 def addpathconfig(key, value):
717 if value:
717 if value:
718 lines.append('%s = %s\n' % (key, value))
718 lines.append('%s = %s\n' % (key, value))
719 self.ui.setconfig('paths', key, value, 'subrepo')
719 self.ui.setconfig('paths', key, value, 'subrepo')
720
720
721 defpath = _abssource(self._repo, abort=False)
721 defpath = _abssource(self._repo, abort=False)
722 defpushpath = _abssource(self._repo, True, abort=False)
722 defpushpath = _abssource(self._repo, True, abort=False)
723 addpathconfig('default', defpath)
723 addpathconfig('default', defpath)
724 if defpath != defpushpath:
724 if defpath != defpushpath:
725 addpathconfig('default-push', defpushpath)
725 addpathconfig('default-push', defpushpath)
726
726
727 fp = self._repo.vfs("hgrc", "w", text=True)
727 fp = self._repo.vfs("hgrc", "w", text=True)
728 try:
728 try:
729 fp.write(''.join(lines))
729 fp.write(''.join(lines))
730 finally:
730 finally:
731 fp.close()
731 fp.close()
732
732
733 @annotatesubrepoerror
733 @annotatesubrepoerror
734 def add(self, ui, match, prefix, explicitonly, **opts):
734 def add(self, ui, match, prefix, explicitonly, **opts):
735 return cmdutil.add(ui, self._repo, match,
735 return cmdutil.add(ui, self._repo, match,
736 self.wvfs.reljoin(prefix, self._path),
736 self.wvfs.reljoin(prefix, self._path),
737 explicitonly, **opts)
737 explicitonly, **opts)
738
738
739 @annotatesubrepoerror
739 @annotatesubrepoerror
740 def addremove(self, m, prefix, opts, dry_run, similarity):
740 def addremove(self, m, prefix, opts, dry_run, similarity):
741 # In the same way as sub directories are processed, once in a subrepo,
741 # In the same way as sub directories are processed, once in a subrepo,
742 # always entry any of its subrepos. Don't corrupt the options that will
742 # always entry any of its subrepos. Don't corrupt the options that will
743 # be used to process sibling subrepos however.
743 # be used to process sibling subrepos however.
744 opts = copy.copy(opts)
744 opts = copy.copy(opts)
745 opts['subrepos'] = True
745 opts['subrepos'] = True
746 return scmutil.addremove(self._repo, m,
746 return scmutil.addremove(self._repo, m,
747 self.wvfs.reljoin(prefix, self._path), opts,
747 self.wvfs.reljoin(prefix, self._path), opts,
748 dry_run, similarity)
748 dry_run, similarity)
749
749
750 @annotatesubrepoerror
750 @annotatesubrepoerror
751 def cat(self, match, prefix, **opts):
751 def cat(self, match, prefix, **opts):
752 rev = self._state[1]
752 rev = self._state[1]
753 ctx = self._repo[rev]
753 ctx = self._repo[rev]
754 return cmdutil.cat(self.ui, self._repo, ctx, match, prefix, **opts)
754 return cmdutil.cat(self.ui, self._repo, ctx, match, prefix, **opts)
755
755
756 @annotatesubrepoerror
756 @annotatesubrepoerror
757 def status(self, rev2, **opts):
757 def status(self, rev2, **opts):
758 try:
758 try:
759 rev1 = self._state[1]
759 rev1 = self._state[1]
760 ctx1 = self._repo[rev1]
760 ctx1 = self._repo[rev1]
761 ctx2 = self._repo[rev2]
761 ctx2 = self._repo[rev2]
762 return self._repo.status(ctx1, ctx2, **opts)
762 return self._repo.status(ctx1, ctx2, **opts)
763 except error.RepoLookupError as inst:
763 except error.RepoLookupError as inst:
764 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
764 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
765 % (inst, subrelpath(self)))
765 % (inst, subrelpath(self)))
766 return scmutil.status([], [], [], [], [], [], [])
766 return scmutil.status([], [], [], [], [], [], [])
767
767
768 @annotatesubrepoerror
768 @annotatesubrepoerror
769 def diff(self, ui, diffopts, node2, match, prefix, **opts):
769 def diff(self, ui, diffopts, node2, match, prefix, **opts):
770 try:
770 try:
771 node1 = node.bin(self._state[1])
771 node1 = node.bin(self._state[1])
772 # We currently expect node2 to come from substate and be
772 # We currently expect node2 to come from substate and be
773 # in hex format
773 # in hex format
774 if node2 is not None:
774 if node2 is not None:
775 node2 = node.bin(node2)
775 node2 = node.bin(node2)
776 cmdutil.diffordiffstat(ui, self._repo, diffopts,
776 cmdutil.diffordiffstat(ui, self._repo, diffopts,
777 node1, node2, match,
777 node1, node2, match,
778 prefix=posixpath.join(prefix, self._path),
778 prefix=posixpath.join(prefix, self._path),
779 listsubrepos=True, **opts)
779 listsubrepos=True, **opts)
780 except error.RepoLookupError as inst:
780 except error.RepoLookupError as inst:
781 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
781 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
782 % (inst, subrelpath(self)))
782 % (inst, subrelpath(self)))
783
783
784 @annotatesubrepoerror
784 @annotatesubrepoerror
785 def archive(self, archiver, prefix, match=None, decode=True):
785 def archive(self, archiver, prefix, match=None, decode=True):
786 self._get(self._state + ('hg',))
786 self._get(self._state + ('hg',))
787 total = abstractsubrepo.archive(self, archiver, prefix, match)
787 total = abstractsubrepo.archive(self, archiver, prefix, match)
788 rev = self._state[1]
788 rev = self._state[1]
789 ctx = self._repo[rev]
789 ctx = self._repo[rev]
790 for subpath in ctx.substate:
790 for subpath in ctx.substate:
791 s = subrepo(ctx, subpath, True)
791 s = subrepo(ctx, subpath, True)
792 submatch = matchmod.subdirmatcher(subpath, match)
792 submatch = matchmod.subdirmatcher(subpath, match)
793 total += s.archive(archiver, prefix + self._path + '/', submatch,
793 total += s.archive(archiver, prefix + self._path + '/', submatch,
794 decode)
794 decode)
795 return total
795 return total
796
796
797 @annotatesubrepoerror
797 @annotatesubrepoerror
798 def dirty(self, ignoreupdate=False):
798 def dirty(self, ignoreupdate=False):
799 r = self._state[1]
799 r = self._state[1]
800 if r == '' and not ignoreupdate: # no state recorded
800 if r == '' and not ignoreupdate: # no state recorded
801 return True
801 return True
802 w = self._repo[None]
802 w = self._repo[None]
803 if r != w.p1().hex() and not ignoreupdate:
803 if r != w.p1().hex() and not ignoreupdate:
804 # different version checked out
804 # different version checked out
805 return True
805 return True
806 return w.dirty() # working directory changed
806 return w.dirty() # working directory changed
807
807
808 def basestate(self):
808 def basestate(self):
809 return self._repo['.'].hex()
809 return self._repo['.'].hex()
810
810
811 def checknested(self, path):
811 def checknested(self, path):
812 return self._repo._checknested(self._repo.wjoin(path))
812 return self._repo._checknested(self._repo.wjoin(path))
813
813
814 @annotatesubrepoerror
814 @annotatesubrepoerror
815 def commit(self, text, user, date):
815 def commit(self, text, user, date):
816 # don't bother committing in the subrepo if it's only been
816 # don't bother committing in the subrepo if it's only been
817 # updated
817 # updated
818 if not self.dirty(True):
818 if not self.dirty(True):
819 return self._repo['.'].hex()
819 return self._repo['.'].hex()
820 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
820 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
821 n = self._repo.commit(text, user, date)
821 n = self._repo.commit(text, user, date)
822 if not n:
822 if not n:
823 return self._repo['.'].hex() # different version checked out
823 return self._repo['.'].hex() # different version checked out
824 return node.hex(n)
824 return node.hex(n)
825
825
826 @annotatesubrepoerror
826 @annotatesubrepoerror
827 def phase(self, state):
827 def phase(self, state):
828 return self._repo[state].phase()
828 return self._repo[state].phase()
829
829
830 @annotatesubrepoerror
830 @annotatesubrepoerror
831 def remove(self):
831 def remove(self):
832 # we can't fully delete the repository as it may contain
832 # we can't fully delete the repository as it may contain
833 # local-only history
833 # local-only history
834 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
834 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
835 hg.clean(self._repo, node.nullid, False)
835 hg.clean(self._repo, node.nullid, False)
836
836
837 def _get(self, state):
837 def _get(self, state):
838 source, revision, kind = state
838 source, revision, kind = state
839 if revision in self._repo.unfiltered():
839 if revision in self._repo.unfiltered():
840 return True
840 return True
841 self._repo._subsource = source
841 self._repo._subsource = source
842 srcurl = _abssource(self._repo)
842 srcurl = _abssource(self._repo)
843 other = hg.peer(self._repo, {}, srcurl)
843 other = hg.peer(self._repo, {}, srcurl)
844 if len(self._repo) == 0:
844 if len(self._repo) == 0:
845 self.ui.status(_('cloning subrepo %s from %s\n')
845 self.ui.status(_('cloning subrepo %s from %s\n')
846 % (subrelpath(self), srcurl))
846 % (subrelpath(self), srcurl))
847 parentrepo = self._repo._subparent
847 parentrepo = self._repo._subparent
848 # use self._repo.vfs instead of self.wvfs to remove .hg only
848 # use self._repo.vfs instead of self.wvfs to remove .hg only
849 self._repo.vfs.rmtree()
849 self._repo.vfs.rmtree()
850 other, cloned = hg.clone(self._repo._subparent.baseui, {},
850 other, cloned = hg.clone(self._repo._subparent.baseui, {},
851 other, self._repo.root,
851 other, self._repo.root,
852 update=False)
852 update=False)
853 self._repo = cloned.local()
853 self._repo = cloned.local()
854 self._initrepo(parentrepo, source, create=True)
854 self._initrepo(parentrepo, source, create=True)
855 self._cachestorehash(srcurl)
855 self._cachestorehash(srcurl)
856 else:
856 else:
857 self.ui.status(_('pulling subrepo %s from %s\n')
857 self.ui.status(_('pulling subrepo %s from %s\n')
858 % (subrelpath(self), srcurl))
858 % (subrelpath(self), srcurl))
859 cleansub = self.storeclean(srcurl)
859 cleansub = self.storeclean(srcurl)
860 exchange.pull(self._repo, other)
860 exchange.pull(self._repo, other)
861 if cleansub:
861 if cleansub:
862 # keep the repo clean after pull
862 # keep the repo clean after pull
863 self._cachestorehash(srcurl)
863 self._cachestorehash(srcurl)
864 return False
864 return False
865
865
866 @annotatesubrepoerror
866 @annotatesubrepoerror
867 def get(self, state, overwrite=False):
867 def get(self, state, overwrite=False):
868 inrepo = self._get(state)
868 inrepo = self._get(state)
869 source, revision, kind = state
869 source, revision, kind = state
870 repo = self._repo
870 repo = self._repo
871 repo.ui.debug("getting subrepo %s\n" % self._path)
871 repo.ui.debug("getting subrepo %s\n" % self._path)
872 if inrepo:
872 if inrepo:
873 urepo = repo.unfiltered()
873 urepo = repo.unfiltered()
874 ctx = urepo[revision]
874 ctx = urepo[revision]
875 if ctx.hidden():
875 if ctx.hidden():
876 urepo.ui.warn(
876 urepo.ui.warn(
877 _('revision %s in subrepo %s is hidden\n') \
877 _('revision %s in subrepo %s is hidden\n') \
878 % (revision[0:12], self._path))
878 % (revision[0:12], self._path))
879 repo = urepo
879 repo = urepo
880 hg.updaterepo(repo, revision, overwrite)
880 hg.updaterepo(repo, revision, overwrite)
881
881
882 @annotatesubrepoerror
882 @annotatesubrepoerror
883 def merge(self, state):
883 def merge(self, state):
884 self._get(state)
884 self._get(state)
885 cur = self._repo['.']
885 cur = self._repo['.']
886 dst = self._repo[state[1]]
886 dst = self._repo[state[1]]
887 anc = dst.ancestor(cur)
887 anc = dst.ancestor(cur)
888
888
889 def mergefunc():
889 def mergefunc():
890 if anc == cur and dst.branch() == cur.branch():
890 if anc == cur and dst.branch() == cur.branch():
891 self.ui.debug("updating subrepo %s\n" % subrelpath(self))
891 self.ui.debug("updating subrepo %s\n" % subrelpath(self))
892 hg.update(self._repo, state[1])
892 hg.update(self._repo, state[1])
893 elif anc == dst:
893 elif anc == dst:
894 self.ui.debug("skipping subrepo %s\n" % subrelpath(self))
894 self.ui.debug("skipping subrepo %s\n" % subrelpath(self))
895 else:
895 else:
896 self.ui.debug("merging subrepo %s\n" % subrelpath(self))
896 self.ui.debug("merging subrepo %s\n" % subrelpath(self))
897 hg.merge(self._repo, state[1], remind=False)
897 hg.merge(self._repo, state[1], remind=False)
898
898
899 wctx = self._repo[None]
899 wctx = self._repo[None]
900 if self.dirty():
900 if self.dirty():
901 if anc != dst:
901 if anc != dst:
902 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
902 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
903 mergefunc()
903 mergefunc()
904 else:
904 else:
905 mergefunc()
905 mergefunc()
906 else:
906 else:
907 mergefunc()
907 mergefunc()
908
908
909 @annotatesubrepoerror
909 @annotatesubrepoerror
910 def push(self, opts):
910 def push(self, opts):
911 force = opts.get('force')
911 force = opts.get('force')
912 newbranch = opts.get('new_branch')
912 newbranch = opts.get('new_branch')
913 ssh = opts.get('ssh')
913 ssh = opts.get('ssh')
914
914
915 # push subrepos depth-first for coherent ordering
915 # push subrepos depth-first for coherent ordering
916 c = self._repo['']
916 c = self._repo['']
917 subs = c.substate # only repos that are committed
917 subs = c.substate # only repos that are committed
918 for s in sorted(subs):
918 for s in sorted(subs):
919 if c.sub(s).push(opts) == 0:
919 if c.sub(s).push(opts) == 0:
920 return False
920 return False
921
921
922 dsturl = _abssource(self._repo, True)
922 dsturl = _abssource(self._repo, True)
923 if not force:
923 if not force:
924 if self.storeclean(dsturl):
924 if self.storeclean(dsturl):
925 self.ui.status(
925 self.ui.status(
926 _('no changes made to subrepo %s since last push to %s\n')
926 _('no changes made to subrepo %s since last push to %s\n')
927 % (subrelpath(self), dsturl))
927 % (subrelpath(self), dsturl))
928 return None
928 return None
929 self.ui.status(_('pushing subrepo %s to %s\n') %
929 self.ui.status(_('pushing subrepo %s to %s\n') %
930 (subrelpath(self), dsturl))
930 (subrelpath(self), dsturl))
931 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
931 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
932 res = exchange.push(self._repo, other, force, newbranch=newbranch)
932 res = exchange.push(self._repo, other, force, newbranch=newbranch)
933
933
934 # the repo is now clean
934 # the repo is now clean
935 self._cachestorehash(dsturl)
935 self._cachestorehash(dsturl)
936 return res.cgresult
936 return res.cgresult
937
937
938 @annotatesubrepoerror
938 @annotatesubrepoerror
939 def outgoing(self, ui, dest, opts):
939 def outgoing(self, ui, dest, opts):
940 if 'rev' in opts or 'branch' in opts:
940 if 'rev' in opts or 'branch' in opts:
941 opts = copy.copy(opts)
941 opts = copy.copy(opts)
942 opts.pop('rev', None)
942 opts.pop('rev', None)
943 opts.pop('branch', None)
943 opts.pop('branch', None)
944 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
944 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
945
945
946 @annotatesubrepoerror
946 @annotatesubrepoerror
947 def incoming(self, ui, source, opts):
947 def incoming(self, ui, source, opts):
948 if 'rev' in opts or 'branch' in opts:
948 if 'rev' in opts or 'branch' in opts:
949 opts = copy.copy(opts)
949 opts = copy.copy(opts)
950 opts.pop('rev', None)
950 opts.pop('rev', None)
951 opts.pop('branch', None)
951 opts.pop('branch', None)
952 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
952 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
953
953
954 @annotatesubrepoerror
954 @annotatesubrepoerror
955 def files(self):
955 def files(self):
956 rev = self._state[1]
956 rev = self._state[1]
957 ctx = self._repo[rev]
957 ctx = self._repo[rev]
958 return ctx.manifest().keys()
958 return ctx.manifest().keys()
959
959
960 def filedata(self, name, decode):
960 def filedata(self, name, decode):
961 rev = self._state[1]
961 rev = self._state[1]
962 data = self._repo[rev][name].data()
962 data = self._repo[rev][name].data()
963 if decode:
963 if decode:
964 data = self._repo.wwritedata(name, data)
964 data = self._repo.wwritedata(name, data)
965 return data
965 return data
966
966
967 def fileflags(self, name):
967 def fileflags(self, name):
968 rev = self._state[1]
968 rev = self._state[1]
969 ctx = self._repo[rev]
969 ctx = self._repo[rev]
970 return ctx.flags(name)
970 return ctx.flags(name)
971
971
972 @annotatesubrepoerror
972 @annotatesubrepoerror
973 def printfiles(self, ui, m, fm, fmt, subrepos):
973 def printfiles(self, ui, m, fm, fmt, subrepos):
974 # If the parent context is a workingctx, use the workingctx here for
974 # If the parent context is a workingctx, use the workingctx here for
975 # consistency.
975 # consistency.
976 if self._ctx.rev() is None:
976 if self._ctx.rev() is None:
977 ctx = self._repo[None]
977 ctx = self._repo[None]
978 else:
978 else:
979 rev = self._state[1]
979 rev = self._state[1]
980 ctx = self._repo[rev]
980 ctx = self._repo[rev]
981 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
981 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
982
982
983 @annotatesubrepoerror
983 @annotatesubrepoerror
984 def getfileset(self, expr):
984 def getfileset(self, expr):
985 if self._ctx.rev() is None:
985 if self._ctx.rev() is None:
986 ctx = self._repo[None]
986 ctx = self._repo[None]
987 else:
987 else:
988 rev = self._state[1]
988 rev = self._state[1]
989 ctx = self._repo[rev]
989 ctx = self._repo[rev]
990
990
991 files = ctx.getfileset(expr)
991 files = ctx.getfileset(expr)
992
992
993 for subpath in ctx.substate:
993 for subpath in ctx.substate:
994 sub = ctx.sub(subpath)
994 sub = ctx.sub(subpath)
995
995
996 try:
996 try:
997 files.extend(subpath + '/' + f for f in sub.getfileset(expr))
997 files.extend(subpath + '/' + f for f in sub.getfileset(expr))
998 except error.LookupError:
998 except error.LookupError:
999 self.ui.status(_("skipping missing subrepository: %s\n")
999 self.ui.status(_("skipping missing subrepository: %s\n")
1000 % self.wvfs.reljoin(reporelpath(self), subpath))
1000 % self.wvfs.reljoin(reporelpath(self), subpath))
1001 return files
1001 return files
1002
1002
1003 def walk(self, match):
1003 def walk(self, match):
1004 ctx = self._repo[None]
1004 ctx = self._repo[None]
1005 return ctx.walk(match)
1005 return ctx.walk(match)
1006
1006
1007 @annotatesubrepoerror
1007 @annotatesubrepoerror
1008 def forget(self, match, prefix):
1008 def forget(self, match, prefix):
1009 return cmdutil.forget(self.ui, self._repo, match,
1009 return cmdutil.forget(self.ui, self._repo, match,
1010 self.wvfs.reljoin(prefix, self._path), True)
1010 self.wvfs.reljoin(prefix, self._path), True)
1011
1011
1012 @annotatesubrepoerror
1012 @annotatesubrepoerror
1013 def removefiles(self, matcher, prefix, after, force, subrepos, warnings):
1013 def removefiles(self, matcher, prefix, after, force, subrepos, warnings):
1014 return cmdutil.remove(self.ui, self._repo, matcher,
1014 return cmdutil.remove(self.ui, self._repo, matcher,
1015 self.wvfs.reljoin(prefix, self._path),
1015 self.wvfs.reljoin(prefix, self._path),
1016 after, force, subrepos)
1016 after, force, subrepos)
1017
1017
1018 @annotatesubrepoerror
1018 @annotatesubrepoerror
1019 def revert(self, substate, *pats, **opts):
1019 def revert(self, substate, *pats, **opts):
1020 # reverting a subrepo is a 2 step process:
1020 # reverting a subrepo is a 2 step process:
1021 # 1. if the no_backup is not set, revert all modified
1021 # 1. if the no_backup is not set, revert all modified
1022 # files inside the subrepo
1022 # files inside the subrepo
1023 # 2. update the subrepo to the revision specified in
1023 # 2. update the subrepo to the revision specified in
1024 # the corresponding substate dictionary
1024 # the corresponding substate dictionary
1025 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1025 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1026 if not opts.get('no_backup'):
1026 if not opts.get('no_backup'):
1027 # Revert all files on the subrepo, creating backups
1027 # Revert all files on the subrepo, creating backups
1028 # Note that this will not recursively revert subrepos
1028 # Note that this will not recursively revert subrepos
1029 # We could do it if there was a set:subrepos() predicate
1029 # We could do it if there was a set:subrepos() predicate
1030 opts = opts.copy()
1030 opts = opts.copy()
1031 opts['date'] = None
1031 opts['date'] = None
1032 opts['rev'] = substate[1]
1032 opts['rev'] = substate[1]
1033
1033
1034 self.filerevert(*pats, **opts)
1034 self.filerevert(*pats, **opts)
1035
1035
1036 # Update the repo to the revision specified in the given substate
1036 # Update the repo to the revision specified in the given substate
1037 if not opts.get('dry_run'):
1037 if not opts.get('dry_run'):
1038 self.get(substate, overwrite=True)
1038 self.get(substate, overwrite=True)
1039
1039
1040 def filerevert(self, *pats, **opts):
1040 def filerevert(self, *pats, **opts):
1041 ctx = self._repo[opts['rev']]
1041 ctx = self._repo[opts['rev']]
1042 parents = self._repo.dirstate.parents()
1042 parents = self._repo.dirstate.parents()
1043 if opts.get('all'):
1043 if opts.get('all'):
1044 pats = ['set:modified()']
1044 pats = ['set:modified()']
1045 else:
1045 else:
1046 pats = []
1046 pats = []
1047 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
1047 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
1048
1048
1049 def shortid(self, revid):
1049 def shortid(self, revid):
1050 return revid[:12]
1050 return revid[:12]
1051
1051
1052 def verify(self):
1052 def verify(self):
1053 try:
1053 try:
1054 rev = self._state[1]
1054 rev = self._state[1]
1055 ctx = self._repo.unfiltered()[rev]
1055 ctx = self._repo.unfiltered()[rev]
1056 if ctx.hidden():
1056 if ctx.hidden():
1057 # Since hidden revisions aren't pushed/pulled, it seems worth an
1057 # Since hidden revisions aren't pushed/pulled, it seems worth an
1058 # explicit warning.
1058 # explicit warning.
1059 ui = self._repo.ui
1059 ui = self._repo.ui
1060 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
1060 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
1061 (self._relpath, node.short(self._ctx.node())))
1061 (self._relpath, node.short(self._ctx.node())))
1062 return 0
1062 return 0
1063 except error.RepoLookupError:
1063 except error.RepoLookupError:
1064 # A missing subrepo revision may be a case of needing to pull it, so
1064 # A missing subrepo revision may be a case of needing to pull it, so
1065 # don't treat this as an error.
1065 # don't treat this as an error.
1066 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
1066 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
1067 (self._relpath, node.short(self._ctx.node())))
1067 (self._relpath, node.short(self._ctx.node())))
1068 return 0
1068 return 0
1069
1069
1070 @propertycache
1070 @propertycache
1071 def wvfs(self):
1071 def wvfs(self):
1072 """return own wvfs for efficiency and consistency
1072 """return own wvfs for efficiency and consistency
1073 """
1073 """
1074 return self._repo.wvfs
1074 return self._repo.wvfs
1075
1075
1076 @propertycache
1076 @propertycache
1077 def _relpath(self):
1077 def _relpath(self):
1078 """return path to this subrepository as seen from outermost repository
1078 """return path to this subrepository as seen from outermost repository
1079 """
1079 """
1080 # Keep consistent dir separators by avoiding vfs.join(self._path)
1080 # Keep consistent dir separators by avoiding vfs.join(self._path)
1081 return reporelpath(self._repo)
1081 return reporelpath(self._repo)
1082
1082
1083 class svnsubrepo(abstractsubrepo):
1083 class svnsubrepo(abstractsubrepo):
1084 def __init__(self, ctx, path, state, allowcreate):
1084 def __init__(self, ctx, path, state, allowcreate):
1085 super(svnsubrepo, self).__init__(ctx, path)
1085 super(svnsubrepo, self).__init__(ctx, path)
1086 self._state = state
1086 self._state = state
1087 self._exe = util.findexe('svn')
1087 self._exe = util.findexe('svn')
1088 if not self._exe:
1088 if not self._exe:
1089 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
1089 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
1090 % self._path)
1090 % self._path)
1091
1091
1092 def _svncommand(self, commands, filename='', failok=False):
1092 def _svncommand(self, commands, filename='', failok=False):
1093 cmd = [self._exe]
1093 cmd = [self._exe]
1094 extrakw = {}
1094 extrakw = {}
1095 if not self.ui.interactive():
1095 if not self.ui.interactive():
1096 # Making stdin be a pipe should prevent svn from behaving
1096 # Making stdin be a pipe should prevent svn from behaving
1097 # interactively even if we can't pass --non-interactive.
1097 # interactively even if we can't pass --non-interactive.
1098 extrakw['stdin'] = subprocess.PIPE
1098 extrakw['stdin'] = subprocess.PIPE
1099 # Starting in svn 1.5 --non-interactive is a global flag
1099 # Starting in svn 1.5 --non-interactive is a global flag
1100 # instead of being per-command, but we need to support 1.4 so
1100 # instead of being per-command, but we need to support 1.4 so
1101 # we have to be intelligent about what commands take
1101 # we have to be intelligent about what commands take
1102 # --non-interactive.
1102 # --non-interactive.
1103 if commands[0] in ('update', 'checkout', 'commit'):
1103 if commands[0] in ('update', 'checkout', 'commit'):
1104 cmd.append('--non-interactive')
1104 cmd.append('--non-interactive')
1105 cmd.extend(commands)
1105 cmd.extend(commands)
1106 if filename is not None:
1106 if filename is not None:
1107 path = self.wvfs.reljoin(self._ctx.repo().origroot,
1107 path = self.wvfs.reljoin(self._ctx.repo().origroot,
1108 self._path, filename)
1108 self._path, filename)
1109 cmd.append(path)
1109 cmd.append(path)
1110 env = dict(encoding.environ)
1110 env = dict(encoding.environ)
1111 # Avoid localized output, preserve current locale for everything else.
1111 # Avoid localized output, preserve current locale for everything else.
1112 lc_all = env.get('LC_ALL')
1112 lc_all = env.get('LC_ALL')
1113 if lc_all:
1113 if lc_all:
1114 env['LANG'] = lc_all
1114 env['LANG'] = lc_all
1115 del env['LC_ALL']
1115 del env['LC_ALL']
1116 env['LC_MESSAGES'] = 'C'
1116 env['LC_MESSAGES'] = 'C'
1117 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
1117 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
1118 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1118 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1119 universal_newlines=True, env=env, **extrakw)
1119 universal_newlines=True, env=env, **extrakw)
1120 stdout, stderr = p.communicate()
1120 stdout, stderr = p.communicate()
1121 stderr = stderr.strip()
1121 stderr = stderr.strip()
1122 if not failok:
1122 if not failok:
1123 if p.returncode:
1123 if p.returncode:
1124 raise error.Abort(stderr or 'exited with code %d'
1124 raise error.Abort(stderr or 'exited with code %d'
1125 % p.returncode)
1125 % p.returncode)
1126 if stderr:
1126 if stderr:
1127 self.ui.warn(stderr + '\n')
1127 self.ui.warn(stderr + '\n')
1128 return stdout, stderr
1128 return stdout, stderr
1129
1129
1130 @propertycache
1130 @propertycache
1131 def _svnversion(self):
1131 def _svnversion(self):
1132 output, err = self._svncommand(['--version', '--quiet'], filename=None)
1132 output, err = self._svncommand(['--version', '--quiet'], filename=None)
1133 m = re.search(r'^(\d+)\.(\d+)', output)
1133 m = re.search(r'^(\d+)\.(\d+)', output)
1134 if not m:
1134 if not m:
1135 raise error.Abort(_('cannot retrieve svn tool version'))
1135 raise error.Abort(_('cannot retrieve svn tool version'))
1136 return (int(m.group(1)), int(m.group(2)))
1136 return (int(m.group(1)), int(m.group(2)))
1137
1137
1138 def _wcrevs(self):
1138 def _wcrevs(self):
1139 # Get the working directory revision as well as the last
1139 # Get the working directory revision as well as the last
1140 # commit revision so we can compare the subrepo state with
1140 # commit revision so we can compare the subrepo state with
1141 # both. We used to store the working directory one.
1141 # both. We used to store the working directory one.
1142 output, err = self._svncommand(['info', '--xml'])
1142 output, err = self._svncommand(['info', '--xml'])
1143 doc = xml.dom.minidom.parseString(output)
1143 doc = xml.dom.minidom.parseString(output)
1144 entries = doc.getElementsByTagName('entry')
1144 entries = doc.getElementsByTagName('entry')
1145 lastrev, rev = '0', '0'
1145 lastrev, rev = '0', '0'
1146 if entries:
1146 if entries:
1147 rev = str(entries[0].getAttribute('revision')) or '0'
1147 rev = str(entries[0].getAttribute('revision')) or '0'
1148 commits = entries[0].getElementsByTagName('commit')
1148 commits = entries[0].getElementsByTagName('commit')
1149 if commits:
1149 if commits:
1150 lastrev = str(commits[0].getAttribute('revision')) or '0'
1150 lastrev = str(commits[0].getAttribute('revision')) or '0'
1151 return (lastrev, rev)
1151 return (lastrev, rev)
1152
1152
1153 def _wcrev(self):
1153 def _wcrev(self):
1154 return self._wcrevs()[0]
1154 return self._wcrevs()[0]
1155
1155
1156 def _wcchanged(self):
1156 def _wcchanged(self):
1157 """Return (changes, extchanges, missing) where changes is True
1157 """Return (changes, extchanges, missing) where changes is True
1158 if the working directory was changed, extchanges is
1158 if the working directory was changed, extchanges is
1159 True if any of these changes concern an external entry and missing
1159 True if any of these changes concern an external entry and missing
1160 is True if any change is a missing entry.
1160 is True if any change is a missing entry.
1161 """
1161 """
1162 output, err = self._svncommand(['status', '--xml'])
1162 output, err = self._svncommand(['status', '--xml'])
1163 externals, changes, missing = [], [], []
1163 externals, changes, missing = [], [], []
1164 doc = xml.dom.minidom.parseString(output)
1164 doc = xml.dom.minidom.parseString(output)
1165 for e in doc.getElementsByTagName('entry'):
1165 for e in doc.getElementsByTagName('entry'):
1166 s = e.getElementsByTagName('wc-status')
1166 s = e.getElementsByTagName('wc-status')
1167 if not s:
1167 if not s:
1168 continue
1168 continue
1169 item = s[0].getAttribute('item')
1169 item = s[0].getAttribute('item')
1170 props = s[0].getAttribute('props')
1170 props = s[0].getAttribute('props')
1171 path = e.getAttribute('path')
1171 path = e.getAttribute('path')
1172 if item == 'external':
1172 if item == 'external':
1173 externals.append(path)
1173 externals.append(path)
1174 elif item == 'missing':
1174 elif item == 'missing':
1175 missing.append(path)
1175 missing.append(path)
1176 if (item not in ('', 'normal', 'unversioned', 'external')
1176 if (item not in ('', 'normal', 'unversioned', 'external')
1177 or props not in ('', 'none', 'normal')):
1177 or props not in ('', 'none', 'normal')):
1178 changes.append(path)
1178 changes.append(path)
1179 for path in changes:
1179 for path in changes:
1180 for ext in externals:
1180 for ext in externals:
1181 if path == ext or path.startswith(ext + pycompat.ossep):
1181 if path == ext or path.startswith(ext + pycompat.ossep):
1182 return True, True, bool(missing)
1182 return True, True, bool(missing)
1183 return bool(changes), False, bool(missing)
1183 return bool(changes), False, bool(missing)
1184
1184
1185 def dirty(self, ignoreupdate=False):
1185 def dirty(self, ignoreupdate=False):
1186 if not self._wcchanged()[0]:
1186 if not self._wcchanged()[0]:
1187 if self._state[1] in self._wcrevs() or ignoreupdate:
1187 if self._state[1] in self._wcrevs() or ignoreupdate:
1188 return False
1188 return False
1189 return True
1189 return True
1190
1190
1191 def basestate(self):
1191 def basestate(self):
1192 lastrev, rev = self._wcrevs()
1192 lastrev, rev = self._wcrevs()
1193 if lastrev != rev:
1193 if lastrev != rev:
1194 # Last committed rev is not the same than rev. We would
1194 # Last committed rev is not the same than rev. We would
1195 # like to take lastrev but we do not know if the subrepo
1195 # like to take lastrev but we do not know if the subrepo
1196 # URL exists at lastrev. Test it and fallback to rev it
1196 # URL exists at lastrev. Test it and fallback to rev it
1197 # is not there.
1197 # is not there.
1198 try:
1198 try:
1199 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1199 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1200 return lastrev
1200 return lastrev
1201 except error.Abort:
1201 except error.Abort:
1202 pass
1202 pass
1203 return rev
1203 return rev
1204
1204
1205 @annotatesubrepoerror
1205 @annotatesubrepoerror
1206 def commit(self, text, user, date):
1206 def commit(self, text, user, date):
1207 # user and date are out of our hands since svn is centralized
1207 # user and date are out of our hands since svn is centralized
1208 changed, extchanged, missing = self._wcchanged()
1208 changed, extchanged, missing = self._wcchanged()
1209 if not changed:
1209 if not changed:
1210 return self.basestate()
1210 return self.basestate()
1211 if extchanged:
1211 if extchanged:
1212 # Do not try to commit externals
1212 # Do not try to commit externals
1213 raise error.Abort(_('cannot commit svn externals'))
1213 raise error.Abort(_('cannot commit svn externals'))
1214 if missing:
1214 if missing:
1215 # svn can commit with missing entries but aborting like hg
1215 # svn can commit with missing entries but aborting like hg
1216 # seems a better approach.
1216 # seems a better approach.
1217 raise error.Abort(_('cannot commit missing svn entries'))
1217 raise error.Abort(_('cannot commit missing svn entries'))
1218 commitinfo, err = self._svncommand(['commit', '-m', text])
1218 commitinfo, err = self._svncommand(['commit', '-m', text])
1219 self.ui.status(commitinfo)
1219 self.ui.status(commitinfo)
1220 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1220 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1221 if not newrev:
1221 if not newrev:
1222 if not commitinfo.strip():
1222 if not commitinfo.strip():
1223 # Sometimes, our definition of "changed" differs from
1223 # Sometimes, our definition of "changed" differs from
1224 # svn one. For instance, svn ignores missing files
1224 # svn one. For instance, svn ignores missing files
1225 # when committing. If there are only missing files, no
1225 # when committing. If there are only missing files, no
1226 # commit is made, no output and no error code.
1226 # commit is made, no output and no error code.
1227 raise error.Abort(_('failed to commit svn changes'))
1227 raise error.Abort(_('failed to commit svn changes'))
1228 raise error.Abort(commitinfo.splitlines()[-1])
1228 raise error.Abort(commitinfo.splitlines()[-1])
1229 newrev = newrev.groups()[0]
1229 newrev = newrev.groups()[0]
1230 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1230 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1231 return newrev
1231 return newrev
1232
1232
1233 @annotatesubrepoerror
1233 @annotatesubrepoerror
1234 def remove(self):
1234 def remove(self):
1235 if self.dirty():
1235 if self.dirty():
1236 self.ui.warn(_('not removing repo %s because '
1236 self.ui.warn(_('not removing repo %s because '
1237 'it has changes.\n') % self._path)
1237 'it has changes.\n') % self._path)
1238 return
1238 return
1239 self.ui.note(_('removing subrepo %s\n') % self._path)
1239 self.ui.note(_('removing subrepo %s\n') % self._path)
1240
1240
1241 self.wvfs.rmtree(forcibly=True)
1241 self.wvfs.rmtree(forcibly=True)
1242 try:
1242 try:
1243 pwvfs = self._ctx.repo().wvfs
1243 pwvfs = self._ctx.repo().wvfs
1244 pwvfs.removedirs(pwvfs.dirname(self._path))
1244 pwvfs.removedirs(pwvfs.dirname(self._path))
1245 except OSError:
1245 except OSError:
1246 pass
1246 pass
1247
1247
1248 @annotatesubrepoerror
1248 @annotatesubrepoerror
1249 def get(self, state, overwrite=False):
1249 def get(self, state, overwrite=False):
1250 if overwrite:
1250 if overwrite:
1251 self._svncommand(['revert', '--recursive'])
1251 self._svncommand(['revert', '--recursive'])
1252 args = ['checkout']
1252 args = ['checkout']
1253 if self._svnversion >= (1, 5):
1253 if self._svnversion >= (1, 5):
1254 args.append('--force')
1254 args.append('--force')
1255 # The revision must be specified at the end of the URL to properly
1255 # The revision must be specified at the end of the URL to properly
1256 # update to a directory which has since been deleted and recreated.
1256 # update to a directory which has since been deleted and recreated.
1257 args.append('%s@%s' % (state[0], state[1]))
1257 args.append('%s@%s' % (state[0], state[1]))
1258 status, err = self._svncommand(args, failok=True)
1258 status, err = self._svncommand(args, failok=True)
1259 _sanitize(self.ui, self.wvfs, '.svn')
1259 _sanitize(self.ui, self.wvfs, '.svn')
1260 if not re.search('Checked out revision [0-9]+.', status):
1260 if not re.search('Checked out revision [0-9]+.', status):
1261 if ('is already a working copy for a different URL' in err
1261 if ('is already a working copy for a different URL' in err
1262 and (self._wcchanged()[:2] == (False, False))):
1262 and (self._wcchanged()[:2] == (False, False))):
1263 # obstructed but clean working copy, so just blow it away.
1263 # obstructed but clean working copy, so just blow it away.
1264 self.remove()
1264 self.remove()
1265 self.get(state, overwrite=False)
1265 self.get(state, overwrite=False)
1266 return
1266 return
1267 raise error.Abort((status or err).splitlines()[-1])
1267 raise error.Abort((status or err).splitlines()[-1])
1268 self.ui.status(status)
1268 self.ui.status(status)
1269
1269
1270 @annotatesubrepoerror
1270 @annotatesubrepoerror
1271 def merge(self, state):
1271 def merge(self, state):
1272 old = self._state[1]
1272 old = self._state[1]
1273 new = state[1]
1273 new = state[1]
1274 wcrev = self._wcrev()
1274 wcrev = self._wcrev()
1275 if new != wcrev:
1275 if new != wcrev:
1276 dirty = old == wcrev or self._wcchanged()[0]
1276 dirty = old == wcrev or self._wcchanged()[0]
1277 if _updateprompt(self.ui, self, dirty, wcrev, new):
1277 if _updateprompt(self.ui, self, dirty, wcrev, new):
1278 self.get(state, False)
1278 self.get(state, False)
1279
1279
1280 def push(self, opts):
1280 def push(self, opts):
1281 # push is a no-op for SVN
1281 # push is a no-op for SVN
1282 return True
1282 return True
1283
1283
1284 @annotatesubrepoerror
1284 @annotatesubrepoerror
1285 def files(self):
1285 def files(self):
1286 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1286 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1287 doc = xml.dom.minidom.parseString(output)
1287 doc = xml.dom.minidom.parseString(output)
1288 paths = []
1288 paths = []
1289 for e in doc.getElementsByTagName('entry'):
1289 for e in doc.getElementsByTagName('entry'):
1290 kind = str(e.getAttribute('kind'))
1290 kind = str(e.getAttribute('kind'))
1291 if kind != 'file':
1291 if kind != 'file':
1292 continue
1292 continue
1293 name = ''.join(c.data for c
1293 name = ''.join(c.data for c
1294 in e.getElementsByTagName('name')[0].childNodes
1294 in e.getElementsByTagName('name')[0].childNodes
1295 if c.nodeType == c.TEXT_NODE)
1295 if c.nodeType == c.TEXT_NODE)
1296 paths.append(name.encode('utf-8'))
1296 paths.append(name.encode('utf-8'))
1297 return paths
1297 return paths
1298
1298
1299 def filedata(self, name, decode):
1299 def filedata(self, name, decode):
1300 return self._svncommand(['cat'], name)[0]
1300 return self._svncommand(['cat'], name)[0]
1301
1301
1302
1302
1303 class gitsubrepo(abstractsubrepo):
1303 class gitsubrepo(abstractsubrepo):
1304 def __init__(self, ctx, path, state, allowcreate):
1304 def __init__(self, ctx, path, state, allowcreate):
1305 super(gitsubrepo, self).__init__(ctx, path)
1305 super(gitsubrepo, self).__init__(ctx, path)
1306 self._state = state
1306 self._state = state
1307 self._abspath = ctx.repo().wjoin(path)
1307 self._abspath = ctx.repo().wjoin(path)
1308 self._subparent = ctx.repo()
1308 self._subparent = ctx.repo()
1309 self._ensuregit()
1309 self._ensuregit()
1310
1310
1311 def _ensuregit(self):
1311 def _ensuregit(self):
1312 try:
1312 try:
1313 self._gitexecutable = 'git'
1313 self._gitexecutable = 'git'
1314 out, err = self._gitnodir(['--version'])
1314 out, err = self._gitnodir(['--version'])
1315 except OSError as e:
1315 except OSError as e:
1316 genericerror = _("error executing git for subrepo '%s': %s")
1316 genericerror = _("error executing git for subrepo '%s': %s")
1317 notfoundhint = _("check git is installed and in your PATH")
1317 notfoundhint = _("check git is installed and in your PATH")
1318 if e.errno != errno.ENOENT:
1318 if e.errno != errno.ENOENT:
1319 raise error.Abort(genericerror % (self._path, e.strerror))
1319 raise error.Abort(genericerror % (self._path, e.strerror))
1320 elif pycompat.osname == 'nt':
1320 elif pycompat.osname == 'nt':
1321 try:
1321 try:
1322 self._gitexecutable = 'git.cmd'
1322 self._gitexecutable = 'git.cmd'
1323 out, err = self._gitnodir(['--version'])
1323 out, err = self._gitnodir(['--version'])
1324 except OSError as e2:
1324 except OSError as e2:
1325 if e2.errno == errno.ENOENT:
1325 if e2.errno == errno.ENOENT:
1326 raise error.Abort(_("couldn't find 'git' or 'git.cmd'"
1326 raise error.Abort(_("couldn't find 'git' or 'git.cmd'"
1327 " for subrepo '%s'") % self._path,
1327 " for subrepo '%s'") % self._path,
1328 hint=notfoundhint)
1328 hint=notfoundhint)
1329 else:
1329 else:
1330 raise error.Abort(genericerror % (self._path,
1330 raise error.Abort(genericerror % (self._path,
1331 e2.strerror))
1331 e2.strerror))
1332 else:
1332 else:
1333 raise error.Abort(_("couldn't find git for subrepo '%s'")
1333 raise error.Abort(_("couldn't find git for subrepo '%s'")
1334 % self._path, hint=notfoundhint)
1334 % self._path, hint=notfoundhint)
1335 versionstatus = self._checkversion(out)
1335 versionstatus = self._checkversion(out)
1336 if versionstatus == 'unknown':
1336 if versionstatus == 'unknown':
1337 self.ui.warn(_('cannot retrieve git version\n'))
1337 self.ui.warn(_('cannot retrieve git version\n'))
1338 elif versionstatus == 'abort':
1338 elif versionstatus == 'abort':
1339 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1339 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1340 elif versionstatus == 'warning':
1340 elif versionstatus == 'warning':
1341 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1341 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1342
1342
1343 @staticmethod
1343 @staticmethod
1344 def _gitversion(out):
1344 def _gitversion(out):
1345 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
1345 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
1346 if m:
1346 if m:
1347 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1347 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1348
1348
1349 m = re.search(r'^git version (\d+)\.(\d+)', out)
1349 m = re.search(r'^git version (\d+)\.(\d+)', out)
1350 if m:
1350 if m:
1351 return (int(m.group(1)), int(m.group(2)), 0)
1351 return (int(m.group(1)), int(m.group(2)), 0)
1352
1352
1353 return -1
1353 return -1
1354
1354
1355 @staticmethod
1355 @staticmethod
1356 def _checkversion(out):
1356 def _checkversion(out):
1357 '''ensure git version is new enough
1357 '''ensure git version is new enough
1358
1358
1359 >>> _checkversion = gitsubrepo._checkversion
1359 >>> _checkversion = gitsubrepo._checkversion
1360 >>> _checkversion('git version 1.6.0')
1360 >>> _checkversion('git version 1.6.0')
1361 'ok'
1361 'ok'
1362 >>> _checkversion('git version 1.8.5')
1362 >>> _checkversion('git version 1.8.5')
1363 'ok'
1363 'ok'
1364 >>> _checkversion('git version 1.4.0')
1364 >>> _checkversion('git version 1.4.0')
1365 'abort'
1365 'abort'
1366 >>> _checkversion('git version 1.5.0')
1366 >>> _checkversion('git version 1.5.0')
1367 'warning'
1367 'warning'
1368 >>> _checkversion('git version 1.9-rc0')
1368 >>> _checkversion('git version 1.9-rc0')
1369 'ok'
1369 'ok'
1370 >>> _checkversion('git version 1.9.0.265.g81cdec2')
1370 >>> _checkversion('git version 1.9.0.265.g81cdec2')
1371 'ok'
1371 'ok'
1372 >>> _checkversion('git version 1.9.0.GIT')
1372 >>> _checkversion('git version 1.9.0.GIT')
1373 'ok'
1373 'ok'
1374 >>> _checkversion('git version 12345')
1374 >>> _checkversion('git version 12345')
1375 'unknown'
1375 'unknown'
1376 >>> _checkversion('no')
1376 >>> _checkversion('no')
1377 'unknown'
1377 'unknown'
1378 '''
1378 '''
1379 version = gitsubrepo._gitversion(out)
1379 version = gitsubrepo._gitversion(out)
1380 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1380 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1381 # despite the docstring comment. For now, error on 1.4.0, warn on
1381 # despite the docstring comment. For now, error on 1.4.0, warn on
1382 # 1.5.0 but attempt to continue.
1382 # 1.5.0 but attempt to continue.
1383 if version == -1:
1383 if version == -1:
1384 return 'unknown'
1384 return 'unknown'
1385 if version < (1, 5, 0):
1385 if version < (1, 5, 0):
1386 return 'abort'
1386 return 'abort'
1387 elif version < (1, 6, 0):
1387 elif version < (1, 6, 0):
1388 return 'warning'
1388 return 'warning'
1389 return 'ok'
1389 return 'ok'
1390
1390
1391 def _gitcommand(self, commands, env=None, stream=False):
1391 def _gitcommand(self, commands, env=None, stream=False):
1392 return self._gitdir(commands, env=env, stream=stream)[0]
1392 return self._gitdir(commands, env=env, stream=stream)[0]
1393
1393
1394 def _gitdir(self, commands, env=None, stream=False):
1394 def _gitdir(self, commands, env=None, stream=False):
1395 return self._gitnodir(commands, env=env, stream=stream,
1395 return self._gitnodir(commands, env=env, stream=stream,
1396 cwd=self._abspath)
1396 cwd=self._abspath)
1397
1397
1398 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1398 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1399 """Calls the git command
1399 """Calls the git command
1400
1400
1401 The methods tries to call the git command. versions prior to 1.6.0
1401 The methods tries to call the git command. versions prior to 1.6.0
1402 are not supported and very probably fail.
1402 are not supported and very probably fail.
1403 """
1403 """
1404 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1404 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1405 if env is None:
1405 if env is None:
1406 env = encoding.environ.copy()
1406 env = encoding.environ.copy()
1407 # disable localization for Git output (issue5176)
1407 # disable localization for Git output (issue5176)
1408 env['LC_ALL'] = 'C'
1408 env['LC_ALL'] = 'C'
1409 # fix for Git CVE-2015-7545
1409 # fix for Git CVE-2015-7545
1410 if 'GIT_ALLOW_PROTOCOL' not in env:
1410 if 'GIT_ALLOW_PROTOCOL' not in env:
1411 env['GIT_ALLOW_PROTOCOL'] = 'file:git:http:https:ssh'
1411 env['GIT_ALLOW_PROTOCOL'] = 'file:git:http:https:ssh'
1412 # unless ui.quiet is set, print git's stderr,
1412 # unless ui.quiet is set, print git's stderr,
1413 # which is mostly progress and useful info
1413 # which is mostly progress and useful info
1414 errpipe = None
1414 errpipe = None
1415 if self.ui.quiet:
1415 if self.ui.quiet:
1416 errpipe = open(os.devnull, 'w')
1416 errpipe = open(os.devnull, 'w')
1417 if self.ui._colormode and len(commands) and commands[0] == "diff":
1418 # insert the argument in the front,
1419 # the end of git diff arguments is used for paths
1420 commands.insert(1, '--color')
1417 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1421 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1418 cwd=cwd, env=env, close_fds=util.closefds,
1422 cwd=cwd, env=env, close_fds=util.closefds,
1419 stdout=subprocess.PIPE, stderr=errpipe)
1423 stdout=subprocess.PIPE, stderr=errpipe)
1420 if stream:
1424 if stream:
1421 return p.stdout, None
1425 return p.stdout, None
1422
1426
1423 retdata = p.stdout.read().strip()
1427 retdata = p.stdout.read().strip()
1424 # wait for the child to exit to avoid race condition.
1428 # wait for the child to exit to avoid race condition.
1425 p.wait()
1429 p.wait()
1426
1430
1427 if p.returncode != 0 and p.returncode != 1:
1431 if p.returncode != 0 and p.returncode != 1:
1428 # there are certain error codes that are ok
1432 # there are certain error codes that are ok
1429 command = commands[0]
1433 command = commands[0]
1430 if command in ('cat-file', 'symbolic-ref'):
1434 if command in ('cat-file', 'symbolic-ref'):
1431 return retdata, p.returncode
1435 return retdata, p.returncode
1432 # for all others, abort
1436 # for all others, abort
1433 raise error.Abort(_('git %s error %d in %s') %
1437 raise error.Abort(_('git %s error %d in %s') %
1434 (command, p.returncode, self._relpath))
1438 (command, p.returncode, self._relpath))
1435
1439
1436 return retdata, p.returncode
1440 return retdata, p.returncode
1437
1441
1438 def _gitmissing(self):
1442 def _gitmissing(self):
1439 return not self.wvfs.exists('.git')
1443 return not self.wvfs.exists('.git')
1440
1444
1441 def _gitstate(self):
1445 def _gitstate(self):
1442 return self._gitcommand(['rev-parse', 'HEAD'])
1446 return self._gitcommand(['rev-parse', 'HEAD'])
1443
1447
1444 def _gitcurrentbranch(self):
1448 def _gitcurrentbranch(self):
1445 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1449 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1446 if err:
1450 if err:
1447 current = None
1451 current = None
1448 return current
1452 return current
1449
1453
1450 def _gitremote(self, remote):
1454 def _gitremote(self, remote):
1451 out = self._gitcommand(['remote', 'show', '-n', remote])
1455 out = self._gitcommand(['remote', 'show', '-n', remote])
1452 line = out.split('\n')[1]
1456 line = out.split('\n')[1]
1453 i = line.index('URL: ') + len('URL: ')
1457 i = line.index('URL: ') + len('URL: ')
1454 return line[i:]
1458 return line[i:]
1455
1459
1456 def _githavelocally(self, revision):
1460 def _githavelocally(self, revision):
1457 out, code = self._gitdir(['cat-file', '-e', revision])
1461 out, code = self._gitdir(['cat-file', '-e', revision])
1458 return code == 0
1462 return code == 0
1459
1463
1460 def _gitisancestor(self, r1, r2):
1464 def _gitisancestor(self, r1, r2):
1461 base = self._gitcommand(['merge-base', r1, r2])
1465 base = self._gitcommand(['merge-base', r1, r2])
1462 return base == r1
1466 return base == r1
1463
1467
1464 def _gitisbare(self):
1468 def _gitisbare(self):
1465 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1469 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1466
1470
1467 def _gitupdatestat(self):
1471 def _gitupdatestat(self):
1468 """This must be run before git diff-index.
1472 """This must be run before git diff-index.
1469 diff-index only looks at changes to file stat;
1473 diff-index only looks at changes to file stat;
1470 this command looks at file contents and updates the stat."""
1474 this command looks at file contents and updates the stat."""
1471 self._gitcommand(['update-index', '-q', '--refresh'])
1475 self._gitcommand(['update-index', '-q', '--refresh'])
1472
1476
1473 def _gitbranchmap(self):
1477 def _gitbranchmap(self):
1474 '''returns 2 things:
1478 '''returns 2 things:
1475 a map from git branch to revision
1479 a map from git branch to revision
1476 a map from revision to branches'''
1480 a map from revision to branches'''
1477 branch2rev = {}
1481 branch2rev = {}
1478 rev2branch = {}
1482 rev2branch = {}
1479
1483
1480 out = self._gitcommand(['for-each-ref', '--format',
1484 out = self._gitcommand(['for-each-ref', '--format',
1481 '%(objectname) %(refname)'])
1485 '%(objectname) %(refname)'])
1482 for line in out.split('\n'):
1486 for line in out.split('\n'):
1483 revision, ref = line.split(' ')
1487 revision, ref = line.split(' ')
1484 if (not ref.startswith('refs/heads/') and
1488 if (not ref.startswith('refs/heads/') and
1485 not ref.startswith('refs/remotes/')):
1489 not ref.startswith('refs/remotes/')):
1486 continue
1490 continue
1487 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1491 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1488 continue # ignore remote/HEAD redirects
1492 continue # ignore remote/HEAD redirects
1489 branch2rev[ref] = revision
1493 branch2rev[ref] = revision
1490 rev2branch.setdefault(revision, []).append(ref)
1494 rev2branch.setdefault(revision, []).append(ref)
1491 return branch2rev, rev2branch
1495 return branch2rev, rev2branch
1492
1496
1493 def _gittracking(self, branches):
1497 def _gittracking(self, branches):
1494 'return map of remote branch to local tracking branch'
1498 'return map of remote branch to local tracking branch'
1495 # assumes no more than one local tracking branch for each remote
1499 # assumes no more than one local tracking branch for each remote
1496 tracking = {}
1500 tracking = {}
1497 for b in branches:
1501 for b in branches:
1498 if b.startswith('refs/remotes/'):
1502 if b.startswith('refs/remotes/'):
1499 continue
1503 continue
1500 bname = b.split('/', 2)[2]
1504 bname = b.split('/', 2)[2]
1501 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1505 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1502 if remote:
1506 if remote:
1503 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1507 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1504 tracking['refs/remotes/%s/%s' %
1508 tracking['refs/remotes/%s/%s' %
1505 (remote, ref.split('/', 2)[2])] = b
1509 (remote, ref.split('/', 2)[2])] = b
1506 return tracking
1510 return tracking
1507
1511
1508 def _abssource(self, source):
1512 def _abssource(self, source):
1509 if '://' not in source:
1513 if '://' not in source:
1510 # recognize the scp syntax as an absolute source
1514 # recognize the scp syntax as an absolute source
1511 colon = source.find(':')
1515 colon = source.find(':')
1512 if colon != -1 and '/' not in source[:colon]:
1516 if colon != -1 and '/' not in source[:colon]:
1513 return source
1517 return source
1514 self._subsource = source
1518 self._subsource = source
1515 return _abssource(self)
1519 return _abssource(self)
1516
1520
1517 def _fetch(self, source, revision):
1521 def _fetch(self, source, revision):
1518 if self._gitmissing():
1522 if self._gitmissing():
1519 source = self._abssource(source)
1523 source = self._abssource(source)
1520 self.ui.status(_('cloning subrepo %s from %s\n') %
1524 self.ui.status(_('cloning subrepo %s from %s\n') %
1521 (self._relpath, source))
1525 (self._relpath, source))
1522 self._gitnodir(['clone', source, self._abspath])
1526 self._gitnodir(['clone', source, self._abspath])
1523 if self._githavelocally(revision):
1527 if self._githavelocally(revision):
1524 return
1528 return
1525 self.ui.status(_('pulling subrepo %s from %s\n') %
1529 self.ui.status(_('pulling subrepo %s from %s\n') %
1526 (self._relpath, self._gitremote('origin')))
1530 (self._relpath, self._gitremote('origin')))
1527 # try only origin: the originally cloned repo
1531 # try only origin: the originally cloned repo
1528 self._gitcommand(['fetch'])
1532 self._gitcommand(['fetch'])
1529 if not self._githavelocally(revision):
1533 if not self._githavelocally(revision):
1530 raise error.Abort(_("revision %s does not exist in subrepo %s\n") %
1534 raise error.Abort(_("revision %s does not exist in subrepo %s\n") %
1531 (revision, self._relpath))
1535 (revision, self._relpath))
1532
1536
1533 @annotatesubrepoerror
1537 @annotatesubrepoerror
1534 def dirty(self, ignoreupdate=False):
1538 def dirty(self, ignoreupdate=False):
1535 if self._gitmissing():
1539 if self._gitmissing():
1536 return self._state[1] != ''
1540 return self._state[1] != ''
1537 if self._gitisbare():
1541 if self._gitisbare():
1538 return True
1542 return True
1539 if not ignoreupdate and self._state[1] != self._gitstate():
1543 if not ignoreupdate and self._state[1] != self._gitstate():
1540 # different version checked out
1544 # different version checked out
1541 return True
1545 return True
1542 # check for staged changes or modified files; ignore untracked files
1546 # check for staged changes or modified files; ignore untracked files
1543 self._gitupdatestat()
1547 self._gitupdatestat()
1544 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1548 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1545 return code == 1
1549 return code == 1
1546
1550
1547 def basestate(self):
1551 def basestate(self):
1548 return self._gitstate()
1552 return self._gitstate()
1549
1553
1550 @annotatesubrepoerror
1554 @annotatesubrepoerror
1551 def get(self, state, overwrite=False):
1555 def get(self, state, overwrite=False):
1552 source, revision, kind = state
1556 source, revision, kind = state
1553 if not revision:
1557 if not revision:
1554 self.remove()
1558 self.remove()
1555 return
1559 return
1556 self._fetch(source, revision)
1560 self._fetch(source, revision)
1557 # if the repo was set to be bare, unbare it
1561 # if the repo was set to be bare, unbare it
1558 if self._gitisbare():
1562 if self._gitisbare():
1559 self._gitcommand(['config', 'core.bare', 'false'])
1563 self._gitcommand(['config', 'core.bare', 'false'])
1560 if self._gitstate() == revision:
1564 if self._gitstate() == revision:
1561 self._gitcommand(['reset', '--hard', 'HEAD'])
1565 self._gitcommand(['reset', '--hard', 'HEAD'])
1562 return
1566 return
1563 elif self._gitstate() == revision:
1567 elif self._gitstate() == revision:
1564 if overwrite:
1568 if overwrite:
1565 # first reset the index to unmark new files for commit, because
1569 # first reset the index to unmark new files for commit, because
1566 # reset --hard will otherwise throw away files added for commit,
1570 # reset --hard will otherwise throw away files added for commit,
1567 # not just unmark them.
1571 # not just unmark them.
1568 self._gitcommand(['reset', 'HEAD'])
1572 self._gitcommand(['reset', 'HEAD'])
1569 self._gitcommand(['reset', '--hard', 'HEAD'])
1573 self._gitcommand(['reset', '--hard', 'HEAD'])
1570 return
1574 return
1571 branch2rev, rev2branch = self._gitbranchmap()
1575 branch2rev, rev2branch = self._gitbranchmap()
1572
1576
1573 def checkout(args):
1577 def checkout(args):
1574 cmd = ['checkout']
1578 cmd = ['checkout']
1575 if overwrite:
1579 if overwrite:
1576 # first reset the index to unmark new files for commit, because
1580 # first reset the index to unmark new files for commit, because
1577 # the -f option will otherwise throw away files added for
1581 # the -f option will otherwise throw away files added for
1578 # commit, not just unmark them.
1582 # commit, not just unmark them.
1579 self._gitcommand(['reset', 'HEAD'])
1583 self._gitcommand(['reset', 'HEAD'])
1580 cmd.append('-f')
1584 cmd.append('-f')
1581 self._gitcommand(cmd + args)
1585 self._gitcommand(cmd + args)
1582 _sanitize(self.ui, self.wvfs, '.git')
1586 _sanitize(self.ui, self.wvfs, '.git')
1583
1587
1584 def rawcheckout():
1588 def rawcheckout():
1585 # no branch to checkout, check it out with no branch
1589 # no branch to checkout, check it out with no branch
1586 self.ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1590 self.ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1587 self._relpath)
1591 self._relpath)
1588 self.ui.warn(_('check out a git branch if you intend '
1592 self.ui.warn(_('check out a git branch if you intend '
1589 'to make changes\n'))
1593 'to make changes\n'))
1590 checkout(['-q', revision])
1594 checkout(['-q', revision])
1591
1595
1592 if revision not in rev2branch:
1596 if revision not in rev2branch:
1593 rawcheckout()
1597 rawcheckout()
1594 return
1598 return
1595 branches = rev2branch[revision]
1599 branches = rev2branch[revision]
1596 firstlocalbranch = None
1600 firstlocalbranch = None
1597 for b in branches:
1601 for b in branches:
1598 if b == 'refs/heads/master':
1602 if b == 'refs/heads/master':
1599 # master trumps all other branches
1603 # master trumps all other branches
1600 checkout(['refs/heads/master'])
1604 checkout(['refs/heads/master'])
1601 return
1605 return
1602 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1606 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1603 firstlocalbranch = b
1607 firstlocalbranch = b
1604 if firstlocalbranch:
1608 if firstlocalbranch:
1605 checkout([firstlocalbranch])
1609 checkout([firstlocalbranch])
1606 return
1610 return
1607
1611
1608 tracking = self._gittracking(branch2rev.keys())
1612 tracking = self._gittracking(branch2rev.keys())
1609 # choose a remote branch already tracked if possible
1613 # choose a remote branch already tracked if possible
1610 remote = branches[0]
1614 remote = branches[0]
1611 if remote not in tracking:
1615 if remote not in tracking:
1612 for b in branches:
1616 for b in branches:
1613 if b in tracking:
1617 if b in tracking:
1614 remote = b
1618 remote = b
1615 break
1619 break
1616
1620
1617 if remote not in tracking:
1621 if remote not in tracking:
1618 # create a new local tracking branch
1622 # create a new local tracking branch
1619 local = remote.split('/', 3)[3]
1623 local = remote.split('/', 3)[3]
1620 checkout(['-b', local, remote])
1624 checkout(['-b', local, remote])
1621 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1625 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1622 # When updating to a tracked remote branch,
1626 # When updating to a tracked remote branch,
1623 # if the local tracking branch is downstream of it,
1627 # if the local tracking branch is downstream of it,
1624 # a normal `git pull` would have performed a "fast-forward merge"
1628 # a normal `git pull` would have performed a "fast-forward merge"
1625 # which is equivalent to updating the local branch to the remote.
1629 # which is equivalent to updating the local branch to the remote.
1626 # Since we are only looking at branching at update, we need to
1630 # Since we are only looking at branching at update, we need to
1627 # detect this situation and perform this action lazily.
1631 # detect this situation and perform this action lazily.
1628 if tracking[remote] != self._gitcurrentbranch():
1632 if tracking[remote] != self._gitcurrentbranch():
1629 checkout([tracking[remote]])
1633 checkout([tracking[remote]])
1630 self._gitcommand(['merge', '--ff', remote])
1634 self._gitcommand(['merge', '--ff', remote])
1631 _sanitize(self.ui, self.wvfs, '.git')
1635 _sanitize(self.ui, self.wvfs, '.git')
1632 else:
1636 else:
1633 # a real merge would be required, just checkout the revision
1637 # a real merge would be required, just checkout the revision
1634 rawcheckout()
1638 rawcheckout()
1635
1639
1636 @annotatesubrepoerror
1640 @annotatesubrepoerror
1637 def commit(self, text, user, date):
1641 def commit(self, text, user, date):
1638 if self._gitmissing():
1642 if self._gitmissing():
1639 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1643 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1640 cmd = ['commit', '-a', '-m', text]
1644 cmd = ['commit', '-a', '-m', text]
1641 env = encoding.environ.copy()
1645 env = encoding.environ.copy()
1642 if user:
1646 if user:
1643 cmd += ['--author', user]
1647 cmd += ['--author', user]
1644 if date:
1648 if date:
1645 # git's date parser silently ignores when seconds < 1e9
1649 # git's date parser silently ignores when seconds < 1e9
1646 # convert to ISO8601
1650 # convert to ISO8601
1647 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1651 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1648 '%Y-%m-%dT%H:%M:%S %1%2')
1652 '%Y-%m-%dT%H:%M:%S %1%2')
1649 self._gitcommand(cmd, env=env)
1653 self._gitcommand(cmd, env=env)
1650 # make sure commit works otherwise HEAD might not exist under certain
1654 # make sure commit works otherwise HEAD might not exist under certain
1651 # circumstances
1655 # circumstances
1652 return self._gitstate()
1656 return self._gitstate()
1653
1657
1654 @annotatesubrepoerror
1658 @annotatesubrepoerror
1655 def merge(self, state):
1659 def merge(self, state):
1656 source, revision, kind = state
1660 source, revision, kind = state
1657 self._fetch(source, revision)
1661 self._fetch(source, revision)
1658 base = self._gitcommand(['merge-base', revision, self._state[1]])
1662 base = self._gitcommand(['merge-base', revision, self._state[1]])
1659 self._gitupdatestat()
1663 self._gitupdatestat()
1660 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1664 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1661
1665
1662 def mergefunc():
1666 def mergefunc():
1663 if base == revision:
1667 if base == revision:
1664 self.get(state) # fast forward merge
1668 self.get(state) # fast forward merge
1665 elif base != self._state[1]:
1669 elif base != self._state[1]:
1666 self._gitcommand(['merge', '--no-commit', revision])
1670 self._gitcommand(['merge', '--no-commit', revision])
1667 _sanitize(self.ui, self.wvfs, '.git')
1671 _sanitize(self.ui, self.wvfs, '.git')
1668
1672
1669 if self.dirty():
1673 if self.dirty():
1670 if self._gitstate() != revision:
1674 if self._gitstate() != revision:
1671 dirty = self._gitstate() == self._state[1] or code != 0
1675 dirty = self._gitstate() == self._state[1] or code != 0
1672 if _updateprompt(self.ui, self, dirty,
1676 if _updateprompt(self.ui, self, dirty,
1673 self._state[1][:7], revision[:7]):
1677 self._state[1][:7], revision[:7]):
1674 mergefunc()
1678 mergefunc()
1675 else:
1679 else:
1676 mergefunc()
1680 mergefunc()
1677
1681
1678 @annotatesubrepoerror
1682 @annotatesubrepoerror
1679 def push(self, opts):
1683 def push(self, opts):
1680 force = opts.get('force')
1684 force = opts.get('force')
1681
1685
1682 if not self._state[1]:
1686 if not self._state[1]:
1683 return True
1687 return True
1684 if self._gitmissing():
1688 if self._gitmissing():
1685 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1689 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1686 # if a branch in origin contains the revision, nothing to do
1690 # if a branch in origin contains the revision, nothing to do
1687 branch2rev, rev2branch = self._gitbranchmap()
1691 branch2rev, rev2branch = self._gitbranchmap()
1688 if self._state[1] in rev2branch:
1692 if self._state[1] in rev2branch:
1689 for b in rev2branch[self._state[1]]:
1693 for b in rev2branch[self._state[1]]:
1690 if b.startswith('refs/remotes/origin/'):
1694 if b.startswith('refs/remotes/origin/'):
1691 return True
1695 return True
1692 for b, revision in branch2rev.iteritems():
1696 for b, revision in branch2rev.iteritems():
1693 if b.startswith('refs/remotes/origin/'):
1697 if b.startswith('refs/remotes/origin/'):
1694 if self._gitisancestor(self._state[1], revision):
1698 if self._gitisancestor(self._state[1], revision):
1695 return True
1699 return True
1696 # otherwise, try to push the currently checked out branch
1700 # otherwise, try to push the currently checked out branch
1697 cmd = ['push']
1701 cmd = ['push']
1698 if force:
1702 if force:
1699 cmd.append('--force')
1703 cmd.append('--force')
1700
1704
1701 current = self._gitcurrentbranch()
1705 current = self._gitcurrentbranch()
1702 if current:
1706 if current:
1703 # determine if the current branch is even useful
1707 # determine if the current branch is even useful
1704 if not self._gitisancestor(self._state[1], current):
1708 if not self._gitisancestor(self._state[1], current):
1705 self.ui.warn(_('unrelated git branch checked out '
1709 self.ui.warn(_('unrelated git branch checked out '
1706 'in subrepo %s\n') % self._relpath)
1710 'in subrepo %s\n') % self._relpath)
1707 return False
1711 return False
1708 self.ui.status(_('pushing branch %s of subrepo %s\n') %
1712 self.ui.status(_('pushing branch %s of subrepo %s\n') %
1709 (current.split('/', 2)[2], self._relpath))
1713 (current.split('/', 2)[2], self._relpath))
1710 ret = self._gitdir(cmd + ['origin', current])
1714 ret = self._gitdir(cmd + ['origin', current])
1711 return ret[1] == 0
1715 return ret[1] == 0
1712 else:
1716 else:
1713 self.ui.warn(_('no branch checked out in subrepo %s\n'
1717 self.ui.warn(_('no branch checked out in subrepo %s\n'
1714 'cannot push revision %s\n') %
1718 'cannot push revision %s\n') %
1715 (self._relpath, self._state[1]))
1719 (self._relpath, self._state[1]))
1716 return False
1720 return False
1717
1721
1718 @annotatesubrepoerror
1722 @annotatesubrepoerror
1719 def add(self, ui, match, prefix, explicitonly, **opts):
1723 def add(self, ui, match, prefix, explicitonly, **opts):
1720 if self._gitmissing():
1724 if self._gitmissing():
1721 return []
1725 return []
1722
1726
1723 (modified, added, removed,
1727 (modified, added, removed,
1724 deleted, unknown, ignored, clean) = self.status(None, unknown=True,
1728 deleted, unknown, ignored, clean) = self.status(None, unknown=True,
1725 clean=True)
1729 clean=True)
1726
1730
1727 tracked = set()
1731 tracked = set()
1728 # dirstates 'amn' warn, 'r' is added again
1732 # dirstates 'amn' warn, 'r' is added again
1729 for l in (modified, added, deleted, clean):
1733 for l in (modified, added, deleted, clean):
1730 tracked.update(l)
1734 tracked.update(l)
1731
1735
1732 # Unknown files not of interest will be rejected by the matcher
1736 # Unknown files not of interest will be rejected by the matcher
1733 files = unknown
1737 files = unknown
1734 files.extend(match.files())
1738 files.extend(match.files())
1735
1739
1736 rejected = []
1740 rejected = []
1737
1741
1738 files = [f for f in sorted(set(files)) if match(f)]
1742 files = [f for f in sorted(set(files)) if match(f)]
1739 for f in files:
1743 for f in files:
1740 exact = match.exact(f)
1744 exact = match.exact(f)
1741 command = ["add"]
1745 command = ["add"]
1742 if exact:
1746 if exact:
1743 command.append("-f") #should be added, even if ignored
1747 command.append("-f") #should be added, even if ignored
1744 if ui.verbose or not exact:
1748 if ui.verbose or not exact:
1745 ui.status(_('adding %s\n') % match.rel(f))
1749 ui.status(_('adding %s\n') % match.rel(f))
1746
1750
1747 if f in tracked: # hg prints 'adding' even if already tracked
1751 if f in tracked: # hg prints 'adding' even if already tracked
1748 if exact:
1752 if exact:
1749 rejected.append(f)
1753 rejected.append(f)
1750 continue
1754 continue
1751 if not opts.get('dry_run'):
1755 if not opts.get('dry_run'):
1752 self._gitcommand(command + [f])
1756 self._gitcommand(command + [f])
1753
1757
1754 for f in rejected:
1758 for f in rejected:
1755 ui.warn(_("%s already tracked!\n") % match.abs(f))
1759 ui.warn(_("%s already tracked!\n") % match.abs(f))
1756
1760
1757 return rejected
1761 return rejected
1758
1762
1759 @annotatesubrepoerror
1763 @annotatesubrepoerror
1760 def remove(self):
1764 def remove(self):
1761 if self._gitmissing():
1765 if self._gitmissing():
1762 return
1766 return
1763 if self.dirty():
1767 if self.dirty():
1764 self.ui.warn(_('not removing repo %s because '
1768 self.ui.warn(_('not removing repo %s because '
1765 'it has changes.\n') % self._relpath)
1769 'it has changes.\n') % self._relpath)
1766 return
1770 return
1767 # we can't fully delete the repository as it may contain
1771 # we can't fully delete the repository as it may contain
1768 # local-only history
1772 # local-only history
1769 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1773 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1770 self._gitcommand(['config', 'core.bare', 'true'])
1774 self._gitcommand(['config', 'core.bare', 'true'])
1771 for f, kind in self.wvfs.readdir():
1775 for f, kind in self.wvfs.readdir():
1772 if f == '.git':
1776 if f == '.git':
1773 continue
1777 continue
1774 if kind == stat.S_IFDIR:
1778 if kind == stat.S_IFDIR:
1775 self.wvfs.rmtree(f)
1779 self.wvfs.rmtree(f)
1776 else:
1780 else:
1777 self.wvfs.unlink(f)
1781 self.wvfs.unlink(f)
1778
1782
1779 def archive(self, archiver, prefix, match=None, decode=True):
1783 def archive(self, archiver, prefix, match=None, decode=True):
1780 total = 0
1784 total = 0
1781 source, revision = self._state
1785 source, revision = self._state
1782 if not revision:
1786 if not revision:
1783 return total
1787 return total
1784 self._fetch(source, revision)
1788 self._fetch(source, revision)
1785
1789
1786 # Parse git's native archive command.
1790 # Parse git's native archive command.
1787 # This should be much faster than manually traversing the trees
1791 # This should be much faster than manually traversing the trees
1788 # and objects with many subprocess calls.
1792 # and objects with many subprocess calls.
1789 tarstream = self._gitcommand(['archive', revision], stream=True)
1793 tarstream = self._gitcommand(['archive', revision], stream=True)
1790 tar = tarfile.open(fileobj=tarstream, mode='r|')
1794 tar = tarfile.open(fileobj=tarstream, mode='r|')
1791 relpath = subrelpath(self)
1795 relpath = subrelpath(self)
1792 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1796 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1793 for i, info in enumerate(tar):
1797 for i, info in enumerate(tar):
1794 if info.isdir():
1798 if info.isdir():
1795 continue
1799 continue
1796 if match and not match(info.name):
1800 if match and not match(info.name):
1797 continue
1801 continue
1798 if info.issym():
1802 if info.issym():
1799 data = info.linkname
1803 data = info.linkname
1800 else:
1804 else:
1801 data = tar.extractfile(info).read()
1805 data = tar.extractfile(info).read()
1802 archiver.addfile(prefix + self._path + '/' + info.name,
1806 archiver.addfile(prefix + self._path + '/' + info.name,
1803 info.mode, info.issym(), data)
1807 info.mode, info.issym(), data)
1804 total += 1
1808 total += 1
1805 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1809 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1806 unit=_('files'))
1810 unit=_('files'))
1807 self.ui.progress(_('archiving (%s)') % relpath, None)
1811 self.ui.progress(_('archiving (%s)') % relpath, None)
1808 return total
1812 return total
1809
1813
1810
1814
1811 @annotatesubrepoerror
1815 @annotatesubrepoerror
1812 def cat(self, match, prefix, **opts):
1816 def cat(self, match, prefix, **opts):
1813 rev = self._state[1]
1817 rev = self._state[1]
1814 if match.anypats():
1818 if match.anypats():
1815 return 1 #No support for include/exclude yet
1819 return 1 #No support for include/exclude yet
1816
1820
1817 if not match.files():
1821 if not match.files():
1818 return 1
1822 return 1
1819
1823
1820 for f in match.files():
1824 for f in match.files():
1821 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1825 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1822 fp = cmdutil.makefileobj(self._subparent, opts.get('output'),
1826 fp = cmdutil.makefileobj(self._subparent, opts.get('output'),
1823 self._ctx.node(),
1827 self._ctx.node(),
1824 pathname=self.wvfs.reljoin(prefix, f))
1828 pathname=self.wvfs.reljoin(prefix, f))
1825 fp.write(output)
1829 fp.write(output)
1826 fp.close()
1830 fp.close()
1827 return 0
1831 return 0
1828
1832
1829
1833
1830 @annotatesubrepoerror
1834 @annotatesubrepoerror
1831 def status(self, rev2, **opts):
1835 def status(self, rev2, **opts):
1832 rev1 = self._state[1]
1836 rev1 = self._state[1]
1833 if self._gitmissing() or not rev1:
1837 if self._gitmissing() or not rev1:
1834 # if the repo is missing, return no results
1838 # if the repo is missing, return no results
1835 return scmutil.status([], [], [], [], [], [], [])
1839 return scmutil.status([], [], [], [], [], [], [])
1836 modified, added, removed = [], [], []
1840 modified, added, removed = [], [], []
1837 self._gitupdatestat()
1841 self._gitupdatestat()
1838 if rev2:
1842 if rev2:
1839 command = ['diff-tree', '--no-renames', '-r', rev1, rev2]
1843 command = ['diff-tree', '--no-renames', '-r', rev1, rev2]
1840 else:
1844 else:
1841 command = ['diff-index', '--no-renames', rev1]
1845 command = ['diff-index', '--no-renames', rev1]
1842 out = self._gitcommand(command)
1846 out = self._gitcommand(command)
1843 for line in out.split('\n'):
1847 for line in out.split('\n'):
1844 tab = line.find('\t')
1848 tab = line.find('\t')
1845 if tab == -1:
1849 if tab == -1:
1846 continue
1850 continue
1847 status, f = line[tab - 1], line[tab + 1:]
1851 status, f = line[tab - 1], line[tab + 1:]
1848 if status == 'M':
1852 if status == 'M':
1849 modified.append(f)
1853 modified.append(f)
1850 elif status == 'A':
1854 elif status == 'A':
1851 added.append(f)
1855 added.append(f)
1852 elif status == 'D':
1856 elif status == 'D':
1853 removed.append(f)
1857 removed.append(f)
1854
1858
1855 deleted, unknown, ignored, clean = [], [], [], []
1859 deleted, unknown, ignored, clean = [], [], [], []
1856
1860
1857 command = ['status', '--porcelain', '-z']
1861 command = ['status', '--porcelain', '-z']
1858 if opts.get('unknown'):
1862 if opts.get('unknown'):
1859 command += ['--untracked-files=all']
1863 command += ['--untracked-files=all']
1860 if opts.get('ignored'):
1864 if opts.get('ignored'):
1861 command += ['--ignored']
1865 command += ['--ignored']
1862 out = self._gitcommand(command)
1866 out = self._gitcommand(command)
1863
1867
1864 changedfiles = set()
1868 changedfiles = set()
1865 changedfiles.update(modified)
1869 changedfiles.update(modified)
1866 changedfiles.update(added)
1870 changedfiles.update(added)
1867 changedfiles.update(removed)
1871 changedfiles.update(removed)
1868 for line in out.split('\0'):
1872 for line in out.split('\0'):
1869 if not line:
1873 if not line:
1870 continue
1874 continue
1871 st = line[0:2]
1875 st = line[0:2]
1872 #moves and copies show 2 files on one line
1876 #moves and copies show 2 files on one line
1873 if line.find('\0') >= 0:
1877 if line.find('\0') >= 0:
1874 filename1, filename2 = line[3:].split('\0')
1878 filename1, filename2 = line[3:].split('\0')
1875 else:
1879 else:
1876 filename1 = line[3:]
1880 filename1 = line[3:]
1877 filename2 = None
1881 filename2 = None
1878
1882
1879 changedfiles.add(filename1)
1883 changedfiles.add(filename1)
1880 if filename2:
1884 if filename2:
1881 changedfiles.add(filename2)
1885 changedfiles.add(filename2)
1882
1886
1883 if st == '??':
1887 if st == '??':
1884 unknown.append(filename1)
1888 unknown.append(filename1)
1885 elif st == '!!':
1889 elif st == '!!':
1886 ignored.append(filename1)
1890 ignored.append(filename1)
1887
1891
1888 if opts.get('clean'):
1892 if opts.get('clean'):
1889 out = self._gitcommand(['ls-files'])
1893 out = self._gitcommand(['ls-files'])
1890 for f in out.split('\n'):
1894 for f in out.split('\n'):
1891 if not f in changedfiles:
1895 if not f in changedfiles:
1892 clean.append(f)
1896 clean.append(f)
1893
1897
1894 return scmutil.status(modified, added, removed, deleted,
1898 return scmutil.status(modified, added, removed, deleted,
1895 unknown, ignored, clean)
1899 unknown, ignored, clean)
1896
1900
1897 @annotatesubrepoerror
1901 @annotatesubrepoerror
1898 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1902 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1899 node1 = self._state[1]
1903 node1 = self._state[1]
1900 cmd = ['diff', '--no-renames']
1904 cmd = ['diff', '--no-renames']
1901 if opts['stat']:
1905 if opts['stat']:
1902 cmd.append('--stat')
1906 cmd.append('--stat')
1903 else:
1907 else:
1904 # for Git, this also implies '-p'
1908 # for Git, this also implies '-p'
1905 cmd.append('-U%d' % diffopts.context)
1909 cmd.append('-U%d' % diffopts.context)
1906
1910
1907 gitprefix = self.wvfs.reljoin(prefix, self._path)
1911 gitprefix = self.wvfs.reljoin(prefix, self._path)
1908
1912
1909 if diffopts.noprefix:
1913 if diffopts.noprefix:
1910 cmd.extend(['--src-prefix=%s/' % gitprefix,
1914 cmd.extend(['--src-prefix=%s/' % gitprefix,
1911 '--dst-prefix=%s/' % gitprefix])
1915 '--dst-prefix=%s/' % gitprefix])
1912 else:
1916 else:
1913 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
1917 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
1914 '--dst-prefix=b/%s/' % gitprefix])
1918 '--dst-prefix=b/%s/' % gitprefix])
1915
1919
1916 if diffopts.ignorews:
1920 if diffopts.ignorews:
1917 cmd.append('--ignore-all-space')
1921 cmd.append('--ignore-all-space')
1918 if diffopts.ignorewsamount:
1922 if diffopts.ignorewsamount:
1919 cmd.append('--ignore-space-change')
1923 cmd.append('--ignore-space-change')
1920 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1924 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1921 and diffopts.ignoreblanklines:
1925 and diffopts.ignoreblanklines:
1922 cmd.append('--ignore-blank-lines')
1926 cmd.append('--ignore-blank-lines')
1923
1927
1924 cmd.append(node1)
1928 cmd.append(node1)
1925 if node2:
1929 if node2:
1926 cmd.append(node2)
1930 cmd.append(node2)
1927
1931
1928 output = ""
1932 output = ""
1929 if match.always():
1933 if match.always():
1930 output += self._gitcommand(cmd) + '\n'
1934 output += self._gitcommand(cmd) + '\n'
1931 else:
1935 else:
1932 st = self.status(node2)[:3]
1936 st = self.status(node2)[:3]
1933 files = [f for sublist in st for f in sublist]
1937 files = [f for sublist in st for f in sublist]
1934 for f in files:
1938 for f in files:
1935 if match(f):
1939 if match(f):
1936 output += self._gitcommand(cmd + ['--', f]) + '\n'
1940 output += self._gitcommand(cmd + ['--', f]) + '\n'
1937
1941
1938 if output.strip():
1942 if output.strip():
1939 ui.write(output)
1943 ui.write(output)
1940
1944
1941 @annotatesubrepoerror
1945 @annotatesubrepoerror
1942 def revert(self, substate, *pats, **opts):
1946 def revert(self, substate, *pats, **opts):
1943 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1947 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1944 if not opts.get('no_backup'):
1948 if not opts.get('no_backup'):
1945 status = self.status(None)
1949 status = self.status(None)
1946 names = status.modified
1950 names = status.modified
1947 for name in names:
1951 for name in names:
1948 bakname = scmutil.origpath(self.ui, self._subparent, name)
1952 bakname = scmutil.origpath(self.ui, self._subparent, name)
1949 self.ui.note(_('saving current version of %s as %s\n') %
1953 self.ui.note(_('saving current version of %s as %s\n') %
1950 (name, bakname))
1954 (name, bakname))
1951 self.wvfs.rename(name, bakname)
1955 self.wvfs.rename(name, bakname)
1952
1956
1953 if not opts.get('dry_run'):
1957 if not opts.get('dry_run'):
1954 self.get(substate, overwrite=True)
1958 self.get(substate, overwrite=True)
1955 return []
1959 return []
1956
1960
1957 def shortid(self, revid):
1961 def shortid(self, revid):
1958 return revid[:7]
1962 return revid[:7]
1959
1963
1960 types = {
1964 types = {
1961 'hg': hgsubrepo,
1965 'hg': hgsubrepo,
1962 'svn': svnsubrepo,
1966 'svn': svnsubrepo,
1963 'git': gitsubrepo,
1967 'git': gitsubrepo,
1964 }
1968 }
General Comments 0
You need to be logged in to leave comments. Login now