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