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