##// END OF EJS Templates
subrepo: add update/merge logic
Matt Mackall -
r8814:ab668c92 default
parent child Browse files
Show More
@@ -1,360 +1,362 b''
1 1 # hg.py - repository classes for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2, incorporated herein by reference.
8 8
9 9 from i18n import _
10 10 from lock import release
11 11 import localrepo, bundlerepo, httprepo, sshrepo, statichttprepo
12 12 import lock, util, extensions, error
13 13 import merge as _merge
14 14 import verify as _verify
15 15 import errno, os, shutil
16 16
17 17 def _local(path):
18 18 return (os.path.isfile(util.drop_scheme('file', path)) and
19 19 bundlerepo or localrepo)
20 20
21 21 def parseurl(url, revs=[]):
22 22 '''parse url#branch, returning url, branch + revs'''
23 23
24 24 if '#' not in url:
25 25 return url, (revs or None), revs and revs[-1] or None
26 26
27 27 url, branch = url.split('#', 1)
28 28 checkout = revs and revs[-1] or branch
29 29 return url, (revs or []) + [branch], checkout
30 30
31 31 schemes = {
32 32 'bundle': bundlerepo,
33 33 'file': _local,
34 34 'http': httprepo,
35 35 'https': httprepo,
36 36 'ssh': sshrepo,
37 37 'static-http': statichttprepo,
38 38 }
39 39
40 40 def _lookup(path):
41 41 scheme = 'file'
42 42 if path:
43 43 c = path.find(':')
44 44 if c > 0:
45 45 scheme = path[:c]
46 46 thing = schemes.get(scheme) or schemes['file']
47 47 try:
48 48 return thing(path)
49 49 except TypeError:
50 50 return thing
51 51
52 52 def islocal(repo):
53 53 '''return true if repo or path is local'''
54 54 if isinstance(repo, str):
55 55 try:
56 56 return _lookup(repo).islocal(repo)
57 57 except AttributeError:
58 58 return False
59 59 return repo.local()
60 60
61 61 def repository(ui, path='', create=False):
62 62 """return a repository object for the specified path"""
63 63 repo = _lookup(path).instance(ui, path, create)
64 64 ui = getattr(repo, "ui", ui)
65 65 for name, module in extensions.extensions():
66 66 hook = getattr(module, 'reposetup', None)
67 67 if hook:
68 68 hook(ui, repo)
69 69 return repo
70 70
71 71 def defaultdest(source):
72 72 '''return default destination of clone if none is given'''
73 73 return os.path.basename(os.path.normpath(source))
74 74
75 75 def localpath(path):
76 76 if path.startswith('file://localhost/'):
77 77 return path[16:]
78 78 if path.startswith('file://'):
79 79 return path[7:]
80 80 if path.startswith('file:'):
81 81 return path[5:]
82 82 return path
83 83
84 84 def share(ui, source, dest=None, update=True):
85 85 '''create a shared repository'''
86 86
87 87 if not islocal(source):
88 88 raise util.Abort(_('can only share local repositories'))
89 89
90 90 if not dest:
91 91 dest = os.path.basename(source)
92 92
93 93 if isinstance(source, str):
94 94 origsource = ui.expandpath(source)
95 95 source, rev, checkout = parseurl(origsource, '')
96 96 srcrepo = repository(ui, source)
97 97 else:
98 98 srcrepo = source
99 99 origsource = source = srcrepo.url()
100 100 checkout = None
101 101
102 102 sharedpath = srcrepo.sharedpath # if our source is already sharing
103 103
104 104 root = os.path.realpath(dest)
105 105 roothg = os.path.join(root, '.hg')
106 106
107 107 if os.path.exists(roothg):
108 108 raise util.Abort(_('destination already exists'))
109 109
110 110 if not os.path.isdir(root):
111 111 os.mkdir(root)
112 112 os.mkdir(roothg)
113 113
114 114 requirements = ''
115 115 try:
116 116 requirements = srcrepo.opener('requires').read()
117 117 except IOError, inst:
118 118 if inst.errno != errno.ENOENT:
119 119 raise
120 120
121 121 requirements += 'shared\n'
122 122 file(os.path.join(roothg, 'requires'), 'w').write(requirements)
123 123 file(os.path.join(roothg, 'sharedpath'), 'w').write(sharedpath)
124 124
125 125 default = srcrepo.ui.config('paths', 'default')
126 126 if default:
127 127 f = file(os.path.join(roothg, 'hgrc'), 'w')
128 128 f.write('[paths]\ndefault = %s\n' % default)
129 129 f.close()
130 130
131 131 r = repository(ui, root)
132 132
133 133 if update:
134 134 r.ui.status(_("updating working directory\n"))
135 135 if update is not True:
136 136 checkout = update
137 137 for test in (checkout, 'default', 'tip'):
138 138 try:
139 139 uprev = r.lookup(test)
140 140 break
141 141 except:
142 142 continue
143 143 _update(r, uprev)
144 144
145 145 def clone(ui, source, dest=None, pull=False, rev=None, update=True,
146 146 stream=False):
147 147 """Make a copy of an existing repository.
148 148
149 149 Create a copy of an existing repository in a new directory. The
150 150 source and destination are URLs, as passed to the repository
151 151 function. Returns a pair of repository objects, the source and
152 152 newly created destination.
153 153
154 154 The location of the source is added to the new repository's
155 155 .hg/hgrc file, as the default to be used for future pulls and
156 156 pushes.
157 157
158 158 If an exception is raised, the partly cloned/updated destination
159 159 repository will be deleted.
160 160
161 161 Arguments:
162 162
163 163 source: repository object or URL
164 164
165 165 dest: URL of destination repository to create (defaults to base
166 166 name of source repository)
167 167
168 168 pull: always pull from source repository, even in local case
169 169
170 170 stream: stream raw data uncompressed from repository (fast over
171 171 LAN, slow over WAN)
172 172
173 173 rev: revision to clone up to (implies pull=True)
174 174
175 175 update: update working directory after clone completes, if
176 176 destination is local repository (True means update to default rev,
177 177 anything else is treated as a revision)
178 178 """
179 179
180 180 if isinstance(source, str):
181 181 origsource = ui.expandpath(source)
182 182 source, rev, checkout = parseurl(origsource, rev)
183 183 src_repo = repository(ui, source)
184 184 else:
185 185 src_repo = source
186 186 origsource = source = src_repo.url()
187 187 checkout = rev and rev[-1] or None
188 188
189 189 if dest is None:
190 190 dest = defaultdest(source)
191 191 ui.status(_("destination directory: %s\n") % dest)
192 192
193 193 dest = localpath(dest)
194 194 source = localpath(source)
195 195
196 196 if os.path.exists(dest):
197 197 if not os.path.isdir(dest):
198 198 raise util.Abort(_("destination '%s' already exists") % dest)
199 199 elif os.listdir(dest):
200 200 raise util.Abort(_("destination '%s' is not empty") % dest)
201 201
202 202 class DirCleanup(object):
203 203 def __init__(self, dir_):
204 204 self.rmtree = shutil.rmtree
205 205 self.dir_ = dir_
206 206 def close(self):
207 207 self.dir_ = None
208 208 def cleanup(self):
209 209 if self.dir_:
210 210 self.rmtree(self.dir_, True)
211 211
212 212 src_lock = dest_lock = dir_cleanup = None
213 213 try:
214 214 if islocal(dest):
215 215 dir_cleanup = DirCleanup(dest)
216 216
217 217 abspath = origsource
218 218 copy = False
219 219 if src_repo.cancopy() and islocal(dest):
220 220 abspath = os.path.abspath(util.drop_scheme('file', origsource))
221 221 copy = not pull and not rev
222 222
223 223 if copy:
224 224 try:
225 225 # we use a lock here because if we race with commit, we
226 226 # can end up with extra data in the cloned revlogs that's
227 227 # not pointed to by changesets, thus causing verify to
228 228 # fail
229 229 src_lock = src_repo.lock(wait=False)
230 230 except error.LockError:
231 231 copy = False
232 232
233 233 if copy:
234 234 hgdir = os.path.realpath(os.path.join(dest, ".hg"))
235 235 if not os.path.exists(dest):
236 236 os.mkdir(dest)
237 237 else:
238 238 # only clean up directories we create ourselves
239 239 dir_cleanup.dir_ = hgdir
240 240 try:
241 241 dest_path = hgdir
242 242 os.mkdir(dest_path)
243 243 except OSError, inst:
244 244 if inst.errno == errno.EEXIST:
245 245 dir_cleanup.close()
246 246 raise util.Abort(_("destination '%s' already exists")
247 247 % dest)
248 248 raise
249 249
250 250 for f in src_repo.store.copylist():
251 251 src = os.path.join(src_repo.path, f)
252 252 dst = os.path.join(dest_path, f)
253 253 dstbase = os.path.dirname(dst)
254 254 if dstbase and not os.path.exists(dstbase):
255 255 os.mkdir(dstbase)
256 256 if os.path.exists(src):
257 257 if dst.endswith('data'):
258 258 # lock to avoid premature writing to the target
259 259 dest_lock = lock.lock(os.path.join(dstbase, "lock"))
260 260 util.copyfiles(src, dst)
261 261
262 262 # we need to re-init the repo after manually copying the data
263 263 # into it
264 264 dest_repo = repository(ui, dest)
265 265
266 266 else:
267 267 try:
268 268 dest_repo = repository(ui, dest, create=True)
269 269 except OSError, inst:
270 270 if inst.errno == errno.EEXIST:
271 271 dir_cleanup.close()
272 272 raise util.Abort(_("destination '%s' already exists")
273 273 % dest)
274 274 raise
275 275
276 276 revs = None
277 277 if rev:
278 278 if 'lookup' not in src_repo.capabilities:
279 279 raise util.Abort(_("src repository does not support revision "
280 280 "lookup and so doesn't support clone by "
281 281 "revision"))
282 282 revs = [src_repo.lookup(r) for r in rev]
283 283 checkout = revs[0]
284 284 if dest_repo.local():
285 285 dest_repo.clone(src_repo, heads=revs, stream=stream)
286 286 elif src_repo.local():
287 287 src_repo.push(dest_repo, revs=revs)
288 288 else:
289 289 raise util.Abort(_("clone from remote to remote not supported"))
290 290
291 291 if dir_cleanup:
292 292 dir_cleanup.close()
293 293
294 294 if dest_repo.local():
295 295 fp = dest_repo.opener("hgrc", "w", text=True)
296 296 fp.write("[paths]\n")
297 297 fp.write("default = %s\n" % abspath)
298 298 fp.close()
299 299
300 dest_repo.ui.setconfig('paths', 'default', abspath)
301
300 302 if update:
301 303 dest_repo.ui.status(_("updating working directory\n"))
302 304 if update is not True:
303 305 checkout = update
304 306 for test in (checkout, 'default', 'tip'):
305 307 try:
306 308 uprev = dest_repo.lookup(test)
307 309 break
308 310 except:
309 311 continue
310 312 _update(dest_repo, uprev)
311 313
312 314 return src_repo, dest_repo
313 315 finally:
314 316 release(src_lock, dest_lock)
315 317 if dir_cleanup is not None:
316 318 dir_cleanup.cleanup()
317 319
318 320 def _showstats(repo, stats):
319 321 stats = ((stats[0], _("updated")),
320 322 (stats[1], _("merged")),
321 323 (stats[2], _("removed")),
322 324 (stats[3], _("unresolved")))
323 325 note = ", ".join([_("%d files %s") % s for s in stats])
324 326 repo.ui.status("%s\n" % note)
325 327
326 328 def update(repo, node):
327 329 """update the working directory to node, merging linear changes"""
328 330 stats = _merge.update(repo, node, False, False, None)
329 331 _showstats(repo, stats)
330 332 if stats[3]:
331 333 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges\n"))
332 334 return stats[3] > 0
333 335
334 336 # naming conflict in clone()
335 337 _update = update
336 338
337 339 def clean(repo, node, show_stats=True):
338 340 """forcibly switch the working directory to node, clobbering changes"""
339 341 stats = _merge.update(repo, node, False, True, None)
340 342 if show_stats: _showstats(repo, stats)
341 343 return stats[3] > 0
342 344
343 345 def merge(repo, node, force=None, remind=True):
344 346 """branch merge with node, resolving changes"""
345 347 stats = _merge.update(repo, node, True, force, False)
346 348 _showstats(repo, stats)
347 349 if stats[3]:
348 350 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges "
349 351 "or 'hg up --clean' to abandon\n"))
350 352 elif remind:
351 353 repo.ui.status(_("(branch merge, don't forget to commit)\n"))
352 354 return stats[3] > 0
353 355
354 356 def revert(repo, node, choose):
355 357 """revert changes to revision in node without updating dirstate"""
356 358 return _merge.update(repo, node, False, True, choose)[3] > 0
357 359
358 360 def verify(repo):
359 361 """verify the consistency of a repository"""
360 362 return _verify.verify(repo)
@@ -1,469 +1,479 b''
1 1 # merge.py - directory-level update/merge handling for Mercurial
2 2 #
3 3 # Copyright 2006, 2007 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, incorporated herein by reference.
7 7
8 8 from node import nullid, nullrev, hex, bin
9 9 from i18n import _
10 import util, filemerge, copies
10 import util, filemerge, copies, subrepo
11 11 import errno, os, shutil
12 12
13 13 class mergestate(object):
14 14 '''track 3-way merge state of individual files'''
15 15 def __init__(self, repo):
16 16 self._repo = repo
17 17 self._read()
18 18 def reset(self, node=None):
19 19 self._state = {}
20 20 if node:
21 21 self._local = node
22 22 shutil.rmtree(self._repo.join("merge"), True)
23 23 def _read(self):
24 24 self._state = {}
25 25 try:
26 26 localnode = None
27 27 f = self._repo.opener("merge/state")
28 28 for i, l in enumerate(f):
29 29 if i == 0:
30 30 localnode = l[:-1]
31 31 else:
32 32 bits = l[:-1].split("\0")
33 33 self._state[bits[0]] = bits[1:]
34 34 self._local = bin(localnode)
35 35 except IOError, err:
36 36 if err.errno != errno.ENOENT:
37 37 raise
38 38 def _write(self):
39 39 f = self._repo.opener("merge/state", "w")
40 40 f.write(hex(self._local) + "\n")
41 41 for d, v in self._state.iteritems():
42 42 f.write("\0".join([d] + v) + "\n")
43 43 def add(self, fcl, fco, fca, fd, flags):
44 44 hash = util.sha1(fcl.path()).hexdigest()
45 45 self._repo.opener("merge/" + hash, "w").write(fcl.data())
46 46 self._state[fd] = ['u', hash, fcl.path(), fca.path(),
47 47 hex(fca.filenode()), fco.path(), flags]
48 48 self._write()
49 49 def __contains__(self, dfile):
50 50 return dfile in self._state
51 51 def __getitem__(self, dfile):
52 52 return self._state[dfile][0]
53 53 def __iter__(self):
54 54 l = self._state.keys()
55 55 l.sort()
56 56 for f in l:
57 57 yield f
58 58 def mark(self, dfile, state):
59 59 self._state[dfile][0] = state
60 60 self._write()
61 61 def resolve(self, dfile, wctx, octx):
62 62 if self[dfile] == 'r':
63 63 return 0
64 64 state, hash, lfile, afile, anode, ofile, flags = self._state[dfile]
65 65 f = self._repo.opener("merge/" + hash)
66 66 self._repo.wwrite(dfile, f.read(), flags)
67 67 fcd = wctx[dfile]
68 68 fco = octx[ofile]
69 69 fca = self._repo.filectx(afile, fileid=anode)
70 70 r = filemerge.filemerge(self._repo, self._local, lfile, fcd, fco, fca)
71 71 if not r:
72 72 self.mark(dfile, 'r')
73 73 return r
74 74
75 75 def _checkunknown(wctx, mctx):
76 76 "check for collisions between unknown files and files in mctx"
77 77 for f in wctx.unknown():
78 78 if f in mctx and mctx[f].cmp(wctx[f].data()):
79 79 raise util.Abort(_("untracked file in working directory differs"
80 80 " from file in requested revision: '%s'") % f)
81 81
82 82 def _checkcollision(mctx):
83 83 "check for case folding collisions in the destination context"
84 84 folded = {}
85 85 for fn in mctx:
86 86 fold = fn.lower()
87 87 if fold in folded:
88 88 raise util.Abort(_("case-folding collision between %s and %s")
89 89 % (fn, folded[fold]))
90 90 folded[fold] = fn
91 91
92 92 def _forgetremoved(wctx, mctx, branchmerge):
93 93 """
94 94 Forget removed files
95 95
96 96 If we're jumping between revisions (as opposed to merging), and if
97 97 neither the working directory nor the target rev has the file,
98 98 then we need to remove it from the dirstate, to prevent the
99 99 dirstate from listing the file when it is no longer in the
100 100 manifest.
101 101
102 102 If we're merging, and the other revision has removed a file
103 103 that is not present in the working directory, we need to mark it
104 104 as removed.
105 105 """
106 106
107 107 action = []
108 108 state = branchmerge and 'r' or 'f'
109 109 for f in wctx.deleted():
110 110 if f not in mctx:
111 111 action.append((f, state))
112 112
113 113 if not branchmerge:
114 114 for f in wctx.removed():
115 115 if f not in mctx:
116 116 action.append((f, "f"))
117 117
118 118 return action
119 119
120 120 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
121 121 """
122 122 Merge p1 and p2 with ancestor ma and generate merge action list
123 123
124 124 overwrite = whether we clobber working files
125 125 partial = function to filter file lists
126 126 """
127 127
128 128 def fmerge(f, f2, fa):
129 129 """merge flags"""
130 130 a, m, n = ma.flags(fa), m1.flags(f), m2.flags(f2)
131 131 if m == n: # flags agree
132 132 return m # unchanged
133 133 if m and n and not a: # flags set, don't agree, differ from parent
134 134 r = repo.ui.prompt(
135 135 _(" conflicting flags for %s\n"
136 136 "(n)one, e(x)ec or sym(l)ink?") % f,
137 137 (_("&None"), _("E&xec"), _("Sym&link")), _("n"))
138 138 return r != _("n") and r or ''
139 139 if m and m != a: # changed from a to m
140 140 return m
141 141 if n and n != a: # changed from a to n
142 142 return n
143 143 return '' # flag was cleared
144 144
145 145 def act(msg, m, f, *args):
146 146 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
147 147 action.append((f, m) + args)
148 148
149 149 action, copy = [], {}
150 150
151 151 if overwrite:
152 152 pa = p1
153 153 elif pa == p2: # backwards
154 154 pa = p1.p1()
155 155 elif pa and repo.ui.configbool("merge", "followcopies", True):
156 156 dirs = repo.ui.configbool("merge", "followdirs", True)
157 157 copy, diverge = copies.copies(repo, p1, p2, pa, dirs)
158 158 for of, fl in diverge.iteritems():
159 159 act("divergent renames", "dr", of, fl)
160 160
161 161 repo.ui.note(_("resolving manifests\n"))
162 162 repo.ui.debug(_(" overwrite %s partial %s\n") % (overwrite, bool(partial)))
163 163 repo.ui.debug(_(" ancestor %s local %s remote %s\n") % (pa, p1, p2))
164 164
165 165 m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest()
166 166 copied = set(copy.values())
167 167
168 168 # Compare manifests
169 169 for f, n in m1.iteritems():
170 170 if partial and not partial(f):
171 171 continue
172 172 if f in m2:
173 173 rflags = fmerge(f, f, f)
174 174 a = ma.get(f, nullid)
175 175 if n == m2[f] or m2[f] == a: # same or local newer
176 176 if m1.flags(f) != rflags:
177 177 act("update permissions", "e", f, rflags)
178 178 elif n == a: # remote newer
179 179 act("remote is newer", "g", f, rflags)
180 180 else: # both changed
181 181 act("versions differ", "m", f, f, f, rflags, False)
182 182 elif f in copied: # files we'll deal with on m2 side
183 183 pass
184 184 elif f in copy:
185 185 f2 = copy[f]
186 186 if f2 not in m2: # directory rename
187 187 act("remote renamed directory to " + f2, "d",
188 188 f, None, f2, m1.flags(f))
189 189 else: # case 2 A,B/B/B or case 4,21 A/B/B
190 190 act("local copied/moved to " + f2, "m",
191 191 f, f2, f, fmerge(f, f2, f2), False)
192 192 elif f in ma: # clean, a different, no remote
193 193 if n != ma[f]:
194 194 if repo.ui.prompt(
195 195 _(" local changed %s which remote deleted\n"
196 196 "use (c)hanged version or (d)elete?") % f,
197 197 (_("&Changed"), _("&Delete")), _("c")) == _("d"):
198 198 act("prompt delete", "r", f)
199 199 else:
200 200 act("prompt keep", "a", f)
201 201 elif n[20:] == "a": # added, no remote
202 202 act("remote deleted", "f", f)
203 203 elif n[20:] != "u":
204 204 act("other deleted", "r", f)
205 205
206 206 for f, n in m2.iteritems():
207 207 if partial and not partial(f):
208 208 continue
209 209 if f in m1 or f in copied: # files already visited
210 210 continue
211 211 if f in copy:
212 212 f2 = copy[f]
213 213 if f2 not in m1: # directory rename
214 214 act("local renamed directory to " + f2, "d",
215 215 None, f, f2, m2.flags(f))
216 216 elif f2 in m2: # rename case 1, A/A,B/A
217 217 act("remote copied to " + f, "m",
218 218 f2, f, f, fmerge(f2, f, f2), False)
219 219 else: # case 3,20 A/B/A
220 220 act("remote moved to " + f, "m",
221 221 f2, f, f, fmerge(f2, f, f2), True)
222 222 elif f not in ma:
223 223 act("remote created", "g", f, m2.flags(f))
224 224 elif n != ma[f]:
225 225 if repo.ui.prompt(
226 226 _("remote changed %s which local deleted\n"
227 227 "use (c)hanged version or leave (d)eleted?") % f,
228 228 (_("&Changed"), _("&Deleted")), _("c")) == _("c"):
229 229 act("prompt recreating", "g", f, m2.flags(f))
230 230
231 231 return action
232 232
233 233 def actionkey(a):
234 234 return a[1] == 'r' and -1 or 0, a
235 235
236 236 def applyupdates(repo, action, wctx, mctx):
237 237 "apply the merge action list to the working directory"
238 238
239 239 updated, merged, removed, unresolved = 0, 0, 0, 0
240 240 ms = mergestate(repo)
241 241 ms.reset(wctx.parents()[0].node())
242 242 moves = []
243 243 action.sort(key=actionkey)
244 substate = wctx.substate # prime
244 245
245 246 # prescan for merges
246 247 for a in action:
247 248 f, m = a[:2]
248 249 if m == 'm': # merge
249 250 f2, fd, flags, move = a[2:]
251 if f == '.hgsubstate': # merged internally
252 continue
250 253 repo.ui.debug(_("preserving %s for resolve of %s\n") % (f, fd))
251 254 fcl = wctx[f]
252 255 fco = mctx[f2]
253 256 fca = fcl.ancestor(fco) or repo.filectx(f, fileid=nullrev)
254 257 ms.add(fcl, fco, fca, fd, flags)
255 258 if f != fd and move:
256 259 moves.append(f)
257 260
258 261 # remove renamed files after safely stored
259 262 for f in moves:
260 263 if util.lexists(repo.wjoin(f)):
261 264 repo.ui.debug(_("removing %s\n") % f)
262 265 os.unlink(repo.wjoin(f))
263 266
264 267 audit_path = util.path_auditor(repo.root)
265 268
266 269 for a in action:
267 270 f, m = a[:2]
268 271 if f and f[0] == "/":
269 272 continue
270 273 if m == "r": # remove
271 274 repo.ui.note(_("removing %s\n") % f)
272 275 audit_path(f)
276 if f == '.hgsubstate': # subrepo states need updating
277 subrepo.submerge(repo, wctx, mctx, wctx)
273 278 try:
274 279 util.unlink(repo.wjoin(f))
275 280 except OSError, inst:
276 281 if inst.errno != errno.ENOENT:
277 282 repo.ui.warn(_("update failed to remove %s: %s!\n") %
278 283 (f, inst.strerror))
279 284 removed += 1
280 285 elif m == "m": # merge
286 if f == '.hgsubstate': # subrepo states need updating
287 subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx))
288 continue
281 289 f2, fd, flags, move = a[2:]
282 290 r = ms.resolve(fd, wctx, mctx)
283 291 if r > 0:
284 292 unresolved += 1
285 293 else:
286 294 if r is None:
287 295 updated += 1
288 296 else:
289 297 merged += 1
290 298 util.set_flags(repo.wjoin(fd), 'l' in flags, 'x' in flags)
291 299 if f != fd and move and util.lexists(repo.wjoin(f)):
292 300 repo.ui.debug(_("removing %s\n") % f)
293 301 os.unlink(repo.wjoin(f))
294 302 elif m == "g": # get
295 303 flags = a[2]
296 304 repo.ui.note(_("getting %s\n") % f)
297 305 t = mctx.filectx(f).data()
298 306 repo.wwrite(f, t, flags)
299 307 updated += 1
308 if f == '.hgsubstate': # subrepo states need updating
309 subrepo.submerge(repo, wctx, mctx, wctx)
300 310 elif m == "d": # directory rename
301 311 f2, fd, flags = a[2:]
302 312 if f:
303 313 repo.ui.note(_("moving %s to %s\n") % (f, fd))
304 314 t = wctx.filectx(f).data()
305 315 repo.wwrite(fd, t, flags)
306 316 util.unlink(repo.wjoin(f))
307 317 if f2:
308 318 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
309 319 t = mctx.filectx(f2).data()
310 320 repo.wwrite(fd, t, flags)
311 321 updated += 1
312 322 elif m == "dr": # divergent renames
313 323 fl = a[2]
314 324 repo.ui.warn(_("warning: detected divergent renames of %s to:\n") % f)
315 325 for nf in fl:
316 326 repo.ui.warn(" %s\n" % nf)
317 327 elif m == "e": # exec
318 328 flags = a[2]
319 329 util.set_flags(repo.wjoin(f), 'l' in flags, 'x' in flags)
320 330
321 331 return updated, merged, removed, unresolved
322 332
323 333 def recordupdates(repo, action, branchmerge):
324 334 "record merge actions to the dirstate"
325 335
326 336 for a in action:
327 337 f, m = a[:2]
328 338 if m == "r": # remove
329 339 if branchmerge:
330 340 repo.dirstate.remove(f)
331 341 else:
332 342 repo.dirstate.forget(f)
333 343 elif m == "a": # re-add
334 344 if not branchmerge:
335 345 repo.dirstate.add(f)
336 346 elif m == "f": # forget
337 347 repo.dirstate.forget(f)
338 348 elif m == "e": # exec change
339 349 repo.dirstate.normallookup(f)
340 350 elif m == "g": # get
341 351 if branchmerge:
342 352 repo.dirstate.normaldirty(f)
343 353 else:
344 354 repo.dirstate.normal(f)
345 355 elif m == "m": # merge
346 356 f2, fd, flag, move = a[2:]
347 357 if branchmerge:
348 358 # We've done a branch merge, mark this file as merged
349 359 # so that we properly record the merger later
350 360 repo.dirstate.merge(fd)
351 361 if f != f2: # copy/rename
352 362 if move:
353 363 repo.dirstate.remove(f)
354 364 if f != fd:
355 365 repo.dirstate.copy(f, fd)
356 366 else:
357 367 repo.dirstate.copy(f2, fd)
358 368 else:
359 369 # We've update-merged a locally modified file, so
360 370 # we set the dirstate to emulate a normal checkout
361 371 # of that file some time in the past. Thus our
362 372 # merge will appear as a normal local file
363 373 # modification.
364 374 repo.dirstate.normallookup(fd)
365 375 if move:
366 376 repo.dirstate.forget(f)
367 377 elif m == "d": # directory rename
368 378 f2, fd, flag = a[2:]
369 379 if not f2 and f not in repo.dirstate:
370 380 # untracked file moved
371 381 continue
372 382 if branchmerge:
373 383 repo.dirstate.add(fd)
374 384 if f:
375 385 repo.dirstate.remove(f)
376 386 repo.dirstate.copy(f, fd)
377 387 if f2:
378 388 repo.dirstate.copy(f2, fd)
379 389 else:
380 390 repo.dirstate.normal(fd)
381 391 if f:
382 392 repo.dirstate.forget(f)
383 393
384 394 def update(repo, node, branchmerge, force, partial):
385 395 """
386 396 Perform a merge between the working directory and the given node
387 397
388 398 branchmerge = whether to merge between branches
389 399 force = whether to force branch merging or file overwriting
390 400 partial = a function to filter file lists (dirstate not updated)
391 401 """
392 402
393 403 wlock = repo.wlock()
394 404 try:
395 405 wc = repo[None]
396 406 if node is None:
397 407 # tip of current branch
398 408 try:
399 409 node = repo.branchtags()[wc.branch()]
400 410 except KeyError:
401 411 if wc.branch() == "default": # no default branch!
402 412 node = repo.lookup("tip") # update to tip
403 413 else:
404 414 raise util.Abort(_("branch %s not found") % wc.branch())
405 415 overwrite = force and not branchmerge
406 416 pl = wc.parents()
407 417 p1, p2 = pl[0], repo[node]
408 418 pa = p1.ancestor(p2)
409 419 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
410 420 fastforward = False
411 421
412 422 ### check phase
413 423 if not overwrite and len(pl) > 1:
414 424 raise util.Abort(_("outstanding uncommitted merges"))
415 425 if branchmerge:
416 426 if pa == p2:
417 427 raise util.Abort(_("can't merge with ancestor"))
418 428 elif pa == p1:
419 429 if p1.branch() != p2.branch():
420 430 fastforward = True
421 431 else:
422 432 raise util.Abort(_("nothing to merge (use 'hg update'"
423 433 " or check 'hg heads')"))
424 434 if not force and (wc.files() or wc.deleted()):
425 435 raise util.Abort(_("outstanding uncommitted changes "
426 436 "(use 'hg status' to list changes)"))
427 437 elif not overwrite:
428 438 if pa == p1 or pa == p2: # linear
429 439 pass # all good
430 440 elif p1.branch() == p2.branch():
431 441 if wc.files() or wc.deleted():
432 442 raise util.Abort(_("crosses branches (use 'hg merge' or "
433 443 "'hg update -C' to discard changes)"))
434 444 raise util.Abort(_("crosses branches (use 'hg merge' "
435 445 "or 'hg update -C')"))
436 446 elif wc.files() or wc.deleted():
437 447 raise util.Abort(_("crosses named branches (use "
438 448 "'hg update -C' to discard changes)"))
439 449 else:
440 450 # Allow jumping branches if there are no changes
441 451 overwrite = True
442 452
443 453 ### calculate phase
444 454 action = []
445 455 if not force:
446 456 _checkunknown(wc, p2)
447 457 if not util.checkcase(repo.path):
448 458 _checkcollision(p2)
449 459 action += _forgetremoved(wc, p2, branchmerge)
450 460 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
451 461
452 462 ### apply phase
453 463 if not branchmerge: # just jump to the new rev
454 464 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
455 465 if not partial:
456 466 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
457 467
458 468 stats = applyupdates(repo, action, wc, p2)
459 469
460 470 if not partial:
461 471 recordupdates(repo, action, branchmerge)
462 472 repo.dirstate.setparents(fp1, fp2)
463 473 if not branchmerge and not fastforward:
464 474 repo.dirstate.setbranch(p2.branch())
465 475 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
466 476
467 477 return stats
468 478 finally:
469 479 wlock.release()
@@ -1,82 +1,178 b''
1 1 # subrepo.py - sub-repository handling for Mercurial
2 2 #
3 3 # Copyright 2006, 2007 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, incorporated herein by reference.
7 7
8 8 import errno, os
9 from i18n import _
9 10 import config, util, node, error
10 localrepo = None
11 localrepo = hg = None
11 12
12 13 nullstate = ('', '')
13 14
14 15 def state(ctx):
15 16 p = config.config()
16 17 def read(f, sections=None, remap=None):
17 18 if f in ctx:
18 19 try:
19 20 p.parse(f, ctx[f].data(), sections, remap)
20 21 except IOError, err:
21 22 if err.errno != errno.ENOENT:
22 23 raise
23 24 read('.hgsub')
24 25
25 26 rev = {}
26 27 if '.hgsubstate' in ctx:
27 28 try:
28 29 for l in ctx['.hgsubstate'].data().splitlines():
29 30 revision, path = l.split()
30 31 rev[path] = revision
31 32 except IOError, err:
32 33 if err.errno != errno.ENOENT:
33 34 raise
34 35
35 36 state = {}
36 37 for path, src in p[''].items():
37 38 state[path] = (src, rev.get(path, ''))
38 39
39 40 return state
40 41
41 42 def writestate(repo, state):
42 43 repo.wwrite('.hgsubstate',
43 44 ''.join(['%s %s\n' % (state[s][1], s)
44 45 for s in sorted(state)]), '')
45 46
47 def submerge(repo, wctx, mctx, actx):
48 if mctx == actx: # backwards?
49 actx = wctx.p1()
50 s1 = wctx.substate
51 s2 = mctx.substate
52 sa = actx.substate
53 sm = {}
54
55 for s, l in s1.items():
56 a = sa.get(s, nullstate)
57 if s in s2:
58 r = s2[s]
59 if l == r or r == a: # no change or local is newer
60 sm[s] = l
61 continue
62 elif l == a: # other side changed
63 wctx.sub(s).get(r)
64 sm[s] = r
65 elif l[0] != r[0]: # sources differ
66 if repo.ui.prompt(
67 _(' subrepository sources for %s differ\n'
68 'use (l)ocal source (%s) or (r)emote source (%s)?'
69 % (s, l[0], r[0]),
70 (_('&Local'), _('&Remote')), _('l'))) == _('r'):
71 wctx.sub(s).get(r)
72 sm[s] = r
73 elif l[1] == a[1]: # local side is unchanged
74 wctx.sub(s).get(r)
75 sm[s] = r
76 else:
77 wctx.sub(s).merge(r)
78 sm[s] = l
79 elif l == a: # remote removed, local unchanged
80 wctx.sub(s).remove()
81 else:
82 if repo.ui.prompt(
83 _(' local changed subrepository %s which remote removed\n'
84 'use (c)hanged version or (d)elete?' % s,
85 (_('&Changed'), _('&Delete')), _('c'))) == _('d'):
86 wctx.sub(s).remove()
87
88 for s, r in s2.items():
89 if s in s1:
90 continue
91 elif s not in sa:
92 wctx.sub(s).get(r)
93 sm[s] = r
94 elif r != sa[s]:
95 if repo.ui.prompt(
96 _(' remote changed subrepository %s which local removed\n'
97 'use (c)hanged version or (d)elete?' % s,
98 (_('&Changed'), _('&Delete')), _('c'))) == _('c'):
99 wctx.sub(s).get(r)
100 sm[s] = r
101
102 # record merged .hgsubstate
103 writestate(repo, sm)
104
105 def _abssource(repo):
106 if hasattr(repo, '_subparent'):
107 source = repo._subsource
108 if source.startswith('/') or '://' in source:
109 return source
110 return os.path.join(_abssource(repo._subparent), repo._subsource)
111 return repo.ui.config('paths', 'default', repo.root)
112
46 113 def subrepo(ctx, path):
47 114 # subrepo inherently violates our import layering rules
48 115 # because it wants to make repo objects from deep inside the stack
49 116 # so we manually delay the circular imports to not break
50 117 # scripts that don't use our demand-loading
51 global localrepo
52 import localrepo as l
118 global localrepo, hg
119 import localrepo as l, hg as h
53 120 localrepo = l
121 hg = h
54 122
55 123 state = ctx.substate.get(path, nullstate)
56 124 if state[0].startswith('['): # future expansion
57 125 raise error.Abort('unknown subrepo source %s' % state[0])
58 126 return hgsubrepo(ctx, path, state)
59 127
60 128 class hgsubrepo(object):
61 129 def __init__(self, ctx, path, state):
62 130 self._parent = ctx
63 131 self._path = path
64 132 self._state = state
65 133 r = ctx._repo
66 134 root = r.wjoin(path)
67 self._repo = localrepo.localrepository(r.ui, root)
135 if os.path.exists(os.path.join(root, '.hg')):
136 self._repo = localrepo.localrepository(r.ui, root)
137 else:
138 util.makedirs(root)
139 self._repo = localrepo.localrepository(r.ui, root, create=True)
140 self._repo._subparent = r
141 self._repo._subsource = state[0]
68 142
69 143 def dirty(self):
70 144 r = self._state[1]
71 145 if r == '':
72 146 return True
73 147 w = self._repo[None]
74 148 if w.p1() != self._repo[r]: # version checked out changed
75 149 return True
76 150 return w.dirty() # working directory changed
77 151
78 152 def commit(self, text, user, date):
79 153 n = self._repo.commit(text, user, date)
80 154 if not n:
81 155 return self._repo['.'].hex() # different version checked out
82 156 return node.hex(n)
157
158 def remove(self):
159 # we can't fully delete the repository as it may contain
160 # local-only history
161 self._repo.ui.note(_('removing subrepo %s\n') % self._path)
162 hg.clean(self._repo, node.nullid, False)
163
164 def get(self, state):
165 source, revision = state
166 try:
167 self._repo.lookup(revision)
168 except error.RepoError:
169 self._repo._subsource = source
170 self._repo.ui.status(_('pulling subrepo %s\n') % self._path)
171 srcurl = _abssource(self._repo)
172 other = hg.repository(self._repo.ui, srcurl)
173 self._repo.pull(other)
174
175 hg.clean(self._repo, revision, False)
176
177 def merge(self, state):
178 hg.merge(self._repo, state[1], remind=False)
General Comments 0
You need to be logged in to leave comments. Login now