##// END OF EJS Templates
subrepo: add 'cat' support for git subrepos...
Mathias De Maré -
r23991:07c1a7d1 default
parent child Browse files
Show More
@@ -1,160 +1,160 b''
1 1 Subrepositories let you nest external repositories or projects into a
2 2 parent Mercurial repository, and make commands operate on them as a
3 3 group.
4 4
5 5 Mercurial currently supports Mercurial, Git, and Subversion
6 6 subrepositories.
7 7
8 8 Subrepositories are made of three components:
9 9
10 10 1. Nested repository checkouts. They can appear anywhere in the
11 11 parent working directory.
12 12
13 13 2. Nested repository references. They are defined in ``.hgsub``, which
14 14 should be placed in the root of working directory, and
15 15 tell where the subrepository checkouts come from. Mercurial
16 16 subrepositories are referenced like::
17 17
18 18 path/to/nested = https://example.com/nested/repo/path
19 19
20 20 Git and Subversion subrepos are also supported::
21 21
22 22 path/to/nested = [git]git://example.com/nested/repo/path
23 23 path/to/nested = [svn]https://example.com/nested/trunk/path
24 24
25 25 where ``path/to/nested`` is the checkout location relatively to the
26 26 parent Mercurial root, and ``https://example.com/nested/repo/path``
27 27 is the source repository path. The source can also reference a
28 28 filesystem path.
29 29
30 30 Note that ``.hgsub`` does not exist by default in Mercurial
31 31 repositories, you have to create and add it to the parent
32 32 repository before using subrepositories.
33 33
34 34 3. Nested repository states. They are defined in ``.hgsubstate``, which
35 35 is placed in the root of working directory, and
36 36 capture whatever information is required to restore the
37 37 subrepositories to the state they were committed in a parent
38 38 repository changeset. Mercurial automatically record the nested
39 39 repositories states when committing in the parent repository.
40 40
41 41 .. note::
42 42
43 43 The ``.hgsubstate`` file should not be edited manually.
44 44
45 45
46 46 Adding a Subrepository
47 47 ======================
48 48
49 49 If ``.hgsub`` does not exist, create it and add it to the parent
50 50 repository. Clone or checkout the external projects where you want it
51 51 to live in the parent repository. Edit ``.hgsub`` and add the
52 52 subrepository entry as described above. At this point, the
53 53 subrepository is tracked and the next commit will record its state in
54 54 ``.hgsubstate`` and bind it to the committed changeset.
55 55
56 56 Synchronizing a Subrepository
57 57 =============================
58 58
59 59 Subrepos do not automatically track the latest changeset of their
60 60 sources. Instead, they are updated to the changeset that corresponds
61 61 with the changeset checked out in the top-level changeset. This is so
62 62 developers always get a consistent set of compatible code and
63 63 libraries when they update.
64 64
65 65 Thus, updating subrepos is a manual process. Simply check out target
66 66 subrepo at the desired revision, test in the top-level repo, then
67 67 commit in the parent repository to record the new combination.
68 68
69 69 Deleting a Subrepository
70 70 ========================
71 71
72 72 To remove a subrepository from the parent repository, delete its
73 73 reference from ``.hgsub``, then remove its files.
74 74
75 75 Interaction with Mercurial Commands
76 76 ===================================
77 77
78 78 :add: add does not recurse in subrepos unless -S/--subrepos is
79 79 specified. However, if you specify the full path of a file in a
80 80 subrepo, it will be added even without -S/--subrepos specified.
81 81 Git and Subversion subrepositories are currently silently
82 82 ignored.
83 83
84 84 :addremove: addremove does not recurse into subrepos unless
85 85 -S/--subrepos is specified. However, if you specify the full
86 86 path of a directory in a subrepo, addremove will be performed on
87 87 it even without -S/--subrepos being specified. Git and
88 88 Subversion subrepositories will print a warning and continue.
89 89
90 90 :archive: archive does not recurse in subrepositories unless
91 91 -S/--subrepos is specified.
92 92
93 93 :cat: cat currently only handles exact file matches in subrepos.
94 Git and Subversion subrepositories are currently ignored.
94 Subversion subrepositories are currently ignored.
95 95
96 96 :commit: commit creates a consistent snapshot of the state of the
97 97 entire project and its subrepositories. If any subrepositories
98 98 have been modified, Mercurial will abort. Mercurial can be made
99 99 to instead commit all modified subrepositories by specifying
100 100 -S/--subrepos, or setting "ui.commitsubrepos=True" in a
101 101 configuration file (see :hg:`help config`). After there are no
102 102 longer any modified subrepositories, it records their state and
103 103 finally commits it in the parent repository. The --addremove
104 104 option also honors the -S/--subrepos option. However, Git and
105 105 Subversion subrepositories will print a warning and abort.
106 106
107 107 :diff: diff does not recurse in subrepos unless -S/--subrepos is
108 108 specified. Changes are displayed as usual, on the subrepositories
109 109 elements. Git subrepositories do not support --include/--exclude.
110 110 Subversion subrepositories are currently silently ignored.
111 111
112 112 :forget: forget currently only handles exact file matches in subrepos.
113 113 Git and Subversion subrepositories are currently silently ignored.
114 114
115 115 :incoming: incoming does not recurse in subrepos unless -S/--subrepos
116 116 is specified. Git and Subversion subrepositories are currently
117 117 silently ignored.
118 118
119 119 :outgoing: outgoing does not recurse in subrepos unless -S/--subrepos
120 120 is specified. Git and Subversion subrepositories are currently
121 121 silently ignored.
122 122
123 123 :pull: pull is not recursive since it is not clear what to pull prior
124 124 to running :hg:`update`. Listing and retrieving all
125 125 subrepositories changes referenced by the parent repository pulled
126 126 changesets is expensive at best, impossible in the Subversion
127 127 case.
128 128
129 129 :push: Mercurial will automatically push all subrepositories first
130 130 when the parent repository is being pushed. This ensures new
131 131 subrepository changes are available when referenced by top-level
132 132 repositories. Push is a no-op for Subversion subrepositories.
133 133
134 134 :status: status does not recurse into subrepositories unless
135 135 -S/--subrepos is specified. Subrepository changes are displayed as
136 136 regular Mercurial changes on the subrepository
137 137 elements. Subversion subrepositories are currently silently
138 138 ignored.
139 139
140 140 :remove: remove does not recurse into subrepositories unless
141 141 -S/--subrepos is specified. However, if you specify a file or
142 142 directory path in a subrepo, it will be removed even without
143 143 -S/--subrepos. Git and Subversion subrepositories are currently
144 144 silently ignored.
145 145
146 146 :update: update restores the subrepos in the state they were
147 147 originally committed in target changeset. If the recorded
148 148 changeset is not available in the current subrepository, Mercurial
149 149 will pull it in first before updating. This means that updating
150 150 can require network access when using subrepositories.
151 151
152 152 Remapping Subrepositories Sources
153 153 =================================
154 154
155 155 A subrepository source location may change during a project life,
156 156 invalidating references stored in the parent repository history. To
157 157 fix this, rewriting rules can be defined in parent repository ``hgrc``
158 158 file or in Mercurial configuration. See the ``[subpaths]`` section in
159 159 hgrc(5) for more details.
160 160
@@ -1,1686 +1,1705 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 import copy
9 9 import errno, os, re, shutil, posixpath, sys
10 10 import xml.dom.minidom
11 11 import stat, subprocess, tarfile
12 12 from i18n import _
13 13 import config, util, node, error, cmdutil, scmutil, match as matchmod
14 14 import phases
15 15 import pathutil
16 16 import exchange
17 17 hg = None
18 18 propertycache = util.propertycache
19 19
20 20 nullstate = ('', '', 'empty')
21 21
22 22 def _expandedabspath(path):
23 23 '''
24 24 get a path or url and if it is a path expand it and return an absolute path
25 25 '''
26 26 expandedpath = util.urllocalpath(util.expandpath(path))
27 27 u = util.url(expandedpath)
28 28 if not u.scheme:
29 29 path = util.normpath(os.path.abspath(u.path))
30 30 return path
31 31
32 32 def _getstorehashcachename(remotepath):
33 33 '''get a unique filename for the store hash cache of a remote repository'''
34 34 return util.sha1(_expandedabspath(remotepath)).hexdigest()[0:12]
35 35
36 36 class SubrepoAbort(error.Abort):
37 37 """Exception class used to avoid handling a subrepo error more than once"""
38 38 def __init__(self, *args, **kw):
39 39 error.Abort.__init__(self, *args, **kw)
40 40 self.subrepo = kw.get('subrepo')
41 41 self.cause = kw.get('cause')
42 42
43 43 def annotatesubrepoerror(func):
44 44 def decoratedmethod(self, *args, **kargs):
45 45 try:
46 46 res = func(self, *args, **kargs)
47 47 except SubrepoAbort, ex:
48 48 # This exception has already been handled
49 49 raise ex
50 50 except error.Abort, ex:
51 51 subrepo = subrelpath(self)
52 52 errormsg = str(ex) + ' ' + _('(in subrepo %s)') % subrepo
53 53 # avoid handling this exception by raising a SubrepoAbort exception
54 54 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
55 55 cause=sys.exc_info())
56 56 return res
57 57 return decoratedmethod
58 58
59 59 def state(ctx, ui):
60 60 """return a state dict, mapping subrepo paths configured in .hgsub
61 61 to tuple: (source from .hgsub, revision from .hgsubstate, kind
62 62 (key in types dict))
63 63 """
64 64 p = config.config()
65 65 def read(f, sections=None, remap=None):
66 66 if f in ctx:
67 67 try:
68 68 data = ctx[f].data()
69 69 except IOError, err:
70 70 if err.errno != errno.ENOENT:
71 71 raise
72 72 # handle missing subrepo spec files as removed
73 73 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
74 74 return
75 75 p.parse(f, data, sections, remap, read)
76 76 else:
77 77 raise util.Abort(_("subrepo spec file %s not found") % f)
78 78
79 79 if '.hgsub' in ctx:
80 80 read('.hgsub')
81 81
82 82 for path, src in ui.configitems('subpaths'):
83 83 p.set('subpaths', path, src, ui.configsource('subpaths', path))
84 84
85 85 rev = {}
86 86 if '.hgsubstate' in ctx:
87 87 try:
88 88 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
89 89 l = l.lstrip()
90 90 if not l:
91 91 continue
92 92 try:
93 93 revision, path = l.split(" ", 1)
94 94 except ValueError:
95 95 raise util.Abort(_("invalid subrepository revision "
96 96 "specifier in .hgsubstate line %d")
97 97 % (i + 1))
98 98 rev[path] = revision
99 99 except IOError, err:
100 100 if err.errno != errno.ENOENT:
101 101 raise
102 102
103 103 def remap(src):
104 104 for pattern, repl in p.items('subpaths'):
105 105 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
106 106 # does a string decode.
107 107 repl = repl.encode('string-escape')
108 108 # However, we still want to allow back references to go
109 109 # through unharmed, so we turn r'\\1' into r'\1'. Again,
110 110 # extra escapes are needed because re.sub string decodes.
111 111 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
112 112 try:
113 113 src = re.sub(pattern, repl, src, 1)
114 114 except re.error, e:
115 115 raise util.Abort(_("bad subrepository pattern in %s: %s")
116 116 % (p.source('subpaths', pattern), e))
117 117 return src
118 118
119 119 state = {}
120 120 for path, src in p[''].items():
121 121 kind = 'hg'
122 122 if src.startswith('['):
123 123 if ']' not in src:
124 124 raise util.Abort(_('missing ] in subrepo source'))
125 125 kind, src = src.split(']', 1)
126 126 kind = kind[1:]
127 127 src = src.lstrip() # strip any extra whitespace after ']'
128 128
129 129 if not util.url(src).isabs():
130 130 parent = _abssource(ctx._repo, abort=False)
131 131 if parent:
132 132 parent = util.url(parent)
133 133 parent.path = posixpath.join(parent.path or '', src)
134 134 parent.path = posixpath.normpath(parent.path)
135 135 joined = str(parent)
136 136 # Remap the full joined path and use it if it changes,
137 137 # else remap the original source.
138 138 remapped = remap(joined)
139 139 if remapped == joined:
140 140 src = remap(src)
141 141 else:
142 142 src = remapped
143 143
144 144 src = remap(src)
145 145 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
146 146
147 147 return state
148 148
149 149 def writestate(repo, state):
150 150 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
151 151 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
152 152 repo.wwrite('.hgsubstate', ''.join(lines), '')
153 153
154 154 def submerge(repo, wctx, mctx, actx, overwrite):
155 155 """delegated from merge.applyupdates: merging of .hgsubstate file
156 156 in working context, merging context and ancestor context"""
157 157 if mctx == actx: # backwards?
158 158 actx = wctx.p1()
159 159 s1 = wctx.substate
160 160 s2 = mctx.substate
161 161 sa = actx.substate
162 162 sm = {}
163 163
164 164 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
165 165
166 166 def debug(s, msg, r=""):
167 167 if r:
168 168 r = "%s:%s:%s" % r
169 169 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
170 170
171 171 for s, l in sorted(s1.iteritems()):
172 172 a = sa.get(s, nullstate)
173 173 ld = l # local state with possible dirty flag for compares
174 174 if wctx.sub(s).dirty():
175 175 ld = (l[0], l[1] + "+")
176 176 if wctx == actx: # overwrite
177 177 a = ld
178 178
179 179 if s in s2:
180 180 r = s2[s]
181 181 if ld == r or r == a: # no change or local is newer
182 182 sm[s] = l
183 183 continue
184 184 elif ld == a: # other side changed
185 185 debug(s, "other changed, get", r)
186 186 wctx.sub(s).get(r, overwrite)
187 187 sm[s] = r
188 188 elif ld[0] != r[0]: # sources differ
189 189 if repo.ui.promptchoice(
190 190 _(' subrepository sources for %s differ\n'
191 191 'use (l)ocal source (%s) or (r)emote source (%s)?'
192 192 '$$ &Local $$ &Remote') % (s, l[0], r[0]), 0):
193 193 debug(s, "prompt changed, get", r)
194 194 wctx.sub(s).get(r, overwrite)
195 195 sm[s] = r
196 196 elif ld[1] == a[1]: # local side is unchanged
197 197 debug(s, "other side changed, get", r)
198 198 wctx.sub(s).get(r, overwrite)
199 199 sm[s] = r
200 200 else:
201 201 debug(s, "both sides changed")
202 202 srepo = wctx.sub(s)
203 203 option = repo.ui.promptchoice(
204 204 _(' subrepository %s diverged (local revision: %s, '
205 205 'remote revision: %s)\n'
206 206 '(M)erge, keep (l)ocal or keep (r)emote?'
207 207 '$$ &Merge $$ &Local $$ &Remote')
208 208 % (s, srepo.shortid(l[1]), srepo.shortid(r[1])), 0)
209 209 if option == 0:
210 210 wctx.sub(s).merge(r)
211 211 sm[s] = l
212 212 debug(s, "merge with", r)
213 213 elif option == 1:
214 214 sm[s] = l
215 215 debug(s, "keep local subrepo revision", l)
216 216 else:
217 217 wctx.sub(s).get(r, overwrite)
218 218 sm[s] = r
219 219 debug(s, "get remote subrepo revision", r)
220 220 elif ld == a: # remote removed, local unchanged
221 221 debug(s, "remote removed, remove")
222 222 wctx.sub(s).remove()
223 223 elif a == nullstate: # not present in remote or ancestor
224 224 debug(s, "local added, keep")
225 225 sm[s] = l
226 226 continue
227 227 else:
228 228 if repo.ui.promptchoice(
229 229 _(' local changed subrepository %s which remote removed\n'
230 230 'use (c)hanged version or (d)elete?'
231 231 '$$ &Changed $$ &Delete') % s, 0):
232 232 debug(s, "prompt remove")
233 233 wctx.sub(s).remove()
234 234
235 235 for s, r in sorted(s2.items()):
236 236 if s in s1:
237 237 continue
238 238 elif s not in sa:
239 239 debug(s, "remote added, get", r)
240 240 mctx.sub(s).get(r)
241 241 sm[s] = r
242 242 elif r != sa[s]:
243 243 if repo.ui.promptchoice(
244 244 _(' remote changed subrepository %s which local removed\n'
245 245 'use (c)hanged version or (d)elete?'
246 246 '$$ &Changed $$ &Delete') % s, 0) == 0:
247 247 debug(s, "prompt recreate", r)
248 248 wctx.sub(s).get(r)
249 249 sm[s] = r
250 250
251 251 # record merged .hgsubstate
252 252 writestate(repo, sm)
253 253 return sm
254 254
255 255 def _updateprompt(ui, sub, dirty, local, remote):
256 256 if dirty:
257 257 msg = (_(' subrepository sources for %s differ\n'
258 258 'use (l)ocal source (%s) or (r)emote source (%s)?'
259 259 '$$ &Local $$ &Remote')
260 260 % (subrelpath(sub), local, remote))
261 261 else:
262 262 msg = (_(' subrepository sources for %s differ (in checked out '
263 263 'version)\n'
264 264 'use (l)ocal source (%s) or (r)emote source (%s)?'
265 265 '$$ &Local $$ &Remote')
266 266 % (subrelpath(sub), local, remote))
267 267 return ui.promptchoice(msg, 0)
268 268
269 269 def reporelpath(repo):
270 270 """return path to this (sub)repo as seen from outermost repo"""
271 271 parent = repo
272 272 while util.safehasattr(parent, '_subparent'):
273 273 parent = parent._subparent
274 274 return repo.root[len(pathutil.normasprefix(parent.root)):]
275 275
276 276 def subrelpath(sub):
277 277 """return path to this subrepo as seen from outermost repo"""
278 278 if util.safehasattr(sub, '_relpath'):
279 279 return sub._relpath
280 280 if not util.safehasattr(sub, '_repo'):
281 281 return sub._path
282 282 return reporelpath(sub._repo)
283 283
284 284 def _abssource(repo, push=False, abort=True):
285 285 """return pull/push path of repo - either based on parent repo .hgsub info
286 286 or on the top repo config. Abort or return None if no source found."""
287 287 if util.safehasattr(repo, '_subparent'):
288 288 source = util.url(repo._subsource)
289 289 if source.isabs():
290 290 return str(source)
291 291 source.path = posixpath.normpath(source.path)
292 292 parent = _abssource(repo._subparent, push, abort=False)
293 293 if parent:
294 294 parent = util.url(util.pconvert(parent))
295 295 parent.path = posixpath.join(parent.path or '', source.path)
296 296 parent.path = posixpath.normpath(parent.path)
297 297 return str(parent)
298 298 else: # recursion reached top repo
299 299 if util.safehasattr(repo, '_subtoppath'):
300 300 return repo._subtoppath
301 301 if push and repo.ui.config('paths', 'default-push'):
302 302 return repo.ui.config('paths', 'default-push')
303 303 if repo.ui.config('paths', 'default'):
304 304 return repo.ui.config('paths', 'default')
305 305 if repo.shared():
306 306 # chop off the .hg component to get the default path form
307 307 return os.path.dirname(repo.sharedpath)
308 308 if abort:
309 309 raise util.Abort(_("default path for subrepository not found"))
310 310
311 311 def _sanitize(ui, path, ignore):
312 312 for dirname, dirs, names in os.walk(path):
313 313 for i, d in enumerate(dirs):
314 314 if d.lower() == ignore:
315 315 del dirs[i]
316 316 break
317 317 if os.path.basename(dirname).lower() != '.hg':
318 318 continue
319 319 for f in names:
320 320 if f.lower() == 'hgrc':
321 321 ui.warn(_("warning: removing potentially hostile 'hgrc' "
322 322 "in '%s'\n") % dirname)
323 323 os.unlink(os.path.join(dirname, f))
324 324
325 325 def subrepo(ctx, path):
326 326 """return instance of the right subrepo class for subrepo in path"""
327 327 # subrepo inherently violates our import layering rules
328 328 # because it wants to make repo objects from deep inside the stack
329 329 # so we manually delay the circular imports to not break
330 330 # scripts that don't use our demand-loading
331 331 global hg
332 332 import hg as h
333 333 hg = h
334 334
335 335 pathutil.pathauditor(ctx._repo.root)(path)
336 336 state = ctx.substate[path]
337 337 if state[2] not in types:
338 338 raise util.Abort(_('unknown subrepo type %s') % state[2])
339 339 return types[state[2]](ctx, path, state[:2])
340 340
341 341 def newcommitphase(ui, ctx):
342 342 commitphase = phases.newcommitphase(ui)
343 343 substate = getattr(ctx, "substate", None)
344 344 if not substate:
345 345 return commitphase
346 346 check = ui.config('phases', 'checksubrepos', 'follow')
347 347 if check not in ('ignore', 'follow', 'abort'):
348 348 raise util.Abort(_('invalid phases.checksubrepos configuration: %s')
349 349 % (check))
350 350 if check == 'ignore':
351 351 return commitphase
352 352 maxphase = phases.public
353 353 maxsub = None
354 354 for s in sorted(substate):
355 355 sub = ctx.sub(s)
356 356 subphase = sub.phase(substate[s][1])
357 357 if maxphase < subphase:
358 358 maxphase = subphase
359 359 maxsub = s
360 360 if commitphase < maxphase:
361 361 if check == 'abort':
362 362 raise util.Abort(_("can't commit in %s phase"
363 363 " conflicting %s from subrepository %s") %
364 364 (phases.phasenames[commitphase],
365 365 phases.phasenames[maxphase], maxsub))
366 366 ui.warn(_("warning: changes are committed in"
367 367 " %s phase from subrepository %s\n") %
368 368 (phases.phasenames[maxphase], maxsub))
369 369 return maxphase
370 370 return commitphase
371 371
372 372 # subrepo classes need to implement the following abstract class:
373 373
374 374 class abstractsubrepo(object):
375 375
376 376 def __init__(self, ui):
377 377 self.ui = ui
378 378
379 379 def storeclean(self, path):
380 380 """
381 381 returns true if the repository has not changed since it was last
382 382 cloned from or pushed to a given repository.
383 383 """
384 384 return False
385 385
386 386 def dirty(self, ignoreupdate=False):
387 387 """returns true if the dirstate of the subrepo is dirty or does not
388 388 match current stored state. If ignoreupdate is true, only check
389 389 whether the subrepo has uncommitted changes in its dirstate.
390 390 """
391 391 raise NotImplementedError
392 392
393 393 def basestate(self):
394 394 """current working directory base state, disregarding .hgsubstate
395 395 state and working directory modifications"""
396 396 raise NotImplementedError
397 397
398 398 def checknested(self, path):
399 399 """check if path is a subrepository within this repository"""
400 400 return False
401 401
402 402 def commit(self, text, user, date):
403 403 """commit the current changes to the subrepo with the given
404 404 log message. Use given user and date if possible. Return the
405 405 new state of the subrepo.
406 406 """
407 407 raise NotImplementedError
408 408
409 409 def phase(self, state):
410 410 """returns phase of specified state in the subrepository.
411 411 """
412 412 return phases.public
413 413
414 414 def remove(self):
415 415 """remove the subrepo
416 416
417 417 (should verify the dirstate is not dirty first)
418 418 """
419 419 raise NotImplementedError
420 420
421 421 def get(self, state, overwrite=False):
422 422 """run whatever commands are needed to put the subrepo into
423 423 this state
424 424 """
425 425 raise NotImplementedError
426 426
427 427 def merge(self, state):
428 428 """merge currently-saved state with the new state."""
429 429 raise NotImplementedError
430 430
431 431 def push(self, opts):
432 432 """perform whatever action is analogous to 'hg push'
433 433
434 434 This may be a no-op on some systems.
435 435 """
436 436 raise NotImplementedError
437 437
438 438 def add(self, ui, match, prefix, explicitonly, **opts):
439 439 return []
440 440
441 441 def addremove(self, matcher, prefix, opts, dry_run, similarity):
442 442 self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
443 443 return 1
444 444
445 445 def cat(self, match, prefix, **opts):
446 446 return 1
447 447
448 448 def status(self, rev2, **opts):
449 449 return scmutil.status([], [], [], [], [], [], [])
450 450
451 451 def diff(self, ui, diffopts, node2, match, prefix, **opts):
452 452 pass
453 453
454 454 def outgoing(self, ui, dest, opts):
455 455 return 1
456 456
457 457 def incoming(self, ui, source, opts):
458 458 return 1
459 459
460 460 def files(self):
461 461 """return filename iterator"""
462 462 raise NotImplementedError
463 463
464 464 def filedata(self, name):
465 465 """return file data"""
466 466 raise NotImplementedError
467 467
468 468 def fileflags(self, name):
469 469 """return file flags"""
470 470 return ''
471 471
472 472 def archive(self, archiver, prefix, match=None):
473 473 if match is not None:
474 474 files = [f for f in self.files() if match(f)]
475 475 else:
476 476 files = self.files()
477 477 total = len(files)
478 478 relpath = subrelpath(self)
479 479 self.ui.progress(_('archiving (%s)') % relpath, 0,
480 480 unit=_('files'), total=total)
481 481 for i, name in enumerate(files):
482 482 flags = self.fileflags(name)
483 483 mode = 'x' in flags and 0755 or 0644
484 484 symlink = 'l' in flags
485 485 archiver.addfile(os.path.join(prefix, self._path, name),
486 486 mode, symlink, self.filedata(name))
487 487 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
488 488 unit=_('files'), total=total)
489 489 self.ui.progress(_('archiving (%s)') % relpath, None)
490 490 return total
491 491
492 492 def walk(self, match):
493 493 '''
494 494 walk recursively through the directory tree, finding all files
495 495 matched by the match function
496 496 '''
497 497 pass
498 498
499 499 def forget(self, match, prefix):
500 500 return ([], [])
501 501
502 502 def removefiles(self, matcher, prefix, after, force, subrepos):
503 503 """remove the matched files from the subrepository and the filesystem,
504 504 possibly by force and/or after the file has been removed from the
505 505 filesystem. Return 0 on success, 1 on any warning.
506 506 """
507 507 return 1
508 508
509 509 def revert(self, substate, *pats, **opts):
510 510 self.ui.warn('%s: reverting %s subrepos is unsupported\n' \
511 511 % (substate[0], substate[2]))
512 512 return []
513 513
514 514 def shortid(self, revid):
515 515 return revid
516 516
517 517 class hgsubrepo(abstractsubrepo):
518 518 def __init__(self, ctx, path, state):
519 519 super(hgsubrepo, self).__init__(ctx._repo.ui)
520 520 self._path = path
521 521 self._state = state
522 522 r = ctx._repo
523 523 root = r.wjoin(path)
524 524 create = not r.wvfs.exists('%s/.hg' % path)
525 525 self._repo = hg.repository(r.baseui, root, create=create)
526 526 self.ui = self._repo.ui
527 527 for s, k in [('ui', 'commitsubrepos')]:
528 528 v = r.ui.config(s, k)
529 529 if v:
530 530 self.ui.setconfig(s, k, v, 'subrepo')
531 531 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
532 532 self._initrepo(r, state[0], create)
533 533
534 534 def storeclean(self, path):
535 535 lock = self._repo.lock()
536 536 try:
537 537 return self._storeclean(path)
538 538 finally:
539 539 lock.release()
540 540
541 541 def _storeclean(self, path):
542 542 clean = True
543 543 itercache = self._calcstorehash(path)
544 544 try:
545 545 for filehash in self._readstorehashcache(path):
546 546 if filehash != itercache.next():
547 547 clean = False
548 548 break
549 549 except StopIteration:
550 550 # the cached and current pull states have a different size
551 551 clean = False
552 552 if clean:
553 553 try:
554 554 itercache.next()
555 555 # the cached and current pull states have a different size
556 556 clean = False
557 557 except StopIteration:
558 558 pass
559 559 return clean
560 560
561 561 def _calcstorehash(self, remotepath):
562 562 '''calculate a unique "store hash"
563 563
564 564 This method is used to to detect when there are changes that may
565 565 require a push to a given remote path.'''
566 566 # sort the files that will be hashed in increasing (likely) file size
567 567 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
568 568 yield '# %s\n' % _expandedabspath(remotepath)
569 569 vfs = self._repo.vfs
570 570 for relname in filelist:
571 571 filehash = util.sha1(vfs.tryread(relname)).hexdigest()
572 572 yield '%s = %s\n' % (relname, filehash)
573 573
574 574 @propertycache
575 575 def _cachestorehashvfs(self):
576 576 return scmutil.vfs(self._repo.join('cache/storehash'))
577 577
578 578 def _readstorehashcache(self, remotepath):
579 579 '''read the store hash cache for a given remote repository'''
580 580 cachefile = _getstorehashcachename(remotepath)
581 581 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
582 582
583 583 def _cachestorehash(self, remotepath):
584 584 '''cache the current store hash
585 585
586 586 Each remote repo requires its own store hash cache, because a subrepo
587 587 store may be "clean" versus a given remote repo, but not versus another
588 588 '''
589 589 cachefile = _getstorehashcachename(remotepath)
590 590 lock = self._repo.lock()
591 591 try:
592 592 storehash = list(self._calcstorehash(remotepath))
593 593 vfs = self._cachestorehashvfs
594 594 vfs.writelines(cachefile, storehash, mode='w', notindexed=True)
595 595 finally:
596 596 lock.release()
597 597
598 598 @annotatesubrepoerror
599 599 def _initrepo(self, parentrepo, source, create):
600 600 self._repo._subparent = parentrepo
601 601 self._repo._subsource = source
602 602
603 603 if create:
604 604 lines = ['[paths]\n']
605 605
606 606 def addpathconfig(key, value):
607 607 if value:
608 608 lines.append('%s = %s\n' % (key, value))
609 609 self.ui.setconfig('paths', key, value, 'subrepo')
610 610
611 611 defpath = _abssource(self._repo, abort=False)
612 612 defpushpath = _abssource(self._repo, True, abort=False)
613 613 addpathconfig('default', defpath)
614 614 if defpath != defpushpath:
615 615 addpathconfig('default-push', defpushpath)
616 616
617 617 fp = self._repo.vfs("hgrc", "w", text=True)
618 618 try:
619 619 fp.write(''.join(lines))
620 620 finally:
621 621 fp.close()
622 622
623 623 @annotatesubrepoerror
624 624 def add(self, ui, match, prefix, explicitonly, **opts):
625 625 return cmdutil.add(ui, self._repo, match,
626 626 os.path.join(prefix, self._path), explicitonly,
627 627 **opts)
628 628
629 629 def addremove(self, m, prefix, opts, dry_run, similarity):
630 630 # In the same way as sub directories are processed, once in a subrepo,
631 631 # always entry any of its subrepos. Don't corrupt the options that will
632 632 # be used to process sibling subrepos however.
633 633 opts = copy.copy(opts)
634 634 opts['subrepos'] = True
635 635 return scmutil.addremove(self._repo, m,
636 636 os.path.join(prefix, self._path), opts,
637 637 dry_run, similarity)
638 638
639 639 @annotatesubrepoerror
640 640 def cat(self, match, prefix, **opts):
641 641 rev = self._state[1]
642 642 ctx = self._repo[rev]
643 643 return cmdutil.cat(self.ui, self._repo, ctx, match, prefix, **opts)
644 644
645 645 @annotatesubrepoerror
646 646 def status(self, rev2, **opts):
647 647 try:
648 648 rev1 = self._state[1]
649 649 ctx1 = self._repo[rev1]
650 650 ctx2 = self._repo[rev2]
651 651 return self._repo.status(ctx1, ctx2, **opts)
652 652 except error.RepoLookupError, inst:
653 653 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
654 654 % (inst, subrelpath(self)))
655 655 return scmutil.status([], [], [], [], [], [], [])
656 656
657 657 @annotatesubrepoerror
658 658 def diff(self, ui, diffopts, node2, match, prefix, **opts):
659 659 try:
660 660 node1 = node.bin(self._state[1])
661 661 # We currently expect node2 to come from substate and be
662 662 # in hex format
663 663 if node2 is not None:
664 664 node2 = node.bin(node2)
665 665 cmdutil.diffordiffstat(ui, self._repo, diffopts,
666 666 node1, node2, match,
667 667 prefix=posixpath.join(prefix, self._path),
668 668 listsubrepos=True, **opts)
669 669 except error.RepoLookupError, inst:
670 670 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
671 671 % (inst, subrelpath(self)))
672 672
673 673 @annotatesubrepoerror
674 674 def archive(self, archiver, prefix, match=None):
675 675 self._get(self._state + ('hg',))
676 676 total = abstractsubrepo.archive(self, archiver, prefix, match)
677 677 rev = self._state[1]
678 678 ctx = self._repo[rev]
679 679 for subpath in ctx.substate:
680 680 s = subrepo(ctx, subpath)
681 681 submatch = matchmod.narrowmatcher(subpath, match)
682 682 total += s.archive(
683 683 archiver, os.path.join(prefix, self._path), submatch)
684 684 return total
685 685
686 686 @annotatesubrepoerror
687 687 def dirty(self, ignoreupdate=False):
688 688 r = self._state[1]
689 689 if r == '' and not ignoreupdate: # no state recorded
690 690 return True
691 691 w = self._repo[None]
692 692 if r != w.p1().hex() and not ignoreupdate:
693 693 # different version checked out
694 694 return True
695 695 return w.dirty() # working directory changed
696 696
697 697 def basestate(self):
698 698 return self._repo['.'].hex()
699 699
700 700 def checknested(self, path):
701 701 return self._repo._checknested(self._repo.wjoin(path))
702 702
703 703 @annotatesubrepoerror
704 704 def commit(self, text, user, date):
705 705 # don't bother committing in the subrepo if it's only been
706 706 # updated
707 707 if not self.dirty(True):
708 708 return self._repo['.'].hex()
709 709 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
710 710 n = self._repo.commit(text, user, date)
711 711 if not n:
712 712 return self._repo['.'].hex() # different version checked out
713 713 return node.hex(n)
714 714
715 715 @annotatesubrepoerror
716 716 def phase(self, state):
717 717 return self._repo[state].phase()
718 718
719 719 @annotatesubrepoerror
720 720 def remove(self):
721 721 # we can't fully delete the repository as it may contain
722 722 # local-only history
723 723 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
724 724 hg.clean(self._repo, node.nullid, False)
725 725
726 726 def _get(self, state):
727 727 source, revision, kind = state
728 728 if revision in self._repo.unfiltered():
729 729 return True
730 730 self._repo._subsource = source
731 731 srcurl = _abssource(self._repo)
732 732 other = hg.peer(self._repo, {}, srcurl)
733 733 if len(self._repo) == 0:
734 734 self.ui.status(_('cloning subrepo %s from %s\n')
735 735 % (subrelpath(self), srcurl))
736 736 parentrepo = self._repo._subparent
737 737 shutil.rmtree(self._repo.path)
738 738 other, cloned = hg.clone(self._repo._subparent.baseui, {},
739 739 other, self._repo.root,
740 740 update=False)
741 741 self._repo = cloned.local()
742 742 self._initrepo(parentrepo, source, create=True)
743 743 self._cachestorehash(srcurl)
744 744 else:
745 745 self.ui.status(_('pulling subrepo %s from %s\n')
746 746 % (subrelpath(self), srcurl))
747 747 cleansub = self.storeclean(srcurl)
748 748 exchange.pull(self._repo, other)
749 749 if cleansub:
750 750 # keep the repo clean after pull
751 751 self._cachestorehash(srcurl)
752 752 return False
753 753
754 754 @annotatesubrepoerror
755 755 def get(self, state, overwrite=False):
756 756 inrepo = self._get(state)
757 757 source, revision, kind = state
758 758 repo = self._repo
759 759 repo.ui.debug("getting subrepo %s\n" % self._path)
760 760 if inrepo:
761 761 urepo = repo.unfiltered()
762 762 ctx = urepo[revision]
763 763 if ctx.hidden():
764 764 urepo.ui.warn(
765 765 _('revision %s in subrepo %s is hidden\n') \
766 766 % (revision[0:12], self._path))
767 767 repo = urepo
768 768 hg.updaterepo(repo, revision, overwrite)
769 769
770 770 @annotatesubrepoerror
771 771 def merge(self, state):
772 772 self._get(state)
773 773 cur = self._repo['.']
774 774 dst = self._repo[state[1]]
775 775 anc = dst.ancestor(cur)
776 776
777 777 def mergefunc():
778 778 if anc == cur and dst.branch() == cur.branch():
779 779 self.ui.debug("updating subrepo %s\n" % subrelpath(self))
780 780 hg.update(self._repo, state[1])
781 781 elif anc == dst:
782 782 self.ui.debug("skipping subrepo %s\n" % subrelpath(self))
783 783 else:
784 784 self.ui.debug("merging subrepo %s\n" % subrelpath(self))
785 785 hg.merge(self._repo, state[1], remind=False)
786 786
787 787 wctx = self._repo[None]
788 788 if self.dirty():
789 789 if anc != dst:
790 790 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
791 791 mergefunc()
792 792 else:
793 793 mergefunc()
794 794 else:
795 795 mergefunc()
796 796
797 797 @annotatesubrepoerror
798 798 def push(self, opts):
799 799 force = opts.get('force')
800 800 newbranch = opts.get('new_branch')
801 801 ssh = opts.get('ssh')
802 802
803 803 # push subrepos depth-first for coherent ordering
804 804 c = self._repo['']
805 805 subs = c.substate # only repos that are committed
806 806 for s in sorted(subs):
807 807 if c.sub(s).push(opts) == 0:
808 808 return False
809 809
810 810 dsturl = _abssource(self._repo, True)
811 811 if not force:
812 812 if self.storeclean(dsturl):
813 813 self.ui.status(
814 814 _('no changes made to subrepo %s since last push to %s\n')
815 815 % (subrelpath(self), dsturl))
816 816 return None
817 817 self.ui.status(_('pushing subrepo %s to %s\n') %
818 818 (subrelpath(self), dsturl))
819 819 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
820 820 res = exchange.push(self._repo, other, force, newbranch=newbranch)
821 821
822 822 # the repo is now clean
823 823 self._cachestorehash(dsturl)
824 824 return res.cgresult
825 825
826 826 @annotatesubrepoerror
827 827 def outgoing(self, ui, dest, opts):
828 828 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
829 829
830 830 @annotatesubrepoerror
831 831 def incoming(self, ui, source, opts):
832 832 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
833 833
834 834 @annotatesubrepoerror
835 835 def files(self):
836 836 rev = self._state[1]
837 837 ctx = self._repo[rev]
838 838 return ctx.manifest()
839 839
840 840 def filedata(self, name):
841 841 rev = self._state[1]
842 842 return self._repo[rev][name].data()
843 843
844 844 def fileflags(self, name):
845 845 rev = self._state[1]
846 846 ctx = self._repo[rev]
847 847 return ctx.flags(name)
848 848
849 849 def walk(self, match):
850 850 ctx = self._repo[None]
851 851 return ctx.walk(match)
852 852
853 853 @annotatesubrepoerror
854 854 def forget(self, match, prefix):
855 855 return cmdutil.forget(self.ui, self._repo, match,
856 856 os.path.join(prefix, self._path), True)
857 857
858 858 @annotatesubrepoerror
859 859 def removefiles(self, matcher, prefix, after, force, subrepos):
860 860 return cmdutil.remove(self.ui, self._repo, matcher,
861 861 os.path.join(prefix, self._path), after, force,
862 862 subrepos)
863 863
864 864 @annotatesubrepoerror
865 865 def revert(self, substate, *pats, **opts):
866 866 # reverting a subrepo is a 2 step process:
867 867 # 1. if the no_backup is not set, revert all modified
868 868 # files inside the subrepo
869 869 # 2. update the subrepo to the revision specified in
870 870 # the corresponding substate dictionary
871 871 self.ui.status(_('reverting subrepo %s\n') % substate[0])
872 872 if not opts.get('no_backup'):
873 873 # Revert all files on the subrepo, creating backups
874 874 # Note that this will not recursively revert subrepos
875 875 # We could do it if there was a set:subrepos() predicate
876 876 opts = opts.copy()
877 877 opts['date'] = None
878 878 opts['rev'] = substate[1]
879 879
880 880 pats = []
881 881 if not opts.get('all'):
882 882 pats = ['set:modified()']
883 883 self.filerevert(*pats, **opts)
884 884
885 885 # Update the repo to the revision specified in the given substate
886 886 self.get(substate, overwrite=True)
887 887
888 888 def filerevert(self, *pats, **opts):
889 889 ctx = self._repo[opts['rev']]
890 890 parents = self._repo.dirstate.parents()
891 891 if opts.get('all'):
892 892 pats = ['set:modified()']
893 893 else:
894 894 pats = []
895 895 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
896 896
897 897 def shortid(self, revid):
898 898 return revid[:12]
899 899
900 900 class svnsubrepo(abstractsubrepo):
901 901 def __init__(self, ctx, path, state):
902 902 super(svnsubrepo, self).__init__(ctx._repo.ui)
903 903 self._path = path
904 904 self._state = state
905 905 self._ctx = ctx
906 906 self._exe = util.findexe('svn')
907 907 if not self._exe:
908 908 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
909 909 % self._path)
910 910
911 911 def _svncommand(self, commands, filename='', failok=False):
912 912 cmd = [self._exe]
913 913 extrakw = {}
914 914 if not self.ui.interactive():
915 915 # Making stdin be a pipe should prevent svn from behaving
916 916 # interactively even if we can't pass --non-interactive.
917 917 extrakw['stdin'] = subprocess.PIPE
918 918 # Starting in svn 1.5 --non-interactive is a global flag
919 919 # instead of being per-command, but we need to support 1.4 so
920 920 # we have to be intelligent about what commands take
921 921 # --non-interactive.
922 922 if commands[0] in ('update', 'checkout', 'commit'):
923 923 cmd.append('--non-interactive')
924 924 cmd.extend(commands)
925 925 if filename is not None:
926 926 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
927 927 cmd.append(path)
928 928 env = dict(os.environ)
929 929 # Avoid localized output, preserve current locale for everything else.
930 930 lc_all = env.get('LC_ALL')
931 931 if lc_all:
932 932 env['LANG'] = lc_all
933 933 del env['LC_ALL']
934 934 env['LC_MESSAGES'] = 'C'
935 935 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
936 936 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
937 937 universal_newlines=True, env=env, **extrakw)
938 938 stdout, stderr = p.communicate()
939 939 stderr = stderr.strip()
940 940 if not failok:
941 941 if p.returncode:
942 942 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
943 943 if stderr:
944 944 self.ui.warn(stderr + '\n')
945 945 return stdout, stderr
946 946
947 947 @propertycache
948 948 def _svnversion(self):
949 949 output, err = self._svncommand(['--version', '--quiet'], filename=None)
950 950 m = re.search(r'^(\d+)\.(\d+)', output)
951 951 if not m:
952 952 raise util.Abort(_('cannot retrieve svn tool version'))
953 953 return (int(m.group(1)), int(m.group(2)))
954 954
955 955 def _wcrevs(self):
956 956 # Get the working directory revision as well as the last
957 957 # commit revision so we can compare the subrepo state with
958 958 # both. We used to store the working directory one.
959 959 output, err = self._svncommand(['info', '--xml'])
960 960 doc = xml.dom.minidom.parseString(output)
961 961 entries = doc.getElementsByTagName('entry')
962 962 lastrev, rev = '0', '0'
963 963 if entries:
964 964 rev = str(entries[0].getAttribute('revision')) or '0'
965 965 commits = entries[0].getElementsByTagName('commit')
966 966 if commits:
967 967 lastrev = str(commits[0].getAttribute('revision')) or '0'
968 968 return (lastrev, rev)
969 969
970 970 def _wcrev(self):
971 971 return self._wcrevs()[0]
972 972
973 973 def _wcchanged(self):
974 974 """Return (changes, extchanges, missing) where changes is True
975 975 if the working directory was changed, extchanges is
976 976 True if any of these changes concern an external entry and missing
977 977 is True if any change is a missing entry.
978 978 """
979 979 output, err = self._svncommand(['status', '--xml'])
980 980 externals, changes, missing = [], [], []
981 981 doc = xml.dom.minidom.parseString(output)
982 982 for e in doc.getElementsByTagName('entry'):
983 983 s = e.getElementsByTagName('wc-status')
984 984 if not s:
985 985 continue
986 986 item = s[0].getAttribute('item')
987 987 props = s[0].getAttribute('props')
988 988 path = e.getAttribute('path')
989 989 if item == 'external':
990 990 externals.append(path)
991 991 elif item == 'missing':
992 992 missing.append(path)
993 993 if (item not in ('', 'normal', 'unversioned', 'external')
994 994 or props not in ('', 'none', 'normal')):
995 995 changes.append(path)
996 996 for path in changes:
997 997 for ext in externals:
998 998 if path == ext or path.startswith(ext + os.sep):
999 999 return True, True, bool(missing)
1000 1000 return bool(changes), False, bool(missing)
1001 1001
1002 1002 def dirty(self, ignoreupdate=False):
1003 1003 if not self._wcchanged()[0]:
1004 1004 if self._state[1] in self._wcrevs() or ignoreupdate:
1005 1005 return False
1006 1006 return True
1007 1007
1008 1008 def basestate(self):
1009 1009 lastrev, rev = self._wcrevs()
1010 1010 if lastrev != rev:
1011 1011 # Last committed rev is not the same than rev. We would
1012 1012 # like to take lastrev but we do not know if the subrepo
1013 1013 # URL exists at lastrev. Test it and fallback to rev it
1014 1014 # is not there.
1015 1015 try:
1016 1016 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1017 1017 return lastrev
1018 1018 except error.Abort:
1019 1019 pass
1020 1020 return rev
1021 1021
1022 1022 @annotatesubrepoerror
1023 1023 def commit(self, text, user, date):
1024 1024 # user and date are out of our hands since svn is centralized
1025 1025 changed, extchanged, missing = self._wcchanged()
1026 1026 if not changed:
1027 1027 return self.basestate()
1028 1028 if extchanged:
1029 1029 # Do not try to commit externals
1030 1030 raise util.Abort(_('cannot commit svn externals'))
1031 1031 if missing:
1032 1032 # svn can commit with missing entries but aborting like hg
1033 1033 # seems a better approach.
1034 1034 raise util.Abort(_('cannot commit missing svn entries'))
1035 1035 commitinfo, err = self._svncommand(['commit', '-m', text])
1036 1036 self.ui.status(commitinfo)
1037 1037 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1038 1038 if not newrev:
1039 1039 if not commitinfo.strip():
1040 1040 # Sometimes, our definition of "changed" differs from
1041 1041 # svn one. For instance, svn ignores missing files
1042 1042 # when committing. If there are only missing files, no
1043 1043 # commit is made, no output and no error code.
1044 1044 raise util.Abort(_('failed to commit svn changes'))
1045 1045 raise util.Abort(commitinfo.splitlines()[-1])
1046 1046 newrev = newrev.groups()[0]
1047 1047 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1048 1048 return newrev
1049 1049
1050 1050 @annotatesubrepoerror
1051 1051 def remove(self):
1052 1052 if self.dirty():
1053 1053 self.ui.warn(_('not removing repo %s because '
1054 1054 'it has changes.\n') % self._path)
1055 1055 return
1056 1056 self.ui.note(_('removing subrepo %s\n') % self._path)
1057 1057
1058 1058 def onerror(function, path, excinfo):
1059 1059 if function is not os.remove:
1060 1060 raise
1061 1061 # read-only files cannot be unlinked under Windows
1062 1062 s = os.stat(path)
1063 1063 if (s.st_mode & stat.S_IWRITE) != 0:
1064 1064 raise
1065 1065 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
1066 1066 os.remove(path)
1067 1067
1068 1068 path = self._ctx._repo.wjoin(self._path)
1069 1069 shutil.rmtree(path, onerror=onerror)
1070 1070 try:
1071 1071 os.removedirs(os.path.dirname(path))
1072 1072 except OSError:
1073 1073 pass
1074 1074
1075 1075 @annotatesubrepoerror
1076 1076 def get(self, state, overwrite=False):
1077 1077 if overwrite:
1078 1078 self._svncommand(['revert', '--recursive'])
1079 1079 args = ['checkout']
1080 1080 if self._svnversion >= (1, 5):
1081 1081 args.append('--force')
1082 1082 # The revision must be specified at the end of the URL to properly
1083 1083 # update to a directory which has since been deleted and recreated.
1084 1084 args.append('%s@%s' % (state[0], state[1]))
1085 1085 status, err = self._svncommand(args, failok=True)
1086 1086 _sanitize(self.ui, self._ctx._repo.wjoin(self._path), '.svn')
1087 1087 if not re.search('Checked out revision [0-9]+.', status):
1088 1088 if ('is already a working copy for a different URL' in err
1089 1089 and (self._wcchanged()[:2] == (False, False))):
1090 1090 # obstructed but clean working copy, so just blow it away.
1091 1091 self.remove()
1092 1092 self.get(state, overwrite=False)
1093 1093 return
1094 1094 raise util.Abort((status or err).splitlines()[-1])
1095 1095 self.ui.status(status)
1096 1096
1097 1097 @annotatesubrepoerror
1098 1098 def merge(self, state):
1099 1099 old = self._state[1]
1100 1100 new = state[1]
1101 1101 wcrev = self._wcrev()
1102 1102 if new != wcrev:
1103 1103 dirty = old == wcrev or self._wcchanged()[0]
1104 1104 if _updateprompt(self.ui, self, dirty, wcrev, new):
1105 1105 self.get(state, False)
1106 1106
1107 1107 def push(self, opts):
1108 1108 # push is a no-op for SVN
1109 1109 return True
1110 1110
1111 1111 @annotatesubrepoerror
1112 1112 def files(self):
1113 1113 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1114 1114 doc = xml.dom.minidom.parseString(output)
1115 1115 paths = []
1116 1116 for e in doc.getElementsByTagName('entry'):
1117 1117 kind = str(e.getAttribute('kind'))
1118 1118 if kind != 'file':
1119 1119 continue
1120 1120 name = ''.join(c.data for c
1121 1121 in e.getElementsByTagName('name')[0].childNodes
1122 1122 if c.nodeType == c.TEXT_NODE)
1123 1123 paths.append(name.encode('utf-8'))
1124 1124 return paths
1125 1125
1126 1126 def filedata(self, name):
1127 1127 return self._svncommand(['cat'], name)[0]
1128 1128
1129 1129
1130 1130 class gitsubrepo(abstractsubrepo):
1131 1131 def __init__(self, ctx, path, state):
1132 1132 super(gitsubrepo, self).__init__(ctx._repo.ui)
1133 1133 self._state = state
1134 1134 self._ctx = ctx
1135 1135 self._path = path
1136 1136 self._relpath = os.path.join(reporelpath(ctx._repo), path)
1137 1137 self._abspath = ctx._repo.wjoin(path)
1138 1138 self._subparent = ctx._repo
1139 1139 self._ensuregit()
1140 1140
1141 1141 def _ensuregit(self):
1142 1142 try:
1143 1143 self._gitexecutable = 'git'
1144 1144 out, err = self._gitnodir(['--version'])
1145 1145 except OSError, e:
1146 1146 if e.errno != 2 or os.name != 'nt':
1147 1147 raise
1148 1148 self._gitexecutable = 'git.cmd'
1149 1149 out, err = self._gitnodir(['--version'])
1150 1150 versionstatus = self._checkversion(out)
1151 1151 if versionstatus == 'unknown':
1152 1152 self.ui.warn(_('cannot retrieve git version\n'))
1153 1153 elif versionstatus == 'abort':
1154 1154 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
1155 1155 elif versionstatus == 'warning':
1156 1156 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1157 1157
1158 1158 @staticmethod
1159 1159 def _gitversion(out):
1160 1160 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
1161 1161 if m:
1162 1162 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1163 1163
1164 1164 m = re.search(r'^git version (\d+)\.(\d+)', out)
1165 1165 if m:
1166 1166 return (int(m.group(1)), int(m.group(2)), 0)
1167 1167
1168 1168 return -1
1169 1169
1170 1170 @staticmethod
1171 1171 def _checkversion(out):
1172 1172 '''ensure git version is new enough
1173 1173
1174 1174 >>> _checkversion = gitsubrepo._checkversion
1175 1175 >>> _checkversion('git version 1.6.0')
1176 1176 'ok'
1177 1177 >>> _checkversion('git version 1.8.5')
1178 1178 'ok'
1179 1179 >>> _checkversion('git version 1.4.0')
1180 1180 'abort'
1181 1181 >>> _checkversion('git version 1.5.0')
1182 1182 'warning'
1183 1183 >>> _checkversion('git version 1.9-rc0')
1184 1184 'ok'
1185 1185 >>> _checkversion('git version 1.9.0.265.g81cdec2')
1186 1186 'ok'
1187 1187 >>> _checkversion('git version 1.9.0.GIT')
1188 1188 'ok'
1189 1189 >>> _checkversion('git version 12345')
1190 1190 'unknown'
1191 1191 >>> _checkversion('no')
1192 1192 'unknown'
1193 1193 '''
1194 1194 version = gitsubrepo._gitversion(out)
1195 1195 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1196 1196 # despite the docstring comment. For now, error on 1.4.0, warn on
1197 1197 # 1.5.0 but attempt to continue.
1198 1198 if version == -1:
1199 1199 return 'unknown'
1200 1200 if version < (1, 5, 0):
1201 1201 return 'abort'
1202 1202 elif version < (1, 6, 0):
1203 1203 return 'warning'
1204 1204 return 'ok'
1205 1205
1206 1206 def _gitcommand(self, commands, env=None, stream=False):
1207 1207 return self._gitdir(commands, env=env, stream=stream)[0]
1208 1208
1209 1209 def _gitdir(self, commands, env=None, stream=False):
1210 1210 return self._gitnodir(commands, env=env, stream=stream,
1211 1211 cwd=self._abspath)
1212 1212
1213 1213 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1214 1214 """Calls the git command
1215 1215
1216 1216 The methods tries to call the git command. versions prior to 1.6.0
1217 1217 are not supported and very probably fail.
1218 1218 """
1219 1219 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1220 1220 # unless ui.quiet is set, print git's stderr,
1221 1221 # which is mostly progress and useful info
1222 1222 errpipe = None
1223 1223 if self.ui.quiet:
1224 1224 errpipe = open(os.devnull, 'w')
1225 1225 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1226 1226 cwd=cwd, env=env, close_fds=util.closefds,
1227 1227 stdout=subprocess.PIPE, stderr=errpipe)
1228 1228 if stream:
1229 1229 return p.stdout, None
1230 1230
1231 1231 retdata = p.stdout.read().strip()
1232 1232 # wait for the child to exit to avoid race condition.
1233 1233 p.wait()
1234 1234
1235 1235 if p.returncode != 0 and p.returncode != 1:
1236 1236 # there are certain error codes that are ok
1237 1237 command = commands[0]
1238 1238 if command in ('cat-file', 'symbolic-ref'):
1239 1239 return retdata, p.returncode
1240 1240 # for all others, abort
1241 1241 raise util.Abort('git %s error %d in %s' %
1242 1242 (command, p.returncode, self._relpath))
1243 1243
1244 1244 return retdata, p.returncode
1245 1245
1246 1246 def _gitmissing(self):
1247 1247 return not os.path.exists(os.path.join(self._abspath, '.git'))
1248 1248
1249 1249 def _gitstate(self):
1250 1250 return self._gitcommand(['rev-parse', 'HEAD'])
1251 1251
1252 1252 def _gitcurrentbranch(self):
1253 1253 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1254 1254 if err:
1255 1255 current = None
1256 1256 return current
1257 1257
1258 1258 def _gitremote(self, remote):
1259 1259 out = self._gitcommand(['remote', 'show', '-n', remote])
1260 1260 line = out.split('\n')[1]
1261 1261 i = line.index('URL: ') + len('URL: ')
1262 1262 return line[i:]
1263 1263
1264 1264 def _githavelocally(self, revision):
1265 1265 out, code = self._gitdir(['cat-file', '-e', revision])
1266 1266 return code == 0
1267 1267
1268 1268 def _gitisancestor(self, r1, r2):
1269 1269 base = self._gitcommand(['merge-base', r1, r2])
1270 1270 return base == r1
1271 1271
1272 1272 def _gitisbare(self):
1273 1273 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1274 1274
1275 1275 def _gitupdatestat(self):
1276 1276 """This must be run before git diff-index.
1277 1277 diff-index only looks at changes to file stat;
1278 1278 this command looks at file contents and updates the stat."""
1279 1279 self._gitcommand(['update-index', '-q', '--refresh'])
1280 1280
1281 1281 def _gitbranchmap(self):
1282 1282 '''returns 2 things:
1283 1283 a map from git branch to revision
1284 1284 a map from revision to branches'''
1285 1285 branch2rev = {}
1286 1286 rev2branch = {}
1287 1287
1288 1288 out = self._gitcommand(['for-each-ref', '--format',
1289 1289 '%(objectname) %(refname)'])
1290 1290 for line in out.split('\n'):
1291 1291 revision, ref = line.split(' ')
1292 1292 if (not ref.startswith('refs/heads/') and
1293 1293 not ref.startswith('refs/remotes/')):
1294 1294 continue
1295 1295 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1296 1296 continue # ignore remote/HEAD redirects
1297 1297 branch2rev[ref] = revision
1298 1298 rev2branch.setdefault(revision, []).append(ref)
1299 1299 return branch2rev, rev2branch
1300 1300
1301 1301 def _gittracking(self, branches):
1302 1302 'return map of remote branch to local tracking branch'
1303 1303 # assumes no more than one local tracking branch for each remote
1304 1304 tracking = {}
1305 1305 for b in branches:
1306 1306 if b.startswith('refs/remotes/'):
1307 1307 continue
1308 1308 bname = b.split('/', 2)[2]
1309 1309 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1310 1310 if remote:
1311 1311 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1312 1312 tracking['refs/remotes/%s/%s' %
1313 1313 (remote, ref.split('/', 2)[2])] = b
1314 1314 return tracking
1315 1315
1316 1316 def _abssource(self, source):
1317 1317 if '://' not in source:
1318 1318 # recognize the scp syntax as an absolute source
1319 1319 colon = source.find(':')
1320 1320 if colon != -1 and '/' not in source[:colon]:
1321 1321 return source
1322 1322 self._subsource = source
1323 1323 return _abssource(self)
1324 1324
1325 1325 def _fetch(self, source, revision):
1326 1326 if self._gitmissing():
1327 1327 source = self._abssource(source)
1328 1328 self.ui.status(_('cloning subrepo %s from %s\n') %
1329 1329 (self._relpath, source))
1330 1330 self._gitnodir(['clone', source, self._abspath])
1331 1331 if self._githavelocally(revision):
1332 1332 return
1333 1333 self.ui.status(_('pulling subrepo %s from %s\n') %
1334 1334 (self._relpath, self._gitremote('origin')))
1335 1335 # try only origin: the originally cloned repo
1336 1336 self._gitcommand(['fetch'])
1337 1337 if not self._githavelocally(revision):
1338 1338 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1339 1339 (revision, self._relpath))
1340 1340
1341 1341 @annotatesubrepoerror
1342 1342 def dirty(self, ignoreupdate=False):
1343 1343 if self._gitmissing():
1344 1344 return self._state[1] != ''
1345 1345 if self._gitisbare():
1346 1346 return True
1347 1347 if not ignoreupdate and self._state[1] != self._gitstate():
1348 1348 # different version checked out
1349 1349 return True
1350 1350 # check for staged changes or modified files; ignore untracked files
1351 1351 self._gitupdatestat()
1352 1352 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1353 1353 return code == 1
1354 1354
1355 1355 def basestate(self):
1356 1356 return self._gitstate()
1357 1357
1358 1358 @annotatesubrepoerror
1359 1359 def get(self, state, overwrite=False):
1360 1360 source, revision, kind = state
1361 1361 if not revision:
1362 1362 self.remove()
1363 1363 return
1364 1364 self._fetch(source, revision)
1365 1365 # if the repo was set to be bare, unbare it
1366 1366 if self._gitisbare():
1367 1367 self._gitcommand(['config', 'core.bare', 'false'])
1368 1368 if self._gitstate() == revision:
1369 1369 self._gitcommand(['reset', '--hard', 'HEAD'])
1370 1370 return
1371 1371 elif self._gitstate() == revision:
1372 1372 if overwrite:
1373 1373 # first reset the index to unmark new files for commit, because
1374 1374 # reset --hard will otherwise throw away files added for commit,
1375 1375 # not just unmark them.
1376 1376 self._gitcommand(['reset', 'HEAD'])
1377 1377 self._gitcommand(['reset', '--hard', 'HEAD'])
1378 1378 return
1379 1379 branch2rev, rev2branch = self._gitbranchmap()
1380 1380
1381 1381 def checkout(args):
1382 1382 cmd = ['checkout']
1383 1383 if overwrite:
1384 1384 # first reset the index to unmark new files for commit, because
1385 1385 # the -f option will otherwise throw away files added for
1386 1386 # commit, not just unmark them.
1387 1387 self._gitcommand(['reset', 'HEAD'])
1388 1388 cmd.append('-f')
1389 1389 self._gitcommand(cmd + args)
1390 1390 _sanitize(self.ui, self._abspath, '.git')
1391 1391
1392 1392 def rawcheckout():
1393 1393 # no branch to checkout, check it out with no branch
1394 1394 self.ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1395 1395 self._relpath)
1396 1396 self.ui.warn(_('check out a git branch if you intend '
1397 1397 'to make changes\n'))
1398 1398 checkout(['-q', revision])
1399 1399
1400 1400 if revision not in rev2branch:
1401 1401 rawcheckout()
1402 1402 return
1403 1403 branches = rev2branch[revision]
1404 1404 firstlocalbranch = None
1405 1405 for b in branches:
1406 1406 if b == 'refs/heads/master':
1407 1407 # master trumps all other branches
1408 1408 checkout(['refs/heads/master'])
1409 1409 return
1410 1410 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1411 1411 firstlocalbranch = b
1412 1412 if firstlocalbranch:
1413 1413 checkout([firstlocalbranch])
1414 1414 return
1415 1415
1416 1416 tracking = self._gittracking(branch2rev.keys())
1417 1417 # choose a remote branch already tracked if possible
1418 1418 remote = branches[0]
1419 1419 if remote not in tracking:
1420 1420 for b in branches:
1421 1421 if b in tracking:
1422 1422 remote = b
1423 1423 break
1424 1424
1425 1425 if remote not in tracking:
1426 1426 # create a new local tracking branch
1427 1427 local = remote.split('/', 3)[3]
1428 1428 checkout(['-b', local, remote])
1429 1429 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1430 1430 # When updating to a tracked remote branch,
1431 1431 # if the local tracking branch is downstream of it,
1432 1432 # a normal `git pull` would have performed a "fast-forward merge"
1433 1433 # which is equivalent to updating the local branch to the remote.
1434 1434 # Since we are only looking at branching at update, we need to
1435 1435 # detect this situation and perform this action lazily.
1436 1436 if tracking[remote] != self._gitcurrentbranch():
1437 1437 checkout([tracking[remote]])
1438 1438 self._gitcommand(['merge', '--ff', remote])
1439 1439 _sanitize(self.ui, self._abspath, '.git')
1440 1440 else:
1441 1441 # a real merge would be required, just checkout the revision
1442 1442 rawcheckout()
1443 1443
1444 1444 @annotatesubrepoerror
1445 1445 def commit(self, text, user, date):
1446 1446 if self._gitmissing():
1447 1447 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1448 1448 cmd = ['commit', '-a', '-m', text]
1449 1449 env = os.environ.copy()
1450 1450 if user:
1451 1451 cmd += ['--author', user]
1452 1452 if date:
1453 1453 # git's date parser silently ignores when seconds < 1e9
1454 1454 # convert to ISO8601
1455 1455 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1456 1456 '%Y-%m-%dT%H:%M:%S %1%2')
1457 1457 self._gitcommand(cmd, env=env)
1458 1458 # make sure commit works otherwise HEAD might not exist under certain
1459 1459 # circumstances
1460 1460 return self._gitstate()
1461 1461
1462 1462 @annotatesubrepoerror
1463 1463 def merge(self, state):
1464 1464 source, revision, kind = state
1465 1465 self._fetch(source, revision)
1466 1466 base = self._gitcommand(['merge-base', revision, self._state[1]])
1467 1467 self._gitupdatestat()
1468 1468 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1469 1469
1470 1470 def mergefunc():
1471 1471 if base == revision:
1472 1472 self.get(state) # fast forward merge
1473 1473 elif base != self._state[1]:
1474 1474 self._gitcommand(['merge', '--no-commit', revision])
1475 1475 _sanitize(self.ui, self._abspath, '.git')
1476 1476
1477 1477 if self.dirty():
1478 1478 if self._gitstate() != revision:
1479 1479 dirty = self._gitstate() == self._state[1] or code != 0
1480 1480 if _updateprompt(self.ui, self, dirty,
1481 1481 self._state[1][:7], revision[:7]):
1482 1482 mergefunc()
1483 1483 else:
1484 1484 mergefunc()
1485 1485
1486 1486 @annotatesubrepoerror
1487 1487 def push(self, opts):
1488 1488 force = opts.get('force')
1489 1489
1490 1490 if not self._state[1]:
1491 1491 return True
1492 1492 if self._gitmissing():
1493 1493 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1494 1494 # if a branch in origin contains the revision, nothing to do
1495 1495 branch2rev, rev2branch = self._gitbranchmap()
1496 1496 if self._state[1] in rev2branch:
1497 1497 for b in rev2branch[self._state[1]]:
1498 1498 if b.startswith('refs/remotes/origin/'):
1499 1499 return True
1500 1500 for b, revision in branch2rev.iteritems():
1501 1501 if b.startswith('refs/remotes/origin/'):
1502 1502 if self._gitisancestor(self._state[1], revision):
1503 1503 return True
1504 1504 # otherwise, try to push the currently checked out branch
1505 1505 cmd = ['push']
1506 1506 if force:
1507 1507 cmd.append('--force')
1508 1508
1509 1509 current = self._gitcurrentbranch()
1510 1510 if current:
1511 1511 # determine if the current branch is even useful
1512 1512 if not self._gitisancestor(self._state[1], current):
1513 1513 self.ui.warn(_('unrelated git branch checked out '
1514 1514 'in subrepo %s\n') % self._relpath)
1515 1515 return False
1516 1516 self.ui.status(_('pushing branch %s of subrepo %s\n') %
1517 1517 (current.split('/', 2)[2], self._relpath))
1518 1518 ret = self._gitdir(cmd + ['origin', current])
1519 1519 return ret[1] == 0
1520 1520 else:
1521 1521 self.ui.warn(_('no branch checked out in subrepo %s\n'
1522 1522 'cannot push revision %s\n') %
1523 1523 (self._relpath, self._state[1]))
1524 1524 return False
1525 1525
1526 1526 @annotatesubrepoerror
1527 1527 def remove(self):
1528 1528 if self._gitmissing():
1529 1529 return
1530 1530 if self.dirty():
1531 1531 self.ui.warn(_('not removing repo %s because '
1532 1532 'it has changes.\n') % self._relpath)
1533 1533 return
1534 1534 # we can't fully delete the repository as it may contain
1535 1535 # local-only history
1536 1536 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1537 1537 self._gitcommand(['config', 'core.bare', 'true'])
1538 1538 for f in os.listdir(self._abspath):
1539 1539 if f == '.git':
1540 1540 continue
1541 1541 path = os.path.join(self._abspath, f)
1542 1542 if os.path.isdir(path) and not os.path.islink(path):
1543 1543 shutil.rmtree(path)
1544 1544 else:
1545 1545 os.remove(path)
1546 1546
1547 1547 def archive(self, archiver, prefix, match=None):
1548 1548 total = 0
1549 1549 source, revision = self._state
1550 1550 if not revision:
1551 1551 return total
1552 1552 self._fetch(source, revision)
1553 1553
1554 1554 # Parse git's native archive command.
1555 1555 # This should be much faster than manually traversing the trees
1556 1556 # and objects with many subprocess calls.
1557 1557 tarstream = self._gitcommand(['archive', revision], stream=True)
1558 1558 tar = tarfile.open(fileobj=tarstream, mode='r|')
1559 1559 relpath = subrelpath(self)
1560 1560 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1561 1561 for i, info in enumerate(tar):
1562 1562 if info.isdir():
1563 1563 continue
1564 1564 if match and not match(info.name):
1565 1565 continue
1566 1566 if info.issym():
1567 1567 data = info.linkname
1568 1568 else:
1569 1569 data = tar.extractfile(info).read()
1570 1570 archiver.addfile(os.path.join(prefix, self._path, info.name),
1571 1571 info.mode, info.issym(), data)
1572 1572 total += 1
1573 1573 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1574 1574 unit=_('files'))
1575 1575 self.ui.progress(_('archiving (%s)') % relpath, None)
1576 1576 return total
1577 1577
1578 1578
1579 1579 @annotatesubrepoerror
1580 def cat(self, match, prefix, **opts):
1581 rev = self._state[1]
1582 if match.anypats():
1583 return 1 #No support for include/exclude yet
1584
1585 if not match.files():
1586 return 1
1587
1588 for f in match.files():
1589 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1590 fp = cmdutil.makefileobj(self._subparent, opts.get('output'),
1591 self._ctx.node(),
1592 pathname=os.path.join(prefix, f))
1593 fp.write(output)
1594 fp.close()
1595 return 0
1596
1597
1598 @annotatesubrepoerror
1580 1599 def status(self, rev2, **opts):
1581 1600 rev1 = self._state[1]
1582 1601 if self._gitmissing() or not rev1:
1583 1602 # if the repo is missing, return no results
1584 1603 return [], [], [], [], [], [], []
1585 1604 modified, added, removed = [], [], []
1586 1605 self._gitupdatestat()
1587 1606 if rev2:
1588 1607 command = ['diff-tree', rev1, rev2]
1589 1608 else:
1590 1609 command = ['diff-index', rev1]
1591 1610 out = self._gitcommand(command)
1592 1611 for line in out.split('\n'):
1593 1612 tab = line.find('\t')
1594 1613 if tab == -1:
1595 1614 continue
1596 1615 status, f = line[tab - 1], line[tab + 1:]
1597 1616 if status == 'M':
1598 1617 modified.append(f)
1599 1618 elif status == 'A':
1600 1619 added.append(f)
1601 1620 elif status == 'D':
1602 1621 removed.append(f)
1603 1622
1604 1623 deleted, unknown, ignored, clean = [], [], [], []
1605 1624
1606 1625 if not rev2:
1607 1626 command = ['ls-files', '--others', '--exclude-standard']
1608 1627 out = self._gitcommand(command)
1609 1628 for line in out.split('\n'):
1610 1629 if len(line) == 0:
1611 1630 continue
1612 1631 unknown.append(line)
1613 1632
1614 1633 return scmutil.status(modified, added, removed, deleted,
1615 1634 unknown, ignored, clean)
1616 1635
1617 1636 @annotatesubrepoerror
1618 1637 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1619 1638 node1 = self._state[1]
1620 1639 cmd = ['diff']
1621 1640 if opts['stat']:
1622 1641 cmd.append('--stat')
1623 1642 else:
1624 1643 # for Git, this also implies '-p'
1625 1644 cmd.append('-U%d' % diffopts.context)
1626 1645
1627 1646 gitprefix = os.path.join(prefix, self._path)
1628 1647
1629 1648 if diffopts.noprefix:
1630 1649 cmd.extend(['--src-prefix=%s/' % gitprefix,
1631 1650 '--dst-prefix=%s/' % gitprefix])
1632 1651 else:
1633 1652 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
1634 1653 '--dst-prefix=b/%s/' % gitprefix])
1635 1654
1636 1655 if diffopts.ignorews:
1637 1656 cmd.append('--ignore-all-space')
1638 1657 if diffopts.ignorewsamount:
1639 1658 cmd.append('--ignore-space-change')
1640 1659 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1641 1660 and diffopts.ignoreblanklines:
1642 1661 cmd.append('--ignore-blank-lines')
1643 1662
1644 1663 cmd.append(node1)
1645 1664 if node2:
1646 1665 cmd.append(node2)
1647 1666
1648 1667 if match.anypats():
1649 1668 return #No support for include/exclude yet
1650 1669
1651 1670 output = ""
1652 1671 if match.always():
1653 1672 output += self._gitcommand(cmd) + '\n'
1654 1673 elif match.files():
1655 1674 for f in match.files():
1656 1675 output += self._gitcommand(cmd + [f]) + '\n'
1657 1676 elif match(gitprefix): #Subrepo is matched
1658 1677 output += self._gitcommand(cmd) + '\n'
1659 1678
1660 1679 if output.strip():
1661 1680 ui.write(output)
1662 1681
1663 1682 @annotatesubrepoerror
1664 1683 def revert(self, substate, *pats, **opts):
1665 1684 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1666 1685 if not opts.get('no_backup'):
1667 1686 status = self.status(None)
1668 1687 names = status.modified
1669 1688 for name in names:
1670 1689 bakname = "%s.orig" % name
1671 1690 self.ui.note(_('saving current version of %s as %s\n') %
1672 1691 (name, bakname))
1673 1692 util.rename(os.path.join(self._abspath, name),
1674 1693 os.path.join(self._abspath, bakname))
1675 1694
1676 1695 self.get(substate, overwrite=True)
1677 1696 return []
1678 1697
1679 1698 def shortid(self, revid):
1680 1699 return revid[:7]
1681 1700
1682 1701 types = {
1683 1702 'hg': hgsubrepo,
1684 1703 'svn': svnsubrepo,
1685 1704 'git': gitsubrepo,
1686 1705 }
@@ -1,805 +1,853 b''
1 1 #require git
2 2
3 3 make git commits repeatable
4 4
5 5 $ echo "[core]" >> $HOME/.gitconfig
6 6 $ echo "autocrlf = false" >> $HOME/.gitconfig
7 7 $ GIT_AUTHOR_NAME='test'; export GIT_AUTHOR_NAME
8 8 $ GIT_AUTHOR_EMAIL='test@example.org'; export GIT_AUTHOR_EMAIL
9 9 $ GIT_AUTHOR_DATE='1234567891 +0000'; export GIT_AUTHOR_DATE
10 10 $ GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"; export GIT_COMMITTER_NAME
11 11 $ GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"; export GIT_COMMITTER_EMAIL
12 12 $ GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"; export GIT_COMMITTER_DATE
13 13 $ GIT_CONFIG_NOSYSTEM=1; export GIT_CONFIG_NOSYSTEM
14 14
15 15 root hg repo
16 16
17 17 $ hg init t
18 18 $ cd t
19 19 $ echo a > a
20 20 $ hg add a
21 21 $ hg commit -m a
22 22 $ cd ..
23 23
24 24 new external git repo
25 25
26 26 $ mkdir gitroot
27 27 $ cd gitroot
28 28 $ git init -q
29 29 $ echo g > g
30 30 $ git add g
31 31 $ git commit -q -m g
32 32
33 33 add subrepo clone
34 34
35 35 $ cd ../t
36 36 $ echo 's = [git]../gitroot' > .hgsub
37 37 $ git clone -q ../gitroot s
38 38 $ hg add .hgsub
39 39 $ hg commit -m 'new git subrepo'
40 40 $ hg debugsub
41 41 path s
42 42 source ../gitroot
43 43 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
44 44
45 45 record a new commit from upstream from a different branch
46 46
47 47 $ cd ../gitroot
48 48 $ git checkout -q -b testing
49 49 $ echo gg >> g
50 50 $ git commit -q -a -m gg
51 51
52 52 $ cd ../t/s
53 53 $ git pull -q >/dev/null 2>/dev/null
54 54 $ git checkout -q -b testing origin/testing >/dev/null
55 55
56 56 $ cd ..
57 57 $ hg status --subrepos
58 58 M s/g
59 59 $ hg commit -m 'update git subrepo'
60 60 $ hg debugsub
61 61 path s
62 62 source ../gitroot
63 63 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
64 64
65 65 make $GITROOT pushable, by replacing it with a clone with nothing checked out
66 66
67 67 $ cd ..
68 68 $ git clone gitroot gitrootbare --bare -q
69 69 $ rm -rf gitroot
70 70 $ mv gitrootbare gitroot
71 71
72 72 clone root
73 73
74 74 $ cd t
75 75 $ hg clone . ../tc 2> /dev/null
76 76 updating to branch default
77 77 cloning subrepo s from $TESTTMP/gitroot
78 78 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
79 79 $ cd ../tc
80 80 $ hg debugsub
81 81 path s
82 82 source ../gitroot
83 83 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
84 84
85 85 update to previous substate
86 86
87 87 $ hg update 1 -q
88 88 $ cat s/g
89 89 g
90 90 $ hg debugsub
91 91 path s
92 92 source ../gitroot
93 93 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
94 94
95 95 clone root, make local change
96 96
97 97 $ cd ../t
98 98 $ hg clone . ../ta 2> /dev/null
99 99 updating to branch default
100 100 cloning subrepo s from $TESTTMP/gitroot
101 101 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
102 102
103 103 $ cd ../ta
104 104 $ echo ggg >> s/g
105 105 $ hg status --subrepos
106 106 M s/g
107 107 $ hg diff --subrepos
108 108 diff --git a/s/g b/s/g
109 109 index 089258f..85341ee 100644
110 110 --- a/s/g
111 111 +++ b/s/g
112 112 @@ -1,2 +1,3 @@
113 113 g
114 114 gg
115 115 +ggg
116 116 $ hg commit --subrepos -m ggg
117 117 committing subrepository s
118 118 $ hg debugsub
119 119 path s
120 120 source ../gitroot
121 121 revision 79695940086840c99328513acbe35f90fcd55e57
122 122
123 123 clone root separately, make different local change
124 124
125 125 $ cd ../t
126 126 $ hg clone . ../tb 2> /dev/null
127 127 updating to branch default
128 128 cloning subrepo s from $TESTTMP/gitroot
129 129 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
130 130
131 131 $ cd ../tb/s
132 132 $ hg status --subrepos
133 133 $ echo f > f
134 134 $ hg status --subrepos
135 135 ? s/f
136 136 $ hg add .
137 137 $ git add f
138 138 $ cd ..
139 139
140 140 $ hg status --subrepos
141 141 A s/f
142 142 $ hg commit --subrepos -m f
143 143 committing subrepository s
144 144 $ hg debugsub
145 145 path s
146 146 source ../gitroot
147 147 revision aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
148 148
149 149 user b push changes
150 150
151 151 $ hg push 2>/dev/null
152 152 pushing to $TESTTMP/t (glob)
153 153 pushing branch testing of subrepo s
154 154 searching for changes
155 155 adding changesets
156 156 adding manifests
157 157 adding file changes
158 158 added 1 changesets with 1 changes to 1 files
159 159
160 160 user a pulls, merges, commits
161 161
162 162 $ cd ../ta
163 163 $ hg pull
164 164 pulling from $TESTTMP/t (glob)
165 165 searching for changes
166 166 adding changesets
167 167 adding manifests
168 168 adding file changes
169 169 added 1 changesets with 1 changes to 1 files (+1 heads)
170 170 (run 'hg heads' to see heads, 'hg merge' to merge)
171 171 $ hg merge 2>/dev/null
172 172 subrepository s diverged (local revision: 7969594, remote revision: aa84837)
173 173 (M)erge, keep (l)ocal or keep (r)emote? m
174 174 pulling subrepo s from $TESTTMP/gitroot
175 175 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
176 176 (branch merge, don't forget to commit)
177 177 $ cat s/f
178 178 f
179 179 $ cat s/g
180 180 g
181 181 gg
182 182 ggg
183 183 $ hg commit --subrepos -m 'merge'
184 184 committing subrepository s
185 185 $ hg status --subrepos --rev 1:5
186 186 M .hgsubstate
187 187 M s/g
188 188 A s/f
189 189 $ hg debugsub
190 190 path s
191 191 source ../gitroot
192 192 revision f47b465e1bce645dbf37232a00574aa1546ca8d3
193 193 $ hg push 2>/dev/null
194 194 pushing to $TESTTMP/t (glob)
195 195 pushing branch testing of subrepo s
196 196 searching for changes
197 197 adding changesets
198 198 adding manifests
199 199 adding file changes
200 200 added 2 changesets with 2 changes to 1 files
201 201
202 202 make upstream git changes
203 203
204 204 $ cd ..
205 205 $ git clone -q gitroot gitclone
206 206 $ cd gitclone
207 207 $ echo ff >> f
208 208 $ git commit -q -a -m ff
209 209 $ echo fff >> f
210 210 $ git commit -q -a -m fff
211 211 $ git push origin testing 2>/dev/null
212 212
213 213 make and push changes to hg without updating the subrepo
214 214
215 215 $ cd ../t
216 216 $ hg clone . ../td 2>&1 | egrep -v '^Cloning into|^done\.'
217 217 updating to branch default
218 218 cloning subrepo s from $TESTTMP/gitroot
219 219 checking out detached HEAD in subrepo s
220 220 check out a git branch if you intend to make changes
221 221 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
222 222 $ cd ../td
223 223 $ echo aa >> a
224 224 $ hg commit -m aa
225 225 $ hg push
226 226 pushing to $TESTTMP/t (glob)
227 227 searching for changes
228 228 adding changesets
229 229 adding manifests
230 230 adding file changes
231 231 added 1 changesets with 1 changes to 1 files
232 232
233 233 sync to upstream git, distribute changes
234 234
235 235 $ cd ../ta
236 236 $ hg pull -u -q
237 237 $ cd s
238 238 $ git pull -q >/dev/null 2>/dev/null
239 239 $ cd ..
240 240 $ hg commit -m 'git upstream sync'
241 241 $ hg debugsub
242 242 path s
243 243 source ../gitroot
244 244 revision 32a343883b74769118bb1d3b4b1fbf9156f4dddc
245 245 $ hg push -q
246 246
247 247 $ cd ../tb
248 248 $ hg pull -q
249 249 $ hg update 2>/dev/null
250 250 pulling subrepo s from $TESTTMP/gitroot
251 251 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
252 252 $ hg debugsub
253 253 path s
254 254 source ../gitroot
255 255 revision 32a343883b74769118bb1d3b4b1fbf9156f4dddc
256 256
257 257 create a new git branch
258 258
259 259 $ cd s
260 260 $ git checkout -b b2
261 261 Switched to a new branch 'b2'
262 262 $ echo a>a
263 263 $ git add a
264 264 $ git commit -qm 'add a'
265 265 $ cd ..
266 266 $ hg commit -m 'add branch in s'
267 267
268 268 pulling new git branch should not create tracking branch named 'origin/b2'
269 269 (issue3870)
270 270 $ cd ../td/s
271 271 $ git remote set-url origin $TESTTMP/tb/s
272 272 $ git branch --no-track oldtesting
273 273 $ cd ..
274 274 $ hg pull -q ../tb
275 275 $ hg up
276 276 From $TESTTMP/tb/s
277 277 * [new branch] b2 -> origin/b2
278 278 Previous HEAD position was f47b465... merge
279 279 Switched to a new branch 'b2'
280 280 pulling subrepo s from $TESTTMP/tb/s
281 281 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
282 282
283 283 update to a revision without the subrepo, keeping the local git repository
284 284
285 285 $ cd ../t
286 286 $ hg up 0
287 287 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
288 288 $ ls -a s
289 289 .
290 290 ..
291 291 .git
292 292
293 293 $ hg up 2
294 294 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
295 295 $ ls -a s
296 296 .
297 297 ..
298 298 .git
299 299 g
300 300
301 301 archive subrepos
302 302
303 303 $ cd ../tc
304 304 $ hg pull -q
305 305 $ hg archive --subrepos -r 5 ../archive 2>/dev/null
306 306 pulling subrepo s from $TESTTMP/gitroot
307 307 $ cd ../archive
308 308 $ cat s/f
309 309 f
310 310 $ cat s/g
311 311 g
312 312 gg
313 313 ggg
314 314
315 315 $ hg -R ../tc archive --subrepo -r 5 -X ../tc/**f ../archive_x 2>/dev/null
316 316 $ find ../archive_x | sort | grep -v pax_global_header
317 317 ../archive_x
318 318 ../archive_x/.hg_archival.txt
319 319 ../archive_x/.hgsub
320 320 ../archive_x/.hgsubstate
321 321 ../archive_x/a
322 322 ../archive_x/s
323 323 ../archive_x/s/g
324 324
325 325 create nested repo
326 326
327 327 $ cd ..
328 328 $ hg init outer
329 329 $ cd outer
330 330 $ echo b>b
331 331 $ hg add b
332 332 $ hg commit -m b
333 333
334 334 $ hg clone ../t inner 2> /dev/null
335 335 updating to branch default
336 336 cloning subrepo s from $TESTTMP/gitroot
337 337 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
338 338 $ echo inner = inner > .hgsub
339 339 $ hg add .hgsub
340 340 $ hg commit -m 'nested sub'
341 341
342 342 nested commit
343 343
344 344 $ echo ffff >> inner/s/f
345 345 $ hg status --subrepos
346 346 M inner/s/f
347 347 $ hg commit --subrepos -m nested
348 348 committing subrepository inner
349 349 committing subrepository inner/s (glob)
350 350
351 351 nested archive
352 352
353 353 $ hg archive --subrepos ../narchive
354 354 $ ls ../narchive/inner/s | grep -v pax_global_header
355 355 f
356 356 g
357 357
358 358 relative source expansion
359 359
360 360 $ cd ..
361 361 $ mkdir d
362 362 $ hg clone t d/t 2> /dev/null
363 363 updating to branch default
364 364 cloning subrepo s from $TESTTMP/gitroot
365 365 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
366 366
367 367 Don't crash if the subrepo is missing
368 368
369 369 $ hg clone t missing -q
370 370 $ cd missing
371 371 $ rm -rf s
372 372 $ hg status -S
373 373 $ hg sum | grep commit
374 374 commit: 1 subrepos
375 375 $ hg push -q
376 376 abort: subrepo s is missing (in subrepo s)
377 377 [255]
378 378 $ hg commit --subrepos -qm missing
379 379 abort: subrepo s is missing (in subrepo s)
380 380 [255]
381 381 $ hg update -C 2> /dev/null
382 382 cloning subrepo s from $TESTTMP/gitroot
383 383 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
384 384 $ hg sum | grep commit
385 385 commit: (clean)
386 386
387 387 Don't crash if the .hgsubstate entry is missing
388 388
389 389 $ hg update 1 -q
390 390 $ hg rm .hgsubstate
391 391 $ hg commit .hgsubstate -m 'no substate'
392 392 nothing changed
393 393 [1]
394 394 $ hg tag -l nosubstate
395 395 $ hg manifest
396 396 .hgsub
397 397 .hgsubstate
398 398 a
399 399
400 400 $ hg status -S
401 401 R .hgsubstate
402 402 $ hg sum | grep commit
403 403 commit: 1 removed, 1 subrepos (new branch head)
404 404
405 405 $ hg commit -m 'restore substate'
406 406 nothing changed
407 407 [1]
408 408 $ hg manifest
409 409 .hgsub
410 410 .hgsubstate
411 411 a
412 412 $ hg sum | grep commit
413 413 commit: 1 removed, 1 subrepos (new branch head)
414 414
415 415 $ hg update -qC nosubstate
416 416 $ ls s
417 417 g
418 418
419 419 issue3109: false positives in git diff-index
420 420
421 421 $ hg update -q
422 422 $ touch -t 200001010000 s/g
423 423 $ hg status --subrepos
424 424 $ touch -t 200001010000 s/g
425 425 $ hg sum | grep commit
426 426 commit: (clean)
427 427
428 428 Check hg update --clean
429 429 $ cd $TESTTMP/ta
430 430 $ echo > s/g
431 431 $ cd s
432 432 $ echo c1 > f1
433 433 $ echo c1 > f2
434 434 $ git add f1
435 435 $ cd ..
436 436 $ hg status -S
437 437 M s/g
438 438 A s/f1
439 439 ? s/f2
440 440 $ ls s
441 441 f
442 442 f1
443 443 f2
444 444 g
445 445 $ hg update --clean
446 446 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
447 447 $ hg status -S
448 448 ? s/f1
449 449 ? s/f2
450 450 $ ls s
451 451 f
452 452 f1
453 453 f2
454 454 g
455 455
456 456 Sticky subrepositories, no changes
457 457 $ cd $TESTTMP/ta
458 458 $ hg id -n
459 459 7
460 460 $ cd s
461 461 $ git rev-parse HEAD
462 462 32a343883b74769118bb1d3b4b1fbf9156f4dddc
463 463 $ cd ..
464 464 $ hg update 1 > /dev/null 2>&1
465 465 $ hg id -n
466 466 1
467 467 $ cd s
468 468 $ git rev-parse HEAD
469 469 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
470 470 $ cd ..
471 471
472 472 Sticky subrepositories, file changes
473 473 $ touch s/f1
474 474 $ cd s
475 475 $ git add f1
476 476 $ cd ..
477 477 $ hg id -n
478 478 1+
479 479 $ cd s
480 480 $ git rev-parse HEAD
481 481 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
482 482 $ cd ..
483 483 $ hg update 4
484 484 subrepository s diverged (local revision: da5f5b1, remote revision: aa84837)
485 485 (M)erge, keep (l)ocal or keep (r)emote? m
486 486 subrepository sources for s differ
487 487 use (l)ocal source (da5f5b1) or (r)emote source (aa84837)? l
488 488 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
489 489 $ hg id -n
490 490 4+
491 491 $ cd s
492 492 $ git rev-parse HEAD
493 493 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
494 494 $ cd ..
495 495 $ hg update --clean tip > /dev/null 2>&1
496 496
497 497 Sticky subrepository, revision updates
498 498 $ hg id -n
499 499 7
500 500 $ cd s
501 501 $ git rev-parse HEAD
502 502 32a343883b74769118bb1d3b4b1fbf9156f4dddc
503 503 $ cd ..
504 504 $ cd s
505 505 $ git checkout aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
506 506 Previous HEAD position was 32a3438... fff
507 507 HEAD is now at aa84837... f
508 508 $ cd ..
509 509 $ hg update 1
510 510 subrepository s diverged (local revision: 32a3438, remote revision: da5f5b1)
511 511 (M)erge, keep (l)ocal or keep (r)emote? m
512 512 subrepository sources for s differ (in checked out version)
513 513 use (l)ocal source (32a3438) or (r)emote source (da5f5b1)? l
514 514 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
515 515 $ hg id -n
516 516 1+
517 517 $ cd s
518 518 $ git rev-parse HEAD
519 519 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
520 520 $ cd ..
521 521
522 522 Sticky subrepository, file changes and revision updates
523 523 $ touch s/f1
524 524 $ cd s
525 525 $ git add f1
526 526 $ git rev-parse HEAD
527 527 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
528 528 $ cd ..
529 529 $ hg id -n
530 530 1+
531 531 $ hg update 7
532 532 subrepository s diverged (local revision: 32a3438, remote revision: 32a3438)
533 533 (M)erge, keep (l)ocal or keep (r)emote? m
534 534 subrepository sources for s differ
535 535 use (l)ocal source (32a3438) or (r)emote source (32a3438)? l
536 536 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
537 537 $ hg id -n
538 538 7+
539 539 $ cd s
540 540 $ git rev-parse HEAD
541 541 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
542 542 $ cd ..
543 543
544 544 Sticky repository, update --clean
545 545 $ hg update --clean tip 2>/dev/null
546 546 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
547 547 $ hg id -n
548 548 7
549 549 $ cd s
550 550 $ git rev-parse HEAD
551 551 32a343883b74769118bb1d3b4b1fbf9156f4dddc
552 552 $ cd ..
553 553
554 554 Test subrepo already at intended revision:
555 555 $ cd s
556 556 $ git checkout 32a343883b74769118bb1d3b4b1fbf9156f4dddc
557 557 HEAD is now at 32a3438... fff
558 558 $ cd ..
559 559 $ hg update 1
560 560 Previous HEAD position was 32a3438... fff
561 561 HEAD is now at da5f5b1... g
562 562 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
563 563 $ hg id -n
564 564 1
565 565 $ cd s
566 566 $ git rev-parse HEAD
567 567 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
568 568 $ cd ..
569 569
570 570 Test forgetting files, not implemented in git subrepo, used to
571 571 traceback
572 572 #if no-windows
573 573 $ hg forget 'notafile*'
574 574 notafile*: No such file or directory
575 575 [1]
576 576 #else
577 577 $ hg forget 'notafile'
578 578 notafile: * (glob)
579 579 [1]
580 580 #endif
581 581
582 582 $ cd ..
583 583
584 584 Test sanitizing ".hg/hgrc" in subrepo
585 585
586 586 $ cd t
587 587 $ hg tip -q
588 588 7:af6d2edbb0d3
589 589 $ hg update -q -C af6d2edbb0d3
590 590 $ cd s
591 591 $ git checkout -q -b sanitize-test
592 592 $ mkdir .hg
593 593 $ echo '.hg/hgrc in git repo' > .hg/hgrc
594 594 $ mkdir -p sub/.hg
595 595 $ echo 'sub/.hg/hgrc in git repo' > sub/.hg/hgrc
596 596 $ git add .hg sub
597 597 $ git commit -qm 'add .hg/hgrc to be sanitized at hg update'
598 598 $ git push -q origin sanitize-test
599 599 $ cd ..
600 600 $ grep ' s$' .hgsubstate
601 601 32a343883b74769118bb1d3b4b1fbf9156f4dddc s
602 602 $ hg commit -qm 'commit with git revision including .hg/hgrc'
603 603 $ hg parents -q
604 604 8:3473d20bddcf
605 605 $ grep ' s$' .hgsubstate
606 606 c4069473b459cf27fd4d7c2f50c4346b4e936599 s
607 607 $ cd ..
608 608
609 609 $ hg -R tc pull -q
610 610 $ hg -R tc update -q -C 3473d20bddcf 2>&1 | sort
611 611 warning: removing potentially hostile 'hgrc' in '$TESTTMP/tc/s/.hg' (glob)
612 612 warning: removing potentially hostile 'hgrc' in '$TESTTMP/tc/s/sub/.hg' (glob)
613 613 $ cd tc
614 614 $ hg parents -q
615 615 8:3473d20bddcf
616 616 $ grep ' s$' .hgsubstate
617 617 c4069473b459cf27fd4d7c2f50c4346b4e936599 s
618 618 $ test -f s/.hg/hgrc
619 619 [1]
620 620 $ test -f s/sub/.hg/hgrc
621 621 [1]
622 622 $ cd ..
623 623
624 624 additional test for "git merge --ff" route:
625 625
626 626 $ cd t
627 627 $ hg tip -q
628 628 8:3473d20bddcf
629 629 $ hg update -q -C af6d2edbb0d3
630 630 $ cd s
631 631 $ git checkout -q testing
632 632 $ mkdir .hg
633 633 $ echo '.hg/hgrc in git repo' > .hg/hgrc
634 634 $ mkdir -p sub/.hg
635 635 $ echo 'sub/.hg/hgrc in git repo' > sub/.hg/hgrc
636 636 $ git add .hg sub
637 637 $ git commit -qm 'add .hg/hgrc to be sanitized at hg update (git merge --ff)'
638 638 $ git push -q origin testing
639 639 $ cd ..
640 640 $ grep ' s$' .hgsubstate
641 641 32a343883b74769118bb1d3b4b1fbf9156f4dddc s
642 642 $ hg commit -qm 'commit with git revision including .hg/hgrc'
643 643 $ hg parents -q
644 644 9:ed23f7fe024e
645 645 $ grep ' s$' .hgsubstate
646 646 f262643c1077219fbd3858d54e78ef050ef84fbf s
647 647 $ cd ..
648 648
649 649 $ cd tc
650 650 $ hg update -q -C af6d2edbb0d3
651 651 $ test -f s/.hg/hgrc
652 652 [1]
653 653 $ test -f s/sub/.hg/hgrc
654 654 [1]
655 655 $ cd ..
656 656 $ hg -R tc pull -q
657 657 $ hg -R tc update -q -C ed23f7fe024e 2>&1 | sort
658 658 warning: removing potentially hostile 'hgrc' in '$TESTTMP/tc/s/.hg' (glob)
659 659 warning: removing potentially hostile 'hgrc' in '$TESTTMP/tc/s/sub/.hg' (glob)
660 660 $ cd tc
661 661 $ hg parents -q
662 662 9:ed23f7fe024e
663 663 $ grep ' s$' .hgsubstate
664 664 f262643c1077219fbd3858d54e78ef050ef84fbf s
665 665 $ test -f s/.hg/hgrc
666 666 [1]
667 667 $ test -f s/sub/.hg/hgrc
668 668 [1]
669 669
670 670 Test that sanitizing is omitted in meta data area:
671 671
672 672 $ mkdir s/.git/.hg
673 673 $ echo '.hg/hgrc in git metadata area' > s/.git/.hg/hgrc
674 674 $ hg update -q -C af6d2edbb0d3
675 675 checking out detached HEAD in subrepo s
676 676 check out a git branch if you intend to make changes
677 677
678 678 check differences made by most recent change
679 679 $ cd s
680 680 $ cat > foobar << EOF
681 681 > woopwoop
682 682 >
683 683 > foo
684 684 > bar
685 685 > EOF
686 686 $ git add foobar
687 687 $ cd ..
688 688
689 689 $ hg diff --subrepos
690 690 diff --git a/s/foobar b/s/foobar
691 691 new file mode 100644
692 692 index 0000000..8a5a5e2
693 693 --- /dev/null
694 694 +++ b/s/foobar
695 695 @@ -0,0 +1,4 @@
696 696 +woopwoop
697 697 +
698 698 +foo
699 699 +bar
700 700
701 701 $ hg commit --subrepos -m "Added foobar"
702 702 committing subrepository s
703 703 created new head
704 704
705 705 $ hg diff -c . --subrepos --nodates
706 706 diff -r af6d2edbb0d3 -r 255ee8cf690e .hgsubstate
707 707 --- a/.hgsubstate
708 708 +++ b/.hgsubstate
709 709 @@ -1,1 +1,1 @@
710 710 -32a343883b74769118bb1d3b4b1fbf9156f4dddc s
711 711 +fd4dbf828a5b2fcd36b2bcf21ea773820970d129 s
712 712 diff --git a/s/foobar b/s/foobar
713 713 new file mode 100644
714 714 index 0000000..8a5a5e2
715 715 --- /dev/null
716 716 +++ b/s/foobar
717 717 @@ -0,0 +1,4 @@
718 718 +woopwoop
719 719 +
720 720 +foo
721 721 +bar
722 722
723 723 check output when only diffing the subrepository
724 724 $ hg diff -c . --subrepos s
725 725 diff --git a/s/foobar b/s/foobar
726 726 new file mode 100644
727 727 index 0000000..8a5a5e2
728 728 --- /dev/null
729 729 +++ b/s/foobar
730 730 @@ -0,0 +1,4 @@
731 731 +woopwoop
732 732 +
733 733 +foo
734 734 +bar
735 735
736 736 check output when diffing something else
737 737 $ hg diff -c . --subrepos .hgsubstate --nodates
738 738 diff -r af6d2edbb0d3 -r 255ee8cf690e .hgsubstate
739 739 --- a/.hgsubstate
740 740 +++ b/.hgsubstate
741 741 @@ -1,1 +1,1 @@
742 742 -32a343883b74769118bb1d3b4b1fbf9156f4dddc s
743 743 +fd4dbf828a5b2fcd36b2bcf21ea773820970d129 s
744 744
745 745 add new changes, including whitespace
746 746 $ cd s
747 747 $ cat > foobar << EOF
748 748 > woop woop
749 749 >
750 750 > foo
751 751 > bar
752 752 > EOF
753 753 $ echo foo > barfoo
754 754 $ git add barfoo
755 755 $ cd ..
756 756
757 757 $ hg diff --subrepos --ignore-all-space
758 758 diff --git a/s/barfoo b/s/barfoo
759 759 new file mode 100644
760 760 index 0000000..257cc56
761 761 --- /dev/null
762 762 +++ b/s/barfoo
763 763 @@ -0,0 +1 @@
764 764 +foo
765 765 $ hg diff --subrepos s/foobar
766 766 diff --git a/s/foobar b/s/foobar
767 767 index 8a5a5e2..bd5812a 100644
768 768 --- a/s/foobar
769 769 +++ b/s/foobar
770 770 @@ -1,4 +1,4 @@
771 771 -woopwoop
772 772 +woop woop
773 773
774 774 foo
775 775 bar
776 776
777 777 execute a diffstat
778 778 the output contains a regex, because git 1.7.10 and 1.7.11
779 779 change the amount of whitespace
780 780 $ hg diff --subrepos --stat
781 781 \s*barfoo |\s*1 + (re)
782 782 \s*foobar |\s*2 +- (re)
783 783 2 files changed, 2 insertions\(\+\), 1 deletions?\(-\) (re)
784 784
785 785 ensure adding include/exclude ignores the subrepo
786 786 $ hg diff --subrepos -I s/foobar
787 787 $ hg diff --subrepos -X s/foobar
788 788
789 789 revert the subrepository
790 790 $ hg revert --all
791 791 reverting subrepo ../gitroot
792 792
793 793 $ hg status --subrepos
794 794 ? s/barfoo
795 795 ? s/foobar.orig
796 796
797 797 $ mv s/foobar.orig s/foobar
798 798
799 799 $ hg revert --no-backup s
800 800 reverting subrepo ../gitroot
801 801
802 802 $ hg status --subrepos
803 803 ? s/barfoo
804 804
805 show file at specific revision
806 $ cat > s/foobar << EOF
807 > woop woop
808 > fooo bar
809 > EOF
810 $ hg commit --subrepos -m "updated foobar"
811 committing subrepository s
812 $ cat > s/foobar << EOF
813 > current foobar
814 > (should not be visible using hg cat)
815 > EOF
816
817 $ hg cat -r . s/foobar
818 woop woop
819 fooo bar (no-eol)
820 $ hg cat -r "parents(.)" s/foobar > catparents
821
822 $ mkdir -p tmp/s
823
824 $ hg cat -r "parents(.)" --output tmp/%% s/foobar
825 $ diff tmp/% catparents
826
827 $ hg cat -r "parents(.)" --output tmp/%s s/foobar
828 $ diff tmp/foobar catparents
829
830 $ hg cat -r "parents(.)" --output tmp/%d/otherfoobar s/foobar
831 $ diff tmp/s/otherfoobar catparents
832
833 $ hg cat -r "parents(.)" --output tmp/%p s/foobar
834 $ diff tmp/s/foobar catparents
835
836 $ hg cat -r "parents(.)" --output tmp/%H s/foobar
837 $ diff tmp/255ee8cf690ec86e99b1e80147ea93ece117cd9d catparents
838
839 $ hg cat -r "parents(.)" --output tmp/%R s/foobar
840 $ diff tmp/10 catparents
841
842 $ hg cat -r "parents(.)" --output tmp/%h s/foobar
843 $ diff tmp/255ee8cf690e catparents
844
845 $ rm tmp/10
846 $ hg cat -r "parents(.)" --output tmp/%r s/foobar
847 $ diff tmp/10 catparents
848
849 $ mkdir tmp/tc
850 $ hg cat -r "parents(.)" --output tmp/%b/foobar s/foobar
851 $ diff tmp/tc/foobar catparents
852
805 853 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now