##// END OF EJS Templates
subrepo: don't crash when git repo is missing
Eric Eisner -
r13553:dea6efdd stable
parent child Browse files
Show More
@@ -1,1001 +1,1015 b''
1 # subrepo.py - sub-repository handling for Mercurial
1 # subrepo.py - sub-repository handling for Mercurial
2 #
2 #
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import errno, os, re, xml.dom.minidom, shutil, urlparse, posixpath
8 import errno, os, re, xml.dom.minidom, shutil, urlparse, posixpath
9 import stat, subprocess, tarfile
9 import stat, subprocess, tarfile
10 from i18n import _
10 from i18n import _
11 import config, util, node, error, cmdutil
11 import config, util, node, error, cmdutil
12 hg = None
12 hg = None
13
13
14 nullstate = ('', '', 'empty')
14 nullstate = ('', '', 'empty')
15
15
16 def state(ctx, ui):
16 def state(ctx, ui):
17 """return a state dict, mapping subrepo paths configured in .hgsub
17 """return a state dict, mapping subrepo paths configured in .hgsub
18 to tuple: (source from .hgsub, revision from .hgsubstate, kind
18 to tuple: (source from .hgsub, revision from .hgsubstate, kind
19 (key in types dict))
19 (key in types dict))
20 """
20 """
21 p = config.config()
21 p = config.config()
22 def read(f, sections=None, remap=None):
22 def read(f, sections=None, remap=None):
23 if f in ctx:
23 if f in ctx:
24 try:
24 try:
25 data = ctx[f].data()
25 data = ctx[f].data()
26 except IOError, err:
26 except IOError, err:
27 if err.errno != errno.ENOENT:
27 if err.errno != errno.ENOENT:
28 raise
28 raise
29 # handle missing subrepo spec files as removed
29 # handle missing subrepo spec files as removed
30 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
30 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
31 return
31 return
32 p.parse(f, data, sections, remap, read)
32 p.parse(f, data, sections, remap, read)
33 else:
33 else:
34 raise util.Abort(_("subrepo spec file %s not found") % f)
34 raise util.Abort(_("subrepo spec file %s not found") % f)
35
35
36 if '.hgsub' in ctx:
36 if '.hgsub' in ctx:
37 read('.hgsub')
37 read('.hgsub')
38
38
39 for path, src in ui.configitems('subpaths'):
39 for path, src in ui.configitems('subpaths'):
40 p.set('subpaths', path, src, ui.configsource('subpaths', path))
40 p.set('subpaths', path, src, ui.configsource('subpaths', path))
41
41
42 rev = {}
42 rev = {}
43 if '.hgsubstate' in ctx:
43 if '.hgsubstate' in ctx:
44 try:
44 try:
45 for l in ctx['.hgsubstate'].data().splitlines():
45 for l in ctx['.hgsubstate'].data().splitlines():
46 revision, path = l.split(" ", 1)
46 revision, path = l.split(" ", 1)
47 rev[path] = revision
47 rev[path] = revision
48 except IOError, err:
48 except IOError, err:
49 if err.errno != errno.ENOENT:
49 if err.errno != errno.ENOENT:
50 raise
50 raise
51
51
52 state = {}
52 state = {}
53 for path, src in p[''].items():
53 for path, src in p[''].items():
54 kind = 'hg'
54 kind = 'hg'
55 if src.startswith('['):
55 if src.startswith('['):
56 if ']' not in src:
56 if ']' not in src:
57 raise util.Abort(_('missing ] in subrepo source'))
57 raise util.Abort(_('missing ] in subrepo source'))
58 kind, src = src.split(']', 1)
58 kind, src = src.split(']', 1)
59 kind = kind[1:]
59 kind = kind[1:]
60
60
61 for pattern, repl in p.items('subpaths'):
61 for pattern, repl in p.items('subpaths'):
62 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
62 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
63 # does a string decode.
63 # does a string decode.
64 repl = repl.encode('string-escape')
64 repl = repl.encode('string-escape')
65 # However, we still want to allow back references to go
65 # However, we still want to allow back references to go
66 # through unharmed, so we turn r'\\1' into r'\1'. Again,
66 # through unharmed, so we turn r'\\1' into r'\1'. Again,
67 # extra escapes are needed because re.sub string decodes.
67 # extra escapes are needed because re.sub string decodes.
68 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
68 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
69 try:
69 try:
70 src = re.sub(pattern, repl, src, 1)
70 src = re.sub(pattern, repl, src, 1)
71 except re.error, e:
71 except re.error, e:
72 raise util.Abort(_("bad subrepository pattern in %s: %s")
72 raise util.Abort(_("bad subrepository pattern in %s: %s")
73 % (p.source('subpaths', pattern), e))
73 % (p.source('subpaths', pattern), e))
74
74
75 state[path] = (src.strip(), rev.get(path, ''), kind)
75 state[path] = (src.strip(), rev.get(path, ''), kind)
76
76
77 return state
77 return state
78
78
79 def writestate(repo, state):
79 def writestate(repo, state):
80 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
80 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
81 repo.wwrite('.hgsubstate',
81 repo.wwrite('.hgsubstate',
82 ''.join(['%s %s\n' % (state[s][1], s)
82 ''.join(['%s %s\n' % (state[s][1], s)
83 for s in sorted(state)]), '')
83 for s in sorted(state)]), '')
84
84
85 def submerge(repo, wctx, mctx, actx, overwrite):
85 def submerge(repo, wctx, mctx, actx, overwrite):
86 """delegated from merge.applyupdates: merging of .hgsubstate file
86 """delegated from merge.applyupdates: merging of .hgsubstate file
87 in working context, merging context and ancestor context"""
87 in working context, merging context and ancestor context"""
88 if mctx == actx: # backwards?
88 if mctx == actx: # backwards?
89 actx = wctx.p1()
89 actx = wctx.p1()
90 s1 = wctx.substate
90 s1 = wctx.substate
91 s2 = mctx.substate
91 s2 = mctx.substate
92 sa = actx.substate
92 sa = actx.substate
93 sm = {}
93 sm = {}
94
94
95 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
95 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
96
96
97 def debug(s, msg, r=""):
97 def debug(s, msg, r=""):
98 if r:
98 if r:
99 r = "%s:%s:%s" % r
99 r = "%s:%s:%s" % r
100 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
100 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
101
101
102 for s, l in s1.items():
102 for s, l in s1.items():
103 a = sa.get(s, nullstate)
103 a = sa.get(s, nullstate)
104 ld = l # local state with possible dirty flag for compares
104 ld = l # local state with possible dirty flag for compares
105 if wctx.sub(s).dirty():
105 if wctx.sub(s).dirty():
106 ld = (l[0], l[1] + "+")
106 ld = (l[0], l[1] + "+")
107 if wctx == actx: # overwrite
107 if wctx == actx: # overwrite
108 a = ld
108 a = ld
109
109
110 if s in s2:
110 if s in s2:
111 r = s2[s]
111 r = s2[s]
112 if ld == r or r == a: # no change or local is newer
112 if ld == r or r == a: # no change or local is newer
113 sm[s] = l
113 sm[s] = l
114 continue
114 continue
115 elif ld == a: # other side changed
115 elif ld == a: # other side changed
116 debug(s, "other changed, get", r)
116 debug(s, "other changed, get", r)
117 wctx.sub(s).get(r, overwrite)
117 wctx.sub(s).get(r, overwrite)
118 sm[s] = r
118 sm[s] = r
119 elif ld[0] != r[0]: # sources differ
119 elif ld[0] != r[0]: # sources differ
120 if repo.ui.promptchoice(
120 if repo.ui.promptchoice(
121 _(' subrepository sources for %s differ\n'
121 _(' subrepository sources for %s differ\n'
122 'use (l)ocal source (%s) or (r)emote source (%s)?')
122 'use (l)ocal source (%s) or (r)emote source (%s)?')
123 % (s, l[0], r[0]),
123 % (s, l[0], r[0]),
124 (_('&Local'), _('&Remote')), 0):
124 (_('&Local'), _('&Remote')), 0):
125 debug(s, "prompt changed, get", r)
125 debug(s, "prompt changed, get", r)
126 wctx.sub(s).get(r, overwrite)
126 wctx.sub(s).get(r, overwrite)
127 sm[s] = r
127 sm[s] = r
128 elif ld[1] == a[1]: # local side is unchanged
128 elif ld[1] == a[1]: # local side is unchanged
129 debug(s, "other side changed, get", r)
129 debug(s, "other side changed, get", r)
130 wctx.sub(s).get(r, overwrite)
130 wctx.sub(s).get(r, overwrite)
131 sm[s] = r
131 sm[s] = r
132 else:
132 else:
133 debug(s, "both sides changed, merge with", r)
133 debug(s, "both sides changed, merge with", r)
134 wctx.sub(s).merge(r)
134 wctx.sub(s).merge(r)
135 sm[s] = l
135 sm[s] = l
136 elif ld == a: # remote removed, local unchanged
136 elif ld == a: # remote removed, local unchanged
137 debug(s, "remote removed, remove")
137 debug(s, "remote removed, remove")
138 wctx.sub(s).remove()
138 wctx.sub(s).remove()
139 else:
139 else:
140 if repo.ui.promptchoice(
140 if repo.ui.promptchoice(
141 _(' local changed subrepository %s which remote removed\n'
141 _(' local changed subrepository %s which remote removed\n'
142 'use (c)hanged version or (d)elete?') % s,
142 'use (c)hanged version or (d)elete?') % s,
143 (_('&Changed'), _('&Delete')), 0):
143 (_('&Changed'), _('&Delete')), 0):
144 debug(s, "prompt remove")
144 debug(s, "prompt remove")
145 wctx.sub(s).remove()
145 wctx.sub(s).remove()
146
146
147 for s, r in s2.items():
147 for s, r in s2.items():
148 if s in s1:
148 if s in s1:
149 continue
149 continue
150 elif s not in sa:
150 elif s not in sa:
151 debug(s, "remote added, get", r)
151 debug(s, "remote added, get", r)
152 mctx.sub(s).get(r)
152 mctx.sub(s).get(r)
153 sm[s] = r
153 sm[s] = r
154 elif r != sa[s]:
154 elif r != sa[s]:
155 if repo.ui.promptchoice(
155 if repo.ui.promptchoice(
156 _(' remote changed subrepository %s which local removed\n'
156 _(' remote changed subrepository %s which local removed\n'
157 'use (c)hanged version or (d)elete?') % s,
157 'use (c)hanged version or (d)elete?') % s,
158 (_('&Changed'), _('&Delete')), 0) == 0:
158 (_('&Changed'), _('&Delete')), 0) == 0:
159 debug(s, "prompt recreate", r)
159 debug(s, "prompt recreate", r)
160 wctx.sub(s).get(r)
160 wctx.sub(s).get(r)
161 sm[s] = r
161 sm[s] = r
162
162
163 # record merged .hgsubstate
163 # record merged .hgsubstate
164 writestate(repo, sm)
164 writestate(repo, sm)
165
165
166 def _updateprompt(ui, sub, dirty, local, remote):
166 def _updateprompt(ui, sub, dirty, local, remote):
167 if dirty:
167 if dirty:
168 msg = (_(' subrepository sources for %s differ\n'
168 msg = (_(' subrepository sources for %s differ\n'
169 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
169 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
170 % (subrelpath(sub), local, remote))
170 % (subrelpath(sub), local, remote))
171 else:
171 else:
172 msg = (_(' subrepository sources for %s differ (in checked out version)\n'
172 msg = (_(' subrepository sources for %s differ (in checked out version)\n'
173 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
173 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
174 % (subrelpath(sub), local, remote))
174 % (subrelpath(sub), local, remote))
175 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
175 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
176
176
177 def reporelpath(repo):
177 def reporelpath(repo):
178 """return path to this (sub)repo as seen from outermost repo"""
178 """return path to this (sub)repo as seen from outermost repo"""
179 parent = repo
179 parent = repo
180 while hasattr(parent, '_subparent'):
180 while hasattr(parent, '_subparent'):
181 parent = parent._subparent
181 parent = parent._subparent
182 return repo.root[len(parent.root)+1:]
182 return repo.root[len(parent.root)+1:]
183
183
184 def subrelpath(sub):
184 def subrelpath(sub):
185 """return path to this subrepo as seen from outermost repo"""
185 """return path to this subrepo as seen from outermost repo"""
186 if hasattr(sub, '_relpath'):
186 if hasattr(sub, '_relpath'):
187 return sub._relpath
187 return sub._relpath
188 if not hasattr(sub, '_repo'):
188 if not hasattr(sub, '_repo'):
189 return sub._path
189 return sub._path
190 return reporelpath(sub._repo)
190 return reporelpath(sub._repo)
191
191
192 def _abssource(repo, push=False, abort=True):
192 def _abssource(repo, push=False, abort=True):
193 """return pull/push path of repo - either based on parent repo .hgsub info
193 """return pull/push path of repo - either based on parent repo .hgsub info
194 or on the top repo config. Abort or return None if no source found."""
194 or on the top repo config. Abort or return None if no source found."""
195 if hasattr(repo, '_subparent'):
195 if hasattr(repo, '_subparent'):
196 source = repo._subsource
196 source = repo._subsource
197 if source.startswith('/') or '://' in source:
197 if source.startswith('/') or '://' in source:
198 return source
198 return source
199 parent = _abssource(repo._subparent, push, abort=False)
199 parent = _abssource(repo._subparent, push, abort=False)
200 if parent:
200 if parent:
201 if '://' in parent:
201 if '://' in parent:
202 if parent[-1] == '/':
202 if parent[-1] == '/':
203 parent = parent[:-1]
203 parent = parent[:-1]
204 r = urlparse.urlparse(parent + '/' + source)
204 r = urlparse.urlparse(parent + '/' + source)
205 r = urlparse.urlunparse((r[0], r[1],
205 r = urlparse.urlunparse((r[0], r[1],
206 posixpath.normpath(r[2]),
206 posixpath.normpath(r[2]),
207 r[3], r[4], r[5]))
207 r[3], r[4], r[5]))
208 return r
208 return r
209 else: # plain file system path
209 else: # plain file system path
210 return posixpath.normpath(os.path.join(parent, repo._subsource))
210 return posixpath.normpath(os.path.join(parent, repo._subsource))
211 else: # recursion reached top repo
211 else: # recursion reached top repo
212 if hasattr(repo, '_subtoppath'):
212 if hasattr(repo, '_subtoppath'):
213 return repo._subtoppath
213 return repo._subtoppath
214 if push and repo.ui.config('paths', 'default-push'):
214 if push and repo.ui.config('paths', 'default-push'):
215 return repo.ui.config('paths', 'default-push')
215 return repo.ui.config('paths', 'default-push')
216 if repo.ui.config('paths', 'default'):
216 if repo.ui.config('paths', 'default'):
217 return repo.ui.config('paths', 'default')
217 return repo.ui.config('paths', 'default')
218 if abort:
218 if abort:
219 raise util.Abort(_("default path for subrepository %s not found") %
219 raise util.Abort(_("default path for subrepository %s not found") %
220 reporelpath(repo))
220 reporelpath(repo))
221
221
222 def itersubrepos(ctx1, ctx2):
222 def itersubrepos(ctx1, ctx2):
223 """find subrepos in ctx1 or ctx2"""
223 """find subrepos in ctx1 or ctx2"""
224 # Create a (subpath, ctx) mapping where we prefer subpaths from
224 # Create a (subpath, ctx) mapping where we prefer subpaths from
225 # ctx1. The subpaths from ctx2 are important when the .hgsub file
225 # ctx1. The subpaths from ctx2 are important when the .hgsub file
226 # has been modified (in ctx2) but not yet committed (in ctx1).
226 # has been modified (in ctx2) but not yet committed (in ctx1).
227 subpaths = dict.fromkeys(ctx2.substate, ctx2)
227 subpaths = dict.fromkeys(ctx2.substate, ctx2)
228 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
228 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
229 for subpath, ctx in sorted(subpaths.iteritems()):
229 for subpath, ctx in sorted(subpaths.iteritems()):
230 yield subpath, ctx.sub(subpath)
230 yield subpath, ctx.sub(subpath)
231
231
232 def subrepo(ctx, path):
232 def subrepo(ctx, path):
233 """return instance of the right subrepo class for subrepo in path"""
233 """return instance of the right subrepo class for subrepo in path"""
234 # subrepo inherently violates our import layering rules
234 # subrepo inherently violates our import layering rules
235 # because it wants to make repo objects from deep inside the stack
235 # because it wants to make repo objects from deep inside the stack
236 # so we manually delay the circular imports to not break
236 # so we manually delay the circular imports to not break
237 # scripts that don't use our demand-loading
237 # scripts that don't use our demand-loading
238 global hg
238 global hg
239 import hg as h
239 import hg as h
240 hg = h
240 hg = h
241
241
242 util.path_auditor(ctx._repo.root)(path)
242 util.path_auditor(ctx._repo.root)(path)
243 state = ctx.substate.get(path, nullstate)
243 state = ctx.substate.get(path, nullstate)
244 if state[2] not in types:
244 if state[2] not in types:
245 raise util.Abort(_('unknown subrepo type %s') % state[2])
245 raise util.Abort(_('unknown subrepo type %s') % state[2])
246 return types[state[2]](ctx, path, state[:2])
246 return types[state[2]](ctx, path, state[:2])
247
247
248 # subrepo classes need to implement the following abstract class:
248 # subrepo classes need to implement the following abstract class:
249
249
250 class abstractsubrepo(object):
250 class abstractsubrepo(object):
251
251
252 def dirty(self, ignoreupdate=False):
252 def dirty(self, ignoreupdate=False):
253 """returns true if the dirstate of the subrepo is dirty or does not
253 """returns true if the dirstate of the subrepo is dirty or does not
254 match current stored state. If ignoreupdate is true, only check
254 match current stored state. If ignoreupdate is true, only check
255 whether the subrepo has uncommitted changes in its dirstate.
255 whether the subrepo has uncommitted changes in its dirstate.
256 """
256 """
257 raise NotImplementedError
257 raise NotImplementedError
258
258
259 def checknested(self, path):
259 def checknested(self, path):
260 """check if path is a subrepository within this repository"""
260 """check if path is a subrepository within this repository"""
261 return False
261 return False
262
262
263 def commit(self, text, user, date):
263 def commit(self, text, user, date):
264 """commit the current changes to the subrepo with the given
264 """commit the current changes to the subrepo with the given
265 log message. Use given user and date if possible. Return the
265 log message. Use given user and date if possible. Return the
266 new state of the subrepo.
266 new state of the subrepo.
267 """
267 """
268 raise NotImplementedError
268 raise NotImplementedError
269
269
270 def remove(self):
270 def remove(self):
271 """remove the subrepo
271 """remove the subrepo
272
272
273 (should verify the dirstate is not dirty first)
273 (should verify the dirstate is not dirty first)
274 """
274 """
275 raise NotImplementedError
275 raise NotImplementedError
276
276
277 def get(self, state, overwrite=False):
277 def get(self, state, overwrite=False):
278 """run whatever commands are needed to put the subrepo into
278 """run whatever commands are needed to put the subrepo into
279 this state
279 this state
280 """
280 """
281 raise NotImplementedError
281 raise NotImplementedError
282
282
283 def merge(self, state):
283 def merge(self, state):
284 """merge currently-saved state with the new state."""
284 """merge currently-saved state with the new state."""
285 raise NotImplementedError
285 raise NotImplementedError
286
286
287 def push(self, force):
287 def push(self, force):
288 """perform whatever action is analogous to 'hg push'
288 """perform whatever action is analogous to 'hg push'
289
289
290 This may be a no-op on some systems.
290 This may be a no-op on some systems.
291 """
291 """
292 raise NotImplementedError
292 raise NotImplementedError
293
293
294 def add(self, ui, match, dryrun, prefix):
294 def add(self, ui, match, dryrun, prefix):
295 return []
295 return []
296
296
297 def status(self, rev2, **opts):
297 def status(self, rev2, **opts):
298 return [], [], [], [], [], [], []
298 return [], [], [], [], [], [], []
299
299
300 def diff(self, diffopts, node2, match, prefix, **opts):
300 def diff(self, diffopts, node2, match, prefix, **opts):
301 pass
301 pass
302
302
303 def outgoing(self, ui, dest, opts):
303 def outgoing(self, ui, dest, opts):
304 return 1
304 return 1
305
305
306 def incoming(self, ui, source, opts):
306 def incoming(self, ui, source, opts):
307 return 1
307 return 1
308
308
309 def files(self):
309 def files(self):
310 """return filename iterator"""
310 """return filename iterator"""
311 raise NotImplementedError
311 raise NotImplementedError
312
312
313 def filedata(self, name):
313 def filedata(self, name):
314 """return file data"""
314 """return file data"""
315 raise NotImplementedError
315 raise NotImplementedError
316
316
317 def fileflags(self, name):
317 def fileflags(self, name):
318 """return file flags"""
318 """return file flags"""
319 return ''
319 return ''
320
320
321 def archive(self, ui, archiver, prefix):
321 def archive(self, ui, archiver, prefix):
322 files = self.files()
322 files = self.files()
323 total = len(files)
323 total = len(files)
324 relpath = subrelpath(self)
324 relpath = subrelpath(self)
325 ui.progress(_('archiving (%s)') % relpath, 0,
325 ui.progress(_('archiving (%s)') % relpath, 0,
326 unit=_('files'), total=total)
326 unit=_('files'), total=total)
327 for i, name in enumerate(files):
327 for i, name in enumerate(files):
328 flags = self.fileflags(name)
328 flags = self.fileflags(name)
329 mode = 'x' in flags and 0755 or 0644
329 mode = 'x' in flags and 0755 or 0644
330 symlink = 'l' in flags
330 symlink = 'l' in flags
331 archiver.addfile(os.path.join(prefix, self._path, name),
331 archiver.addfile(os.path.join(prefix, self._path, name),
332 mode, symlink, self.filedata(name))
332 mode, symlink, self.filedata(name))
333 ui.progress(_('archiving (%s)') % relpath, i + 1,
333 ui.progress(_('archiving (%s)') % relpath, i + 1,
334 unit=_('files'), total=total)
334 unit=_('files'), total=total)
335 ui.progress(_('archiving (%s)') % relpath, None)
335 ui.progress(_('archiving (%s)') % relpath, None)
336
336
337
337
338 class hgsubrepo(abstractsubrepo):
338 class hgsubrepo(abstractsubrepo):
339 def __init__(self, ctx, path, state):
339 def __init__(self, ctx, path, state):
340 self._path = path
340 self._path = path
341 self._state = state
341 self._state = state
342 r = ctx._repo
342 r = ctx._repo
343 root = r.wjoin(path)
343 root = r.wjoin(path)
344 create = False
344 create = False
345 if not os.path.exists(os.path.join(root, '.hg')):
345 if not os.path.exists(os.path.join(root, '.hg')):
346 create = True
346 create = True
347 util.makedirs(root)
347 util.makedirs(root)
348 self._repo = hg.repository(r.ui, root, create=create)
348 self._repo = hg.repository(r.ui, root, create=create)
349 self._repo._subparent = r
349 self._repo._subparent = r
350 self._repo._subsource = state[0]
350 self._repo._subsource = state[0]
351
351
352 if create:
352 if create:
353 fp = self._repo.opener("hgrc", "w", text=True)
353 fp = self._repo.opener("hgrc", "w", text=True)
354 fp.write('[paths]\n')
354 fp.write('[paths]\n')
355
355
356 def addpathconfig(key, value):
356 def addpathconfig(key, value):
357 if value:
357 if value:
358 fp.write('%s = %s\n' % (key, value))
358 fp.write('%s = %s\n' % (key, value))
359 self._repo.ui.setconfig('paths', key, value)
359 self._repo.ui.setconfig('paths', key, value)
360
360
361 defpath = _abssource(self._repo, abort=False)
361 defpath = _abssource(self._repo, abort=False)
362 defpushpath = _abssource(self._repo, True, abort=False)
362 defpushpath = _abssource(self._repo, True, abort=False)
363 addpathconfig('default', defpath)
363 addpathconfig('default', defpath)
364 if defpath != defpushpath:
364 if defpath != defpushpath:
365 addpathconfig('default-push', defpushpath)
365 addpathconfig('default-push', defpushpath)
366 fp.close()
366 fp.close()
367
367
368 def add(self, ui, match, dryrun, prefix):
368 def add(self, ui, match, dryrun, prefix):
369 return cmdutil.add(ui, self._repo, match, dryrun, True,
369 return cmdutil.add(ui, self._repo, match, dryrun, True,
370 os.path.join(prefix, self._path))
370 os.path.join(prefix, self._path))
371
371
372 def status(self, rev2, **opts):
372 def status(self, rev2, **opts):
373 try:
373 try:
374 rev1 = self._state[1]
374 rev1 = self._state[1]
375 ctx1 = self._repo[rev1]
375 ctx1 = self._repo[rev1]
376 ctx2 = self._repo[rev2]
376 ctx2 = self._repo[rev2]
377 return self._repo.status(ctx1, ctx2, **opts)
377 return self._repo.status(ctx1, ctx2, **opts)
378 except error.RepoLookupError, inst:
378 except error.RepoLookupError, inst:
379 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
379 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
380 % (inst, subrelpath(self)))
380 % (inst, subrelpath(self)))
381 return [], [], [], [], [], [], []
381 return [], [], [], [], [], [], []
382
382
383 def diff(self, diffopts, node2, match, prefix, **opts):
383 def diff(self, diffopts, node2, match, prefix, **opts):
384 try:
384 try:
385 node1 = node.bin(self._state[1])
385 node1 = node.bin(self._state[1])
386 # We currently expect node2 to come from substate and be
386 # We currently expect node2 to come from substate and be
387 # in hex format
387 # in hex format
388 if node2 is not None:
388 if node2 is not None:
389 node2 = node.bin(node2)
389 node2 = node.bin(node2)
390 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
390 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
391 node1, node2, match,
391 node1, node2, match,
392 prefix=os.path.join(prefix, self._path),
392 prefix=os.path.join(prefix, self._path),
393 listsubrepos=True, **opts)
393 listsubrepos=True, **opts)
394 except error.RepoLookupError, inst:
394 except error.RepoLookupError, inst:
395 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
395 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
396 % (inst, subrelpath(self)))
396 % (inst, subrelpath(self)))
397
397
398 def archive(self, ui, archiver, prefix):
398 def archive(self, ui, archiver, prefix):
399 abstractsubrepo.archive(self, ui, archiver, prefix)
399 abstractsubrepo.archive(self, ui, archiver, prefix)
400
400
401 rev = self._state[1]
401 rev = self._state[1]
402 ctx = self._repo[rev]
402 ctx = self._repo[rev]
403 for subpath in ctx.substate:
403 for subpath in ctx.substate:
404 s = subrepo(ctx, subpath)
404 s = subrepo(ctx, subpath)
405 s.archive(ui, archiver, os.path.join(prefix, self._path))
405 s.archive(ui, archiver, os.path.join(prefix, self._path))
406
406
407 def dirty(self, ignoreupdate=False):
407 def dirty(self, ignoreupdate=False):
408 r = self._state[1]
408 r = self._state[1]
409 if r == '' and not ignoreupdate: # no state recorded
409 if r == '' and not ignoreupdate: # no state recorded
410 return True
410 return True
411 w = self._repo[None]
411 w = self._repo[None]
412 if w.p1() != self._repo[r] and not ignoreupdate:
412 if w.p1() != self._repo[r] and not ignoreupdate:
413 # different version checked out
413 # different version checked out
414 return True
414 return True
415 return w.dirty() # working directory changed
415 return w.dirty() # working directory changed
416
416
417 def checknested(self, path):
417 def checknested(self, path):
418 return self._repo._checknested(self._repo.wjoin(path))
418 return self._repo._checknested(self._repo.wjoin(path))
419
419
420 def commit(self, text, user, date):
420 def commit(self, text, user, date):
421 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
421 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
422 n = self._repo.commit(text, user, date)
422 n = self._repo.commit(text, user, date)
423 if not n:
423 if not n:
424 return self._repo['.'].hex() # different version checked out
424 return self._repo['.'].hex() # different version checked out
425 return node.hex(n)
425 return node.hex(n)
426
426
427 def remove(self):
427 def remove(self):
428 # we can't fully delete the repository as it may contain
428 # we can't fully delete the repository as it may contain
429 # local-only history
429 # local-only history
430 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
430 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
431 hg.clean(self._repo, node.nullid, False)
431 hg.clean(self._repo, node.nullid, False)
432
432
433 def _get(self, state):
433 def _get(self, state):
434 source, revision, kind = state
434 source, revision, kind = state
435 try:
435 try:
436 self._repo.lookup(revision)
436 self._repo.lookup(revision)
437 except error.RepoError:
437 except error.RepoError:
438 self._repo._subsource = source
438 self._repo._subsource = source
439 srcurl = _abssource(self._repo)
439 srcurl = _abssource(self._repo)
440 self._repo.ui.status(_('pulling subrepo %s from %s\n')
440 self._repo.ui.status(_('pulling subrepo %s from %s\n')
441 % (subrelpath(self), srcurl))
441 % (subrelpath(self), srcurl))
442 other = hg.repository(self._repo.ui, srcurl)
442 other = hg.repository(self._repo.ui, srcurl)
443 self._repo.pull(other)
443 self._repo.pull(other)
444
444
445 def get(self, state, overwrite=False):
445 def get(self, state, overwrite=False):
446 self._get(state)
446 self._get(state)
447 source, revision, kind = state
447 source, revision, kind = state
448 self._repo.ui.debug("getting subrepo %s\n" % self._path)
448 self._repo.ui.debug("getting subrepo %s\n" % self._path)
449 hg.clean(self._repo, revision, False)
449 hg.clean(self._repo, revision, False)
450
450
451 def merge(self, state):
451 def merge(self, state):
452 self._get(state)
452 self._get(state)
453 cur = self._repo['.']
453 cur = self._repo['.']
454 dst = self._repo[state[1]]
454 dst = self._repo[state[1]]
455 anc = dst.ancestor(cur)
455 anc = dst.ancestor(cur)
456
456
457 def mergefunc():
457 def mergefunc():
458 if anc == cur:
458 if anc == cur:
459 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
459 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
460 hg.update(self._repo, state[1])
460 hg.update(self._repo, state[1])
461 elif anc == dst:
461 elif anc == dst:
462 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
462 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
463 else:
463 else:
464 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
464 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
465 hg.merge(self._repo, state[1], remind=False)
465 hg.merge(self._repo, state[1], remind=False)
466
466
467 wctx = self._repo[None]
467 wctx = self._repo[None]
468 if self.dirty():
468 if self.dirty():
469 if anc != dst:
469 if anc != dst:
470 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
470 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
471 mergefunc()
471 mergefunc()
472 else:
472 else:
473 mergefunc()
473 mergefunc()
474 else:
474 else:
475 mergefunc()
475 mergefunc()
476
476
477 def push(self, force):
477 def push(self, force):
478 # push subrepos depth-first for coherent ordering
478 # push subrepos depth-first for coherent ordering
479 c = self._repo['']
479 c = self._repo['']
480 subs = c.substate # only repos that are committed
480 subs = c.substate # only repos that are committed
481 for s in sorted(subs):
481 for s in sorted(subs):
482 if not c.sub(s).push(force):
482 if not c.sub(s).push(force):
483 return False
483 return False
484
484
485 dsturl = _abssource(self._repo, True)
485 dsturl = _abssource(self._repo, True)
486 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
486 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
487 (subrelpath(self), dsturl))
487 (subrelpath(self), dsturl))
488 other = hg.repository(self._repo.ui, dsturl)
488 other = hg.repository(self._repo.ui, dsturl)
489 return self._repo.push(other, force)
489 return self._repo.push(other, force)
490
490
491 def outgoing(self, ui, dest, opts):
491 def outgoing(self, ui, dest, opts):
492 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
492 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
493
493
494 def incoming(self, ui, source, opts):
494 def incoming(self, ui, source, opts):
495 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
495 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
496
496
497 def files(self):
497 def files(self):
498 rev = self._state[1]
498 rev = self._state[1]
499 ctx = self._repo[rev]
499 ctx = self._repo[rev]
500 return ctx.manifest()
500 return ctx.manifest()
501
501
502 def filedata(self, name):
502 def filedata(self, name):
503 rev = self._state[1]
503 rev = self._state[1]
504 return self._repo[rev][name].data()
504 return self._repo[rev][name].data()
505
505
506 def fileflags(self, name):
506 def fileflags(self, name):
507 rev = self._state[1]
507 rev = self._state[1]
508 ctx = self._repo[rev]
508 ctx = self._repo[rev]
509 return ctx.flags(name)
509 return ctx.flags(name)
510
510
511
511
512 class svnsubrepo(abstractsubrepo):
512 class svnsubrepo(abstractsubrepo):
513 def __init__(self, ctx, path, state):
513 def __init__(self, ctx, path, state):
514 self._path = path
514 self._path = path
515 self._state = state
515 self._state = state
516 self._ctx = ctx
516 self._ctx = ctx
517 self._ui = ctx._repo.ui
517 self._ui = ctx._repo.ui
518
518
519 def _svncommand(self, commands, filename=''):
519 def _svncommand(self, commands, filename=''):
520 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
520 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
521 cmd = ['svn'] + commands + [path]
521 cmd = ['svn'] + commands + [path]
522 env = dict(os.environ)
522 env = dict(os.environ)
523 # Avoid localized output, preserve current locale for everything else.
523 # Avoid localized output, preserve current locale for everything else.
524 env['LC_MESSAGES'] = 'C'
524 env['LC_MESSAGES'] = 'C'
525 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
525 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
526 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
526 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
527 universal_newlines=True, env=env)
527 universal_newlines=True, env=env)
528 stdout, stderr = p.communicate()
528 stdout, stderr = p.communicate()
529 stderr = stderr.strip()
529 stderr = stderr.strip()
530 if stderr:
530 if stderr:
531 raise util.Abort(stderr)
531 raise util.Abort(stderr)
532 return stdout
532 return stdout
533
533
534 def _wcrevs(self):
534 def _wcrevs(self):
535 # Get the working directory revision as well as the last
535 # Get the working directory revision as well as the last
536 # commit revision so we can compare the subrepo state with
536 # commit revision so we can compare the subrepo state with
537 # both. We used to store the working directory one.
537 # both. We used to store the working directory one.
538 output = self._svncommand(['info', '--xml'])
538 output = self._svncommand(['info', '--xml'])
539 doc = xml.dom.minidom.parseString(output)
539 doc = xml.dom.minidom.parseString(output)
540 entries = doc.getElementsByTagName('entry')
540 entries = doc.getElementsByTagName('entry')
541 lastrev, rev = '0', '0'
541 lastrev, rev = '0', '0'
542 if entries:
542 if entries:
543 rev = str(entries[0].getAttribute('revision')) or '0'
543 rev = str(entries[0].getAttribute('revision')) or '0'
544 commits = entries[0].getElementsByTagName('commit')
544 commits = entries[0].getElementsByTagName('commit')
545 if commits:
545 if commits:
546 lastrev = str(commits[0].getAttribute('revision')) or '0'
546 lastrev = str(commits[0].getAttribute('revision')) or '0'
547 return (lastrev, rev)
547 return (lastrev, rev)
548
548
549 def _wcrev(self):
549 def _wcrev(self):
550 return self._wcrevs()[0]
550 return self._wcrevs()[0]
551
551
552 def _wcchanged(self):
552 def _wcchanged(self):
553 """Return (changes, extchanges) where changes is True
553 """Return (changes, extchanges) where changes is True
554 if the working directory was changed, and extchanges is
554 if the working directory was changed, and extchanges is
555 True if any of these changes concern an external entry.
555 True if any of these changes concern an external entry.
556 """
556 """
557 output = self._svncommand(['status', '--xml'])
557 output = self._svncommand(['status', '--xml'])
558 externals, changes = [], []
558 externals, changes = [], []
559 doc = xml.dom.minidom.parseString(output)
559 doc = xml.dom.minidom.parseString(output)
560 for e in doc.getElementsByTagName('entry'):
560 for e in doc.getElementsByTagName('entry'):
561 s = e.getElementsByTagName('wc-status')
561 s = e.getElementsByTagName('wc-status')
562 if not s:
562 if not s:
563 continue
563 continue
564 item = s[0].getAttribute('item')
564 item = s[0].getAttribute('item')
565 props = s[0].getAttribute('props')
565 props = s[0].getAttribute('props')
566 path = e.getAttribute('path')
566 path = e.getAttribute('path')
567 if item == 'external':
567 if item == 'external':
568 externals.append(path)
568 externals.append(path)
569 if (item not in ('', 'normal', 'unversioned', 'external')
569 if (item not in ('', 'normal', 'unversioned', 'external')
570 or props not in ('', 'none')):
570 or props not in ('', 'none')):
571 changes.append(path)
571 changes.append(path)
572 for path in changes:
572 for path in changes:
573 for ext in externals:
573 for ext in externals:
574 if path == ext or path.startswith(ext + os.sep):
574 if path == ext or path.startswith(ext + os.sep):
575 return True, True
575 return True, True
576 return bool(changes), False
576 return bool(changes), False
577
577
578 def dirty(self, ignoreupdate=False):
578 def dirty(self, ignoreupdate=False):
579 if not self._wcchanged()[0]:
579 if not self._wcchanged()[0]:
580 if self._state[1] in self._wcrevs() or ignoreupdate:
580 if self._state[1] in self._wcrevs() or ignoreupdate:
581 return False
581 return False
582 return True
582 return True
583
583
584 def commit(self, text, user, date):
584 def commit(self, text, user, date):
585 # user and date are out of our hands since svn is centralized
585 # user and date are out of our hands since svn is centralized
586 changed, extchanged = self._wcchanged()
586 changed, extchanged = self._wcchanged()
587 if not changed:
587 if not changed:
588 return self._wcrev()
588 return self._wcrev()
589 if extchanged:
589 if extchanged:
590 # Do not try to commit externals
590 # Do not try to commit externals
591 raise util.Abort(_('cannot commit svn externals'))
591 raise util.Abort(_('cannot commit svn externals'))
592 commitinfo = self._svncommand(['commit', '-m', text])
592 commitinfo = self._svncommand(['commit', '-m', text])
593 self._ui.status(commitinfo)
593 self._ui.status(commitinfo)
594 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
594 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
595 if not newrev:
595 if not newrev:
596 raise util.Abort(commitinfo.splitlines()[-1])
596 raise util.Abort(commitinfo.splitlines()[-1])
597 newrev = newrev.groups()[0]
597 newrev = newrev.groups()[0]
598 self._ui.status(self._svncommand(['update', '-r', newrev]))
598 self._ui.status(self._svncommand(['update', '-r', newrev]))
599 return newrev
599 return newrev
600
600
601 def remove(self):
601 def remove(self):
602 if self.dirty():
602 if self.dirty():
603 self._ui.warn(_('not removing repo %s because '
603 self._ui.warn(_('not removing repo %s because '
604 'it has changes.\n' % self._path))
604 'it has changes.\n' % self._path))
605 return
605 return
606 self._ui.note(_('removing subrepo %s\n') % self._path)
606 self._ui.note(_('removing subrepo %s\n') % self._path)
607
607
608 def onerror(function, path, excinfo):
608 def onerror(function, path, excinfo):
609 if function is not os.remove:
609 if function is not os.remove:
610 raise
610 raise
611 # read-only files cannot be unlinked under Windows
611 # read-only files cannot be unlinked under Windows
612 s = os.stat(path)
612 s = os.stat(path)
613 if (s.st_mode & stat.S_IWRITE) != 0:
613 if (s.st_mode & stat.S_IWRITE) != 0:
614 raise
614 raise
615 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
615 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
616 os.remove(path)
616 os.remove(path)
617
617
618 path = self._ctx._repo.wjoin(self._path)
618 path = self._ctx._repo.wjoin(self._path)
619 shutil.rmtree(path, onerror=onerror)
619 shutil.rmtree(path, onerror=onerror)
620 try:
620 try:
621 os.removedirs(os.path.dirname(path))
621 os.removedirs(os.path.dirname(path))
622 except OSError:
622 except OSError:
623 pass
623 pass
624
624
625 def get(self, state, overwrite=False):
625 def get(self, state, overwrite=False):
626 if overwrite:
626 if overwrite:
627 self._svncommand(['revert', '--recursive'])
627 self._svncommand(['revert', '--recursive'])
628 status = self._svncommand(['checkout', '--force', state[0],
628 status = self._svncommand(['checkout', '--force', state[0],
629 '--revision', state[1]])
629 '--revision', state[1]])
630 if not re.search('Checked out revision [0-9]+.', status):
630 if not re.search('Checked out revision [0-9]+.', status):
631 raise util.Abort(status.splitlines()[-1])
631 raise util.Abort(status.splitlines()[-1])
632 self._ui.status(status)
632 self._ui.status(status)
633
633
634 def merge(self, state):
634 def merge(self, state):
635 old = self._state[1]
635 old = self._state[1]
636 new = state[1]
636 new = state[1]
637 if new != self._wcrev():
637 if new != self._wcrev():
638 dirty = old == self._wcrev() or self._wcchanged()[0]
638 dirty = old == self._wcrev() or self._wcchanged()[0]
639 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
639 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
640 self.get(state, False)
640 self.get(state, False)
641
641
642 def push(self, force):
642 def push(self, force):
643 # push is a no-op for SVN
643 # push is a no-op for SVN
644 return True
644 return True
645
645
646 def files(self):
646 def files(self):
647 output = self._svncommand(['list'])
647 output = self._svncommand(['list'])
648 # This works because svn forbids \n in filenames.
648 # This works because svn forbids \n in filenames.
649 return output.splitlines()
649 return output.splitlines()
650
650
651 def filedata(self, name):
651 def filedata(self, name):
652 return self._svncommand(['cat'], name)
652 return self._svncommand(['cat'], name)
653
653
654
654
655 class gitsubrepo(abstractsubrepo):
655 class gitsubrepo(abstractsubrepo):
656 def __init__(self, ctx, path, state):
656 def __init__(self, ctx, path, state):
657 # TODO add git version check.
657 # TODO add git version check.
658 self._state = state
658 self._state = state
659 self._ctx = ctx
659 self._ctx = ctx
660 self._path = path
660 self._path = path
661 self._relpath = os.path.join(reporelpath(ctx._repo), path)
661 self._relpath = os.path.join(reporelpath(ctx._repo), path)
662 self._abspath = ctx._repo.wjoin(path)
662 self._abspath = ctx._repo.wjoin(path)
663 self._subparent = ctx._repo
663 self._subparent = ctx._repo
664 self._ui = ctx._repo.ui
664 self._ui = ctx._repo.ui
665
665
666 def _gitcommand(self, commands, env=None, stream=False):
666 def _gitcommand(self, commands, env=None, stream=False):
667 return self._gitdir(commands, env=env, stream=stream)[0]
667 return self._gitdir(commands, env=env, stream=stream)[0]
668
668
669 def _gitdir(self, commands, env=None, stream=False):
669 def _gitdir(self, commands, env=None, stream=False):
670 return self._gitnodir(commands, env=env, stream=stream,
670 return self._gitnodir(commands, env=env, stream=stream,
671 cwd=self._abspath)
671 cwd=self._abspath)
672
672
673 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
673 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
674 """Calls the git command
674 """Calls the git command
675
675
676 The methods tries to call the git command. versions previor to 1.6.0
676 The methods tries to call the git command. versions previor to 1.6.0
677 are not supported and very probably fail.
677 are not supported and very probably fail.
678 """
678 """
679 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
679 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
680 # unless ui.quiet is set, print git's stderr,
680 # unless ui.quiet is set, print git's stderr,
681 # which is mostly progress and useful info
681 # which is mostly progress and useful info
682 errpipe = None
682 errpipe = None
683 if self._ui.quiet:
683 if self._ui.quiet:
684 errpipe = open(os.devnull, 'w')
684 errpipe = open(os.devnull, 'w')
685 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
685 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
686 close_fds=util.closefds,
686 close_fds=util.closefds,
687 stdout=subprocess.PIPE, stderr=errpipe)
687 stdout=subprocess.PIPE, stderr=errpipe)
688 if stream:
688 if stream:
689 return p.stdout, None
689 return p.stdout, None
690
690
691 retdata = p.stdout.read().strip()
691 retdata = p.stdout.read().strip()
692 # wait for the child to exit to avoid race condition.
692 # wait for the child to exit to avoid race condition.
693 p.wait()
693 p.wait()
694
694
695 if p.returncode != 0 and p.returncode != 1:
695 if p.returncode != 0 and p.returncode != 1:
696 # there are certain error codes that are ok
696 # there are certain error codes that are ok
697 command = commands[0]
697 command = commands[0]
698 if command in ('cat-file', 'symbolic-ref'):
698 if command in ('cat-file', 'symbolic-ref'):
699 return retdata, p.returncode
699 return retdata, p.returncode
700 # for all others, abort
700 # for all others, abort
701 raise util.Abort('git %s error %d in %s' %
701 raise util.Abort('git %s error %d in %s' %
702 (command, p.returncode, self._relpath))
702 (command, p.returncode, self._relpath))
703
703
704 return retdata, p.returncode
704 return retdata, p.returncode
705
705
706 def _gitmissing(self):
707 return not os.path.exists(os.path.join(self._abspath, '.git'))
708
706 def _gitstate(self):
709 def _gitstate(self):
707 return self._gitcommand(['rev-parse', 'HEAD'])
710 return self._gitcommand(['rev-parse', 'HEAD'])
708
711
709 def _gitcurrentbranch(self):
712 def _gitcurrentbranch(self):
710 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
713 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
711 if err:
714 if err:
712 current = None
715 current = None
713 return current
716 return current
714
717
715 def _githavelocally(self, revision):
718 def _githavelocally(self, revision):
716 out, code = self._gitdir(['cat-file', '-e', revision])
719 out, code = self._gitdir(['cat-file', '-e', revision])
717 return code == 0
720 return code == 0
718
721
719 def _gitisancestor(self, r1, r2):
722 def _gitisancestor(self, r1, r2):
720 base = self._gitcommand(['merge-base', r1, r2])
723 base = self._gitcommand(['merge-base', r1, r2])
721 return base == r1
724 return base == r1
722
725
723 def _gitbranchmap(self):
726 def _gitbranchmap(self):
724 '''returns 2 things:
727 '''returns 2 things:
725 a map from git branch to revision
728 a map from git branch to revision
726 a map from revision to branches'''
729 a map from revision to branches'''
727 branch2rev = {}
730 branch2rev = {}
728 rev2branch = {}
731 rev2branch = {}
729
732
730 out = self._gitcommand(['for-each-ref', '--format',
733 out = self._gitcommand(['for-each-ref', '--format',
731 '%(objectname) %(refname)'])
734 '%(objectname) %(refname)'])
732 for line in out.split('\n'):
735 for line in out.split('\n'):
733 revision, ref = line.split(' ')
736 revision, ref = line.split(' ')
734 if (not ref.startswith('refs/heads/') and
737 if (not ref.startswith('refs/heads/') and
735 not ref.startswith('refs/remotes/')):
738 not ref.startswith('refs/remotes/')):
736 continue
739 continue
737 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
740 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
738 continue # ignore remote/HEAD redirects
741 continue # ignore remote/HEAD redirects
739 branch2rev[ref] = revision
742 branch2rev[ref] = revision
740 rev2branch.setdefault(revision, []).append(ref)
743 rev2branch.setdefault(revision, []).append(ref)
741 return branch2rev, rev2branch
744 return branch2rev, rev2branch
742
745
743 def _gittracking(self, branches):
746 def _gittracking(self, branches):
744 'return map of remote branch to local tracking branch'
747 'return map of remote branch to local tracking branch'
745 # assumes no more than one local tracking branch for each remote
748 # assumes no more than one local tracking branch for each remote
746 tracking = {}
749 tracking = {}
747 for b in branches:
750 for b in branches:
748 if b.startswith('refs/remotes/'):
751 if b.startswith('refs/remotes/'):
749 continue
752 continue
750 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
753 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
751 if remote:
754 if remote:
752 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
755 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
753 tracking['refs/remotes/%s/%s' %
756 tracking['refs/remotes/%s/%s' %
754 (remote, ref.split('/', 2)[2])] = b
757 (remote, ref.split('/', 2)[2])] = b
755 return tracking
758 return tracking
756
759
757 def _abssource(self, source):
760 def _abssource(self, source):
758 self._subsource = source
761 self._subsource = source
759 return _abssource(self)
762 return _abssource(self)
760
763
761 def _fetch(self, source, revision):
764 def _fetch(self, source, revision):
762 if not os.path.exists(os.path.join(self._abspath, '.git')):
765 if self._gitmissing():
763 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
766 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
764 self._gitnodir(['clone', self._abssource(source), self._abspath])
767 self._gitnodir(['clone', self._abssource(source), self._abspath])
765 if self._githavelocally(revision):
768 if self._githavelocally(revision):
766 return
769 return
767 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
770 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
768 # try only origin: the originally cloned repo
771 # try only origin: the originally cloned repo
769 self._gitcommand(['fetch'])
772 self._gitcommand(['fetch'])
770 if not self._githavelocally(revision):
773 if not self._githavelocally(revision):
771 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
774 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
772 (revision, self._relpath))
775 (revision, self._relpath))
773
776
774 def dirty(self, ignoreupdate=False):
777 def dirty(self, ignoreupdate=False):
778 if self._gitmissing():
779 return True
775 if not ignoreupdate and self._state[1] != self._gitstate():
780 if not ignoreupdate and self._state[1] != self._gitstate():
776 # different version checked out
781 # different version checked out
777 return True
782 return True
778 # check for staged changes or modified files; ignore untracked files
783 # check for staged changes or modified files; ignore untracked files
779 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
784 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
780 return code == 1
785 return code == 1
781
786
782 def get(self, state, overwrite=False):
787 def get(self, state, overwrite=False):
783 source, revision, kind = state
788 source, revision, kind = state
784 self._fetch(source, revision)
789 self._fetch(source, revision)
785 # if the repo was set to be bare, unbare it
790 # if the repo was set to be bare, unbare it
786 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
791 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
787 self._gitcommand(['config', 'core.bare', 'false'])
792 self._gitcommand(['config', 'core.bare', 'false'])
788 if self._gitstate() == revision:
793 if self._gitstate() == revision:
789 self._gitcommand(['reset', '--hard', 'HEAD'])
794 self._gitcommand(['reset', '--hard', 'HEAD'])
790 return
795 return
791 elif self._gitstate() == revision:
796 elif self._gitstate() == revision:
792 if overwrite:
797 if overwrite:
793 # first reset the index to unmark new files for commit, because
798 # first reset the index to unmark new files for commit, because
794 # reset --hard will otherwise throw away files added for commit,
799 # reset --hard will otherwise throw away files added for commit,
795 # not just unmark them.
800 # not just unmark them.
796 self._gitcommand(['reset', 'HEAD'])
801 self._gitcommand(['reset', 'HEAD'])
797 self._gitcommand(['reset', '--hard', 'HEAD'])
802 self._gitcommand(['reset', '--hard', 'HEAD'])
798 return
803 return
799 branch2rev, rev2branch = self._gitbranchmap()
804 branch2rev, rev2branch = self._gitbranchmap()
800
805
801 def checkout(args):
806 def checkout(args):
802 cmd = ['checkout']
807 cmd = ['checkout']
803 if overwrite:
808 if overwrite:
804 # first reset the index to unmark new files for commit, because
809 # first reset the index to unmark new files for commit, because
805 # the -f option will otherwise throw away files added for
810 # the -f option will otherwise throw away files added for
806 # commit, not just unmark them.
811 # commit, not just unmark them.
807 self._gitcommand(['reset', 'HEAD'])
812 self._gitcommand(['reset', 'HEAD'])
808 cmd.append('-f')
813 cmd.append('-f')
809 self._gitcommand(cmd + args)
814 self._gitcommand(cmd + args)
810
815
811 def rawcheckout():
816 def rawcheckout():
812 # no branch to checkout, check it out with no branch
817 # no branch to checkout, check it out with no branch
813 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
818 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
814 self._relpath)
819 self._relpath)
815 self._ui.warn(_('check out a git branch if you intend '
820 self._ui.warn(_('check out a git branch if you intend '
816 'to make changes\n'))
821 'to make changes\n'))
817 checkout(['-q', revision])
822 checkout(['-q', revision])
818
823
819 if revision not in rev2branch:
824 if revision not in rev2branch:
820 rawcheckout()
825 rawcheckout()
821 return
826 return
822 branches = rev2branch[revision]
827 branches = rev2branch[revision]
823 firstlocalbranch = None
828 firstlocalbranch = None
824 for b in branches:
829 for b in branches:
825 if b == 'refs/heads/master':
830 if b == 'refs/heads/master':
826 # master trumps all other branches
831 # master trumps all other branches
827 checkout(['refs/heads/master'])
832 checkout(['refs/heads/master'])
828 return
833 return
829 if not firstlocalbranch and not b.startswith('refs/remotes/'):
834 if not firstlocalbranch and not b.startswith('refs/remotes/'):
830 firstlocalbranch = b
835 firstlocalbranch = b
831 if firstlocalbranch:
836 if firstlocalbranch:
832 checkout([firstlocalbranch])
837 checkout([firstlocalbranch])
833 return
838 return
834
839
835 tracking = self._gittracking(branch2rev.keys())
840 tracking = self._gittracking(branch2rev.keys())
836 # choose a remote branch already tracked if possible
841 # choose a remote branch already tracked if possible
837 remote = branches[0]
842 remote = branches[0]
838 if remote not in tracking:
843 if remote not in tracking:
839 for b in branches:
844 for b in branches:
840 if b in tracking:
845 if b in tracking:
841 remote = b
846 remote = b
842 break
847 break
843
848
844 if remote not in tracking:
849 if remote not in tracking:
845 # create a new local tracking branch
850 # create a new local tracking branch
846 local = remote.split('/', 2)[2]
851 local = remote.split('/', 2)[2]
847 checkout(['-b', local, remote])
852 checkout(['-b', local, remote])
848 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
853 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
849 # When updating to a tracked remote branch,
854 # When updating to a tracked remote branch,
850 # if the local tracking branch is downstream of it,
855 # if the local tracking branch is downstream of it,
851 # a normal `git pull` would have performed a "fast-forward merge"
856 # a normal `git pull` would have performed a "fast-forward merge"
852 # which is equivalent to updating the local branch to the remote.
857 # which is equivalent to updating the local branch to the remote.
853 # Since we are only looking at branching at update, we need to
858 # Since we are only looking at branching at update, we need to
854 # detect this situation and perform this action lazily.
859 # detect this situation and perform this action lazily.
855 if tracking[remote] != self._gitcurrentbranch():
860 if tracking[remote] != self._gitcurrentbranch():
856 checkout([tracking[remote]])
861 checkout([tracking[remote]])
857 self._gitcommand(['merge', '--ff', remote])
862 self._gitcommand(['merge', '--ff', remote])
858 else:
863 else:
859 # a real merge would be required, just checkout the revision
864 # a real merge would be required, just checkout the revision
860 rawcheckout()
865 rawcheckout()
861
866
862 def commit(self, text, user, date):
867 def commit(self, text, user, date):
868 if self._gitmissing():
869 raise util.Abort(_("subrepo %s is missing") % self._relpath)
863 cmd = ['commit', '-a', '-m', text]
870 cmd = ['commit', '-a', '-m', text]
864 env = os.environ.copy()
871 env = os.environ.copy()
865 if user:
872 if user:
866 cmd += ['--author', user]
873 cmd += ['--author', user]
867 if date:
874 if date:
868 # git's date parser silently ignores when seconds < 1e9
875 # git's date parser silently ignores when seconds < 1e9
869 # convert to ISO8601
876 # convert to ISO8601
870 env['GIT_AUTHOR_DATE'] = util.datestr(date,
877 env['GIT_AUTHOR_DATE'] = util.datestr(date,
871 '%Y-%m-%dT%H:%M:%S %1%2')
878 '%Y-%m-%dT%H:%M:%S %1%2')
872 self._gitcommand(cmd, env=env)
879 self._gitcommand(cmd, env=env)
873 # make sure commit works otherwise HEAD might not exist under certain
880 # make sure commit works otherwise HEAD might not exist under certain
874 # circumstances
881 # circumstances
875 return self._gitstate()
882 return self._gitstate()
876
883
877 def merge(self, state):
884 def merge(self, state):
878 source, revision, kind = state
885 source, revision, kind = state
879 self._fetch(source, revision)
886 self._fetch(source, revision)
880 base = self._gitcommand(['merge-base', revision, self._state[1]])
887 base = self._gitcommand(['merge-base', revision, self._state[1]])
881 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
888 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
882
889
883 def mergefunc():
890 def mergefunc():
884 if base == revision:
891 if base == revision:
885 self.get(state) # fast forward merge
892 self.get(state) # fast forward merge
886 elif base != self._state[1]:
893 elif base != self._state[1]:
887 self._gitcommand(['merge', '--no-commit', revision])
894 self._gitcommand(['merge', '--no-commit', revision])
888
895
889 if self.dirty():
896 if self.dirty():
890 if self._gitstate() != revision:
897 if self._gitstate() != revision:
891 dirty = self._gitstate() == self._state[1] or code != 0
898 dirty = self._gitstate() == self._state[1] or code != 0
892 if _updateprompt(self._ui, self, dirty,
899 if _updateprompt(self._ui, self, dirty,
893 self._state[1][:7], revision[:7]):
900 self._state[1][:7], revision[:7]):
894 mergefunc()
901 mergefunc()
895 else:
902 else:
896 mergefunc()
903 mergefunc()
897
904
898 def push(self, force):
905 def push(self, force):
906 if self._gitmissing():
907 raise util.Abort(_("subrepo %s is missing") % self._relpath)
899 # if a branch in origin contains the revision, nothing to do
908 # if a branch in origin contains the revision, nothing to do
900 branch2rev, rev2branch = self._gitbranchmap()
909 branch2rev, rev2branch = self._gitbranchmap()
901 if self._state[1] in rev2branch:
910 if self._state[1] in rev2branch:
902 for b in rev2branch[self._state[1]]:
911 for b in rev2branch[self._state[1]]:
903 if b.startswith('refs/remotes/origin/'):
912 if b.startswith('refs/remotes/origin/'):
904 return True
913 return True
905 for b, revision in branch2rev.iteritems():
914 for b, revision in branch2rev.iteritems():
906 if b.startswith('refs/remotes/origin/'):
915 if b.startswith('refs/remotes/origin/'):
907 if self._gitisancestor(self._state[1], revision):
916 if self._gitisancestor(self._state[1], revision):
908 return True
917 return True
909 # otherwise, try to push the currently checked out branch
918 # otherwise, try to push the currently checked out branch
910 cmd = ['push']
919 cmd = ['push']
911 if force:
920 if force:
912 cmd.append('--force')
921 cmd.append('--force')
913
922
914 current = self._gitcurrentbranch()
923 current = self._gitcurrentbranch()
915 if current:
924 if current:
916 # determine if the current branch is even useful
925 # determine if the current branch is even useful
917 if not self._gitisancestor(self._state[1], current):
926 if not self._gitisancestor(self._state[1], current):
918 self._ui.warn(_('unrelated git branch checked out '
927 self._ui.warn(_('unrelated git branch checked out '
919 'in subrepo %s\n') % self._relpath)
928 'in subrepo %s\n') % self._relpath)
920 return False
929 return False
921 self._ui.status(_('pushing branch %s of subrepo %s\n') %
930 self._ui.status(_('pushing branch %s of subrepo %s\n') %
922 (current.split('/', 2)[2], self._relpath))
931 (current.split('/', 2)[2], self._relpath))
923 self._gitcommand(cmd + ['origin', current])
932 self._gitcommand(cmd + ['origin', current])
924 return True
933 return True
925 else:
934 else:
926 self._ui.warn(_('no branch checked out in subrepo %s\n'
935 self._ui.warn(_('no branch checked out in subrepo %s\n'
927 'cannot push revision %s') %
936 'cannot push revision %s') %
928 (self._relpath, self._state[1]))
937 (self._relpath, self._state[1]))
929 return False
938 return False
930
939
931 def remove(self):
940 def remove(self):
941 if self._gitmissing():
942 return
932 if self.dirty():
943 if self.dirty():
933 self._ui.warn(_('not removing repo %s because '
944 self._ui.warn(_('not removing repo %s because '
934 'it has changes.\n') % self._relpath)
945 'it has changes.\n') % self._relpath)
935 return
946 return
936 # we can't fully delete the repository as it may contain
947 # we can't fully delete the repository as it may contain
937 # local-only history
948 # local-only history
938 self._ui.note(_('removing subrepo %s\n') % self._relpath)
949 self._ui.note(_('removing subrepo %s\n') % self._relpath)
939 self._gitcommand(['config', 'core.bare', 'true'])
950 self._gitcommand(['config', 'core.bare', 'true'])
940 for f in os.listdir(self._abspath):
951 for f in os.listdir(self._abspath):
941 if f == '.git':
952 if f == '.git':
942 continue
953 continue
943 path = os.path.join(self._abspath, f)
954 path = os.path.join(self._abspath, f)
944 if os.path.isdir(path) and not os.path.islink(path):
955 if os.path.isdir(path) and not os.path.islink(path):
945 shutil.rmtree(path)
956 shutil.rmtree(path)
946 else:
957 else:
947 os.remove(path)
958 os.remove(path)
948
959
949 def archive(self, ui, archiver, prefix):
960 def archive(self, ui, archiver, prefix):
950 source, revision = self._state
961 source, revision = self._state
951 self._fetch(source, revision)
962 self._fetch(source, revision)
952
963
953 # Parse git's native archive command.
964 # Parse git's native archive command.
954 # This should be much faster than manually traversing the trees
965 # This should be much faster than manually traversing the trees
955 # and objects with many subprocess calls.
966 # and objects with many subprocess calls.
956 tarstream = self._gitcommand(['archive', revision], stream=True)
967 tarstream = self._gitcommand(['archive', revision], stream=True)
957 tar = tarfile.open(fileobj=tarstream, mode='r|')
968 tar = tarfile.open(fileobj=tarstream, mode='r|')
958 relpath = subrelpath(self)
969 relpath = subrelpath(self)
959 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
970 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
960 for i, info in enumerate(tar):
971 for i, info in enumerate(tar):
961 if info.isdir():
972 if info.isdir():
962 continue
973 continue
963 if info.issym():
974 if info.issym():
964 data = info.linkname
975 data = info.linkname
965 else:
976 else:
966 data = tar.extractfile(info).read()
977 data = tar.extractfile(info).read()
967 archiver.addfile(os.path.join(prefix, self._path, info.name),
978 archiver.addfile(os.path.join(prefix, self._path, info.name),
968 info.mode, info.issym(), data)
979 info.mode, info.issym(), data)
969 ui.progress(_('archiving (%s)') % relpath, i + 1,
980 ui.progress(_('archiving (%s)') % relpath, i + 1,
970 unit=_('files'))
981 unit=_('files'))
971 ui.progress(_('archiving (%s)') % relpath, None)
982 ui.progress(_('archiving (%s)') % relpath, None)
972
983
973
984
974 def status(self, rev2, **opts):
985 def status(self, rev2, **opts):
986 if self._gitmissing():
987 # if the repo is missing, return no results
988 return [], [], [], [], [], [], []
975 rev1 = self._state[1]
989 rev1 = self._state[1]
976 modified, added, removed = [], [], []
990 modified, added, removed = [], [], []
977 if rev2:
991 if rev2:
978 command = ['diff-tree', rev1, rev2]
992 command = ['diff-tree', rev1, rev2]
979 else:
993 else:
980 command = ['diff-index', rev1]
994 command = ['diff-index', rev1]
981 out = self._gitcommand(command)
995 out = self._gitcommand(command)
982 for line in out.split('\n'):
996 for line in out.split('\n'):
983 tab = line.find('\t')
997 tab = line.find('\t')
984 if tab == -1:
998 if tab == -1:
985 continue
999 continue
986 status, f = line[tab - 1], line[tab + 1:]
1000 status, f = line[tab - 1], line[tab + 1:]
987 if status == 'M':
1001 if status == 'M':
988 modified.append(f)
1002 modified.append(f)
989 elif status == 'A':
1003 elif status == 'A':
990 added.append(f)
1004 added.append(f)
991 elif status == 'D':
1005 elif status == 'D':
992 removed.append(f)
1006 removed.append(f)
993
1007
994 deleted = unknown = ignored = clean = []
1008 deleted = unknown = ignored = clean = []
995 return modified, added, removed, deleted, unknown, ignored, clean
1009 return modified, added, removed, deleted, unknown, ignored, clean
996
1010
997 types = {
1011 types = {
998 'hg': hgsubrepo,
1012 'hg': hgsubrepo,
999 'svn': svnsubrepo,
1013 'svn': svnsubrepo,
1000 'git': gitsubrepo,
1014 'git': gitsubrepo,
1001 }
1015 }
@@ -1,454 +1,474 b''
1 $ "$TESTDIR/hghave" git || exit 80
1 $ "$TESTDIR/hghave" git || exit 80
2
2
3 make git commits repeatable
3 make git commits repeatable
4
4
5 $ GIT_AUTHOR_NAME='test'; export GIT_AUTHOR_NAME
5 $ GIT_AUTHOR_NAME='test'; export GIT_AUTHOR_NAME
6 $ GIT_AUTHOR_EMAIL='test@example.org'; export GIT_AUTHOR_EMAIL
6 $ GIT_AUTHOR_EMAIL='test@example.org'; export GIT_AUTHOR_EMAIL
7 $ GIT_AUTHOR_DATE='1234567891 +0000'; export GIT_AUTHOR_DATE
7 $ GIT_AUTHOR_DATE='1234567891 +0000'; export GIT_AUTHOR_DATE
8 $ GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"; export GIT_COMMITTER_NAME
8 $ GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"; export GIT_COMMITTER_NAME
9 $ GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"; export GIT_COMMITTER_EMAIL
9 $ GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"; export GIT_COMMITTER_EMAIL
10 $ GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"; export GIT_COMMITTER_DATE
10 $ GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"; export GIT_COMMITTER_DATE
11
11
12 root hg repo
12 root hg repo
13
13
14 $ hg init t
14 $ hg init t
15 $ cd t
15 $ cd t
16 $ echo a > a
16 $ echo a > a
17 $ hg add a
17 $ hg add a
18 $ hg commit -m a
18 $ hg commit -m a
19 $ cd ..
19 $ cd ..
20
20
21 new external git repo
21 new external git repo
22
22
23 $ mkdir gitroot
23 $ mkdir gitroot
24 $ cd gitroot
24 $ cd gitroot
25 $ git init -q
25 $ git init -q
26 $ echo g > g
26 $ echo g > g
27 $ git add g
27 $ git add g
28 $ git commit -q -m g
28 $ git commit -q -m g
29
29
30 add subrepo clone
30 add subrepo clone
31
31
32 $ cd ../t
32 $ cd ../t
33 $ echo 's = [git]../gitroot' > .hgsub
33 $ echo 's = [git]../gitroot' > .hgsub
34 $ git clone -q ../gitroot s
34 $ git clone -q ../gitroot s
35 $ hg add .hgsub
35 $ hg add .hgsub
36 $ hg commit -m 'new git subrepo'
36 $ hg commit -m 'new git subrepo'
37 committing subrepository s
37 committing subrepository s
38 $ hg debugsub
38 $ hg debugsub
39 path s
39 path s
40 source ../gitroot
40 source ../gitroot
41 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
41 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
42
42
43 record a new commit from upstream from a different branch
43 record a new commit from upstream from a different branch
44
44
45 $ cd ../gitroot
45 $ cd ../gitroot
46 $ git checkout -q -b testing
46 $ git checkout -q -b testing
47 $ echo gg >> g
47 $ echo gg >> g
48 $ git commit -q -a -m gg
48 $ git commit -q -a -m gg
49
49
50 $ cd ../t/s
50 $ cd ../t/s
51 $ git pull -q >/dev/null 2>/dev/null
51 $ git pull -q >/dev/null 2>/dev/null
52 $ git checkout -q -b testing origin/testing >/dev/null
52 $ git checkout -q -b testing origin/testing >/dev/null
53
53
54 $ cd ..
54 $ cd ..
55 $ hg status --subrepos
55 $ hg status --subrepos
56 M s/g
56 M s/g
57 $ hg commit -m 'update git subrepo'
57 $ hg commit -m 'update git subrepo'
58 committing subrepository s
58 committing subrepository s
59 $ hg debugsub
59 $ hg debugsub
60 path s
60 path s
61 source ../gitroot
61 source ../gitroot
62 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
62 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
63
63
64 make $GITROOT pushable, by replacing it with a clone with nothing checked out
64 make $GITROOT pushable, by replacing it with a clone with nothing checked out
65
65
66 $ cd ..
66 $ cd ..
67 $ git clone gitroot gitrootbare --bare -q
67 $ git clone gitroot gitrootbare --bare -q
68 $ rm -rf gitroot
68 $ rm -rf gitroot
69 $ mv gitrootbare gitroot
69 $ mv gitrootbare gitroot
70
70
71 clone root
71 clone root
72
72
73 $ cd t
73 $ cd t
74 $ hg clone . ../tc
74 $ hg clone . ../tc
75 updating to branch default
75 updating to branch default
76 cloning subrepo s
76 cloning subrepo s
77 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
77 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
78 $ cd ../tc
78 $ cd ../tc
79 $ hg debugsub
79 $ hg debugsub
80 path s
80 path s
81 source ../gitroot
81 source ../gitroot
82 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
82 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
83
83
84 update to previous substate
84 update to previous substate
85
85
86 $ hg update 1 -q
86 $ hg update 1 -q
87 $ cat s/g
87 $ cat s/g
88 g
88 g
89 $ hg debugsub
89 $ hg debugsub
90 path s
90 path s
91 source ../gitroot
91 source ../gitroot
92 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
92 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
93
93
94 clone root, make local change
94 clone root, make local change
95
95
96 $ cd ../t
96 $ cd ../t
97 $ hg clone . ../ta
97 $ hg clone . ../ta
98 updating to branch default
98 updating to branch default
99 cloning subrepo s
99 cloning subrepo s
100 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
100 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
101
101
102 $ cd ../ta
102 $ cd ../ta
103 $ echo ggg >> s/g
103 $ echo ggg >> s/g
104 $ hg status --subrepos
104 $ hg status --subrepos
105 M s/g
105 M s/g
106 $ hg commit -m ggg
106 $ hg commit -m ggg
107 committing subrepository s
107 committing subrepository s
108 $ hg debugsub
108 $ hg debugsub
109 path s
109 path s
110 source ../gitroot
110 source ../gitroot
111 revision 79695940086840c99328513acbe35f90fcd55e57
111 revision 79695940086840c99328513acbe35f90fcd55e57
112
112
113 clone root separately, make different local change
113 clone root separately, make different local change
114
114
115 $ cd ../t
115 $ cd ../t
116 $ hg clone . ../tb
116 $ hg clone . ../tb
117 updating to branch default
117 updating to branch default
118 cloning subrepo s
118 cloning subrepo s
119 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
119 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
120
120
121 $ cd ../tb/s
121 $ cd ../tb/s
122 $ echo f > f
122 $ echo f > f
123 $ git add f
123 $ git add f
124 $ cd ..
124 $ cd ..
125
125
126 $ hg status --subrepos
126 $ hg status --subrepos
127 A s/f
127 A s/f
128 $ hg commit -m f
128 $ hg commit -m f
129 committing subrepository s
129 committing subrepository s
130 $ hg debugsub
130 $ hg debugsub
131 path s
131 path s
132 source ../gitroot
132 source ../gitroot
133 revision aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
133 revision aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
134
134
135 user b push changes
135 user b push changes
136
136
137 $ hg push 2>/dev/null
137 $ hg push 2>/dev/null
138 pushing to $TESTTMP/t
138 pushing to $TESTTMP/t
139 pushing branch testing of subrepo s
139 pushing branch testing of subrepo s
140 searching for changes
140 searching for changes
141 adding changesets
141 adding changesets
142 adding manifests
142 adding manifests
143 adding file changes
143 adding file changes
144 added 1 changesets with 1 changes to 1 files
144 added 1 changesets with 1 changes to 1 files
145
145
146 user a pulls, merges, commits
146 user a pulls, merges, commits
147
147
148 $ cd ../ta
148 $ cd ../ta
149 $ hg pull
149 $ hg pull
150 pulling from $TESTTMP/t
150 pulling from $TESTTMP/t
151 searching for changes
151 searching for changes
152 adding changesets
152 adding changesets
153 adding manifests
153 adding manifests
154 adding file changes
154 adding file changes
155 added 1 changesets with 1 changes to 1 files (+1 heads)
155 added 1 changesets with 1 changes to 1 files (+1 heads)
156 (run 'hg heads' to see heads, 'hg merge' to merge)
156 (run 'hg heads' to see heads, 'hg merge' to merge)
157 $ hg merge 2>/dev/null
157 $ hg merge 2>/dev/null
158 pulling subrepo s
158 pulling subrepo s
159 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
159 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
160 (branch merge, don't forget to commit)
160 (branch merge, don't forget to commit)
161 $ cat s/f
161 $ cat s/f
162 f
162 f
163 $ cat s/g
163 $ cat s/g
164 g
164 g
165 gg
165 gg
166 ggg
166 ggg
167 $ hg commit -m 'merge'
167 $ hg commit -m 'merge'
168 committing subrepository s
168 committing subrepository s
169 $ hg status --subrepos --rev 1:5
169 $ hg status --subrepos --rev 1:5
170 M .hgsubstate
170 M .hgsubstate
171 M s/g
171 M s/g
172 A s/f
172 A s/f
173 $ hg debugsub
173 $ hg debugsub
174 path s
174 path s
175 source ../gitroot
175 source ../gitroot
176 revision f47b465e1bce645dbf37232a00574aa1546ca8d3
176 revision f47b465e1bce645dbf37232a00574aa1546ca8d3
177 $ hg push 2>/dev/null
177 $ hg push 2>/dev/null
178 pushing to $TESTTMP/t
178 pushing to $TESTTMP/t
179 pushing branch testing of subrepo s
179 pushing branch testing of subrepo s
180 searching for changes
180 searching for changes
181 adding changesets
181 adding changesets
182 adding manifests
182 adding manifests
183 adding file changes
183 adding file changes
184 added 2 changesets with 2 changes to 1 files
184 added 2 changesets with 2 changes to 1 files
185
185
186 make upstream git changes
186 make upstream git changes
187
187
188 $ cd ..
188 $ cd ..
189 $ git clone -q gitroot gitclone
189 $ git clone -q gitroot gitclone
190 $ cd gitclone
190 $ cd gitclone
191 $ echo ff >> f
191 $ echo ff >> f
192 $ git commit -q -a -m ff
192 $ git commit -q -a -m ff
193 $ echo fff >> f
193 $ echo fff >> f
194 $ git commit -q -a -m fff
194 $ git commit -q -a -m fff
195 $ git push origin testing 2>/dev/null
195 $ git push origin testing 2>/dev/null
196
196
197 make and push changes to hg without updating the subrepo
197 make and push changes to hg without updating the subrepo
198
198
199 $ cd ../t
199 $ cd ../t
200 $ hg clone . ../td
200 $ hg clone . ../td
201 updating to branch default
201 updating to branch default
202 cloning subrepo s
202 cloning subrepo s
203 checking out detached HEAD in subrepo s
203 checking out detached HEAD in subrepo s
204 check out a git branch if you intend to make changes
204 check out a git branch if you intend to make changes
205 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
205 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
206 $ cd ../td
206 $ cd ../td
207 $ echo aa >> a
207 $ echo aa >> a
208 $ hg commit -m aa
208 $ hg commit -m aa
209 $ hg push
209 $ hg push
210 pushing to $TESTTMP/t
210 pushing to $TESTTMP/t
211 searching for changes
211 searching for changes
212 adding changesets
212 adding changesets
213 adding manifests
213 adding manifests
214 adding file changes
214 adding file changes
215 added 1 changesets with 1 changes to 1 files
215 added 1 changesets with 1 changes to 1 files
216
216
217 sync to upstream git, distribute changes
217 sync to upstream git, distribute changes
218
218
219 $ cd ../ta
219 $ cd ../ta
220 $ hg pull -u -q
220 $ hg pull -u -q
221 $ cd s
221 $ cd s
222 $ git pull -q >/dev/null 2>/dev/null
222 $ git pull -q >/dev/null 2>/dev/null
223 $ cd ..
223 $ cd ..
224 $ hg commit -m 'git upstream sync'
224 $ hg commit -m 'git upstream sync'
225 committing subrepository s
225 committing subrepository s
226 $ hg debugsub
226 $ hg debugsub
227 path s
227 path s
228 source ../gitroot
228 source ../gitroot
229 revision 32a343883b74769118bb1d3b4b1fbf9156f4dddc
229 revision 32a343883b74769118bb1d3b4b1fbf9156f4dddc
230 $ hg push -q
230 $ hg push -q
231
231
232 $ cd ../tb
232 $ cd ../tb
233 $ hg pull -q
233 $ hg pull -q
234 $ hg update 2>/dev/null
234 $ hg update 2>/dev/null
235 pulling subrepo s
235 pulling subrepo s
236 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
236 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
237 $ hg debugsub
237 $ hg debugsub
238 path s
238 path s
239 source ../gitroot
239 source ../gitroot
240 revision 32a343883b74769118bb1d3b4b1fbf9156f4dddc
240 revision 32a343883b74769118bb1d3b4b1fbf9156f4dddc
241
241
242 update to a revision without the subrepo, keeping the local git repository
242 update to a revision without the subrepo, keeping the local git repository
243
243
244 $ cd ../t
244 $ cd ../t
245 $ hg up 0
245 $ hg up 0
246 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
246 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
247 $ ls -a s
247 $ ls -a s
248 .
248 .
249 ..
249 ..
250 .git
250 .git
251
251
252 $ hg up 2
252 $ hg up 2
253 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
253 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
254 $ ls -a s
254 $ ls -a s
255 .
255 .
256 ..
256 ..
257 .git
257 .git
258 g
258 g
259
259
260 archive subrepos
260 archive subrepos
261
261
262 $ cd ../tc
262 $ cd ../tc
263 $ hg pull -q
263 $ hg pull -q
264 $ hg archive --subrepos -r 5 ../archive 2>/dev/null
264 $ hg archive --subrepos -r 5 ../archive 2>/dev/null
265 pulling subrepo s
265 pulling subrepo s
266 $ cd ../archive
266 $ cd ../archive
267 $ cat s/f
267 $ cat s/f
268 f
268 f
269 $ cat s/g
269 $ cat s/g
270 g
270 g
271 gg
271 gg
272 ggg
272 ggg
273
273
274 create nested repo
274 create nested repo
275
275
276 $ cd ..
276 $ cd ..
277 $ hg init outer
277 $ hg init outer
278 $ cd outer
278 $ cd outer
279 $ echo b>b
279 $ echo b>b
280 $ hg add b
280 $ hg add b
281 $ hg commit -m b
281 $ hg commit -m b
282
282
283 $ hg clone ../t inner
283 $ hg clone ../t inner
284 updating to branch default
284 updating to branch default
285 cloning subrepo s
285 cloning subrepo s
286 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
286 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
287 $ echo inner = inner > .hgsub
287 $ echo inner = inner > .hgsub
288 $ hg add .hgsub
288 $ hg add .hgsub
289 $ hg commit -m 'nested sub'
289 $ hg commit -m 'nested sub'
290 committing subrepository inner
290 committing subrepository inner
291
291
292 nested commit
292 nested commit
293
293
294 $ echo ffff >> inner/s/f
294 $ echo ffff >> inner/s/f
295 $ hg status --subrepos
295 $ hg status --subrepos
296 M inner/s/f
296 M inner/s/f
297 $ hg commit -m nested
297 $ hg commit -m nested
298 committing subrepository inner
298 committing subrepository inner
299 committing subrepository inner/s
299 committing subrepository inner/s
300
300
301 nested archive
301 nested archive
302
302
303 $ hg archive --subrepos ../narchive
303 $ hg archive --subrepos ../narchive
304 $ ls ../narchive/inner/s | grep -v pax_global_header
304 $ ls ../narchive/inner/s | grep -v pax_global_header
305 f
305 f
306 g
306 g
307
307
308 relative source expansion
308 relative source expansion
309
309
310 $ cd ..
310 $ cd ..
311 $ mkdir d
311 $ mkdir d
312 $ hg clone t d/t
312 $ hg clone t d/t
313 updating to branch default
313 updating to branch default
314 cloning subrepo s
314 cloning subrepo s
315 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
315 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
316
316
317 Don't crash if the subrepo is missing
318
319 $ hg clone t missing -q
320 $ cd missing
321 $ rm -rf s
322 $ hg status -S
323 $ hg sum | grep commit
324 commit: 1 subrepos
325 $ hg push -q
326 abort: subrepo s is missing
327 [255]
328 $ hg commit -qm missing
329 abort: subrepo s is missing
330 [255]
331 $ hg update -C
332 cloning subrepo s
333 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
334 $ hg sum | grep commit
335 commit: (clean)
336
317 Check hg update --clean
337 Check hg update --clean
318 $ cd $TESTTMP/ta
338 $ cd $TESTTMP/ta
319 $ echo > s/g
339 $ echo > s/g
320 $ cd s
340 $ cd s
321 $ echo c1 > f1
341 $ echo c1 > f1
322 $ echo c1 > f2
342 $ echo c1 > f2
323 $ git add f1
343 $ git add f1
324 $ cd ..
344 $ cd ..
325 $ hg status -S
345 $ hg status -S
326 M s/g
346 M s/g
327 A s/f1
347 A s/f1
328 $ ls s
348 $ ls s
329 f
349 f
330 f1
350 f1
331 f2
351 f2
332 g
352 g
333 $ hg update --clean
353 $ hg update --clean
334 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
354 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
335 $ hg status -S
355 $ hg status -S
336 $ ls s
356 $ ls s
337 f
357 f
338 f1
358 f1
339 f2
359 f2
340 g
360 g
341
361
342 Sticky subrepositories, no changes
362 Sticky subrepositories, no changes
343 $ cd $TESTTMP/ta
363 $ cd $TESTTMP/ta
344 $ hg id -n
364 $ hg id -n
345 7
365 7
346 $ cd s
366 $ cd s
347 $ git rev-parse HEAD
367 $ git rev-parse HEAD
348 32a343883b74769118bb1d3b4b1fbf9156f4dddc
368 32a343883b74769118bb1d3b4b1fbf9156f4dddc
349 $ cd ..
369 $ cd ..
350 $ hg update 1 > /dev/null 2>&1
370 $ hg update 1 > /dev/null 2>&1
351 $ hg id -n
371 $ hg id -n
352 1
372 1
353 $ cd s
373 $ cd s
354 $ git rev-parse HEAD
374 $ git rev-parse HEAD
355 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
375 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
356 $ cd ..
376 $ cd ..
357
377
358 Sticky subrepositorys, file changes
378 Sticky subrepositorys, file changes
359 $ touch s/f1
379 $ touch s/f1
360 $ cd s
380 $ cd s
361 $ git add f1
381 $ git add f1
362 $ cd ..
382 $ cd ..
363 $ hg id -n
383 $ hg id -n
364 1
384 1
365 $ cd s
385 $ cd s
366 $ git rev-parse HEAD
386 $ git rev-parse HEAD
367 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
387 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
368 $ cd ..
388 $ cd ..
369 $ hg update 4
389 $ hg update 4
370 subrepository sources for s differ
390 subrepository sources for s differ
371 use (l)ocal source (da5f5b1) or (r)emote source (aa84837)?
391 use (l)ocal source (da5f5b1) or (r)emote source (aa84837)?
372 l
392 l
373 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
393 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
374 $ hg id -n
394 $ hg id -n
375 4+
395 4+
376 $ cd s
396 $ cd s
377 $ git rev-parse HEAD
397 $ git rev-parse HEAD
378 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
398 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
379 $ cd ..
399 $ cd ..
380 $ hg update --clean tip > /dev/null 2>&1
400 $ hg update --clean tip > /dev/null 2>&1
381
401
382 Sticky subrepository, revision updates
402 Sticky subrepository, revision updates
383 $ hg id -n
403 $ hg id -n
384 7
404 7
385 $ cd s
405 $ cd s
386 $ git rev-parse HEAD
406 $ git rev-parse HEAD
387 32a343883b74769118bb1d3b4b1fbf9156f4dddc
407 32a343883b74769118bb1d3b4b1fbf9156f4dddc
388 $ cd ..
408 $ cd ..
389 $ cd s
409 $ cd s
390 $ git checkout aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
410 $ git checkout aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
391 Previous HEAD position was 32a3438... fff
411 Previous HEAD position was 32a3438... fff
392 HEAD is now at aa84837... f
412 HEAD is now at aa84837... f
393 $ cd ..
413 $ cd ..
394 $ hg update 1
414 $ hg update 1
395 subrepository sources for s differ (in checked out version)
415 subrepository sources for s differ (in checked out version)
396 use (l)ocal source (32a3438) or (r)emote source (da5f5b1)?
416 use (l)ocal source (32a3438) or (r)emote source (da5f5b1)?
397 l
417 l
398 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
418 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
399 $ hg id -n
419 $ hg id -n
400 1+
420 1+
401 $ cd s
421 $ cd s
402 $ git rev-parse HEAD
422 $ git rev-parse HEAD
403 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
423 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
404 $ cd ..
424 $ cd ..
405
425
406 Sticky subrepository, file changes and revision updates
426 Sticky subrepository, file changes and revision updates
407 $ touch s/f1
427 $ touch s/f1
408 $ cd s
428 $ cd s
409 $ git add f1
429 $ git add f1
410 $ git rev-parse HEAD
430 $ git rev-parse HEAD
411 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
431 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
412 $ cd ..
432 $ cd ..
413 $ hg id -n
433 $ hg id -n
414 1+
434 1+
415 $ hg update 7
435 $ hg update 7
416 subrepository sources for s differ
436 subrepository sources for s differ
417 use (l)ocal source (32a3438) or (r)emote source (32a3438)?
437 use (l)ocal source (32a3438) or (r)emote source (32a3438)?
418 l
438 l
419 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
439 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
420 $ hg id -n
440 $ hg id -n
421 7
441 7
422 $ cd s
442 $ cd s
423 $ git rev-parse HEAD
443 $ git rev-parse HEAD
424 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
444 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
425 $ cd ..
445 $ cd ..
426
446
427 Sticky repository, update --clean
447 Sticky repository, update --clean
428 $ hg update --clean tip
448 $ hg update --clean tip
429 Previous HEAD position was aa84837... f
449 Previous HEAD position was aa84837... f
430 HEAD is now at 32a3438... fff
450 HEAD is now at 32a3438... fff
431 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
451 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
432 $ hg id -n
452 $ hg id -n
433 7
453 7
434 $ cd s
454 $ cd s
435 $ git rev-parse HEAD
455 $ git rev-parse HEAD
436 32a343883b74769118bb1d3b4b1fbf9156f4dddc
456 32a343883b74769118bb1d3b4b1fbf9156f4dddc
437 $ cd ..
457 $ cd ..
438
458
439 Test subrepo already at intended revision:
459 Test subrepo already at intended revision:
440 $ cd s
460 $ cd s
441 $ git checkout 32a343883b74769118bb1d3b4b1fbf9156f4dddc
461 $ git checkout 32a343883b74769118bb1d3b4b1fbf9156f4dddc
442 HEAD is now at 32a3438... fff
462 HEAD is now at 32a3438... fff
443 $ cd ..
463 $ cd ..
444 $ hg update 1
464 $ hg update 1
445 Previous HEAD position was 32a3438... fff
465 Previous HEAD position was 32a3438... fff
446 HEAD is now at da5f5b1... g
466 HEAD is now at da5f5b1... g
447 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
467 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
448 $ hg id -n
468 $ hg id -n
449 1
469 1
450 $ cd s
470 $ cd s
451 $ git rev-parse HEAD
471 $ git rev-parse HEAD
452 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
472 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
453 $ cd ..
473 $ cd ..
454
474
General Comments 0
You need to be logged in to leave comments. Login now