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