Show More
@@ -1,395 +1,396 b'' | |||
|
1 | 1 | # subrepoutil.py - sub-repository operations and substate handling |
|
2 | 2 | # |
|
3 | 3 | # Copyright 2009-2010 Matt Mackall <mpm@selenic.com> |
|
4 | 4 | # |
|
5 | 5 | # This software may be used and distributed according to the terms of the |
|
6 | 6 | # GNU General Public License version 2 or any later version. |
|
7 | 7 | |
|
8 | 8 | from __future__ import absolute_import |
|
9 | 9 | |
|
10 | 10 | import errno |
|
11 | 11 | import os |
|
12 | 12 | import posixpath |
|
13 | 13 | import re |
|
14 | 14 | |
|
15 | 15 | from .i18n import _ |
|
16 | 16 | from . import ( |
|
17 | 17 | config, |
|
18 | 18 | error, |
|
19 | 19 | filemerge, |
|
20 | 20 | pathutil, |
|
21 | 21 | phases, |
|
22 | 22 | util, |
|
23 | 23 | ) |
|
24 | 24 | from .utils import ( |
|
25 | 25 | stringutil, |
|
26 | 26 | ) |
|
27 | 27 | |
|
28 | 28 | nullstate = ('', '', 'empty') |
|
29 | 29 | |
|
30 | 30 | def state(ctx, ui): |
|
31 | 31 | """return a state dict, mapping subrepo paths configured in .hgsub |
|
32 | 32 | to tuple: (source from .hgsub, revision from .hgsubstate, kind |
|
33 | 33 | (key in types dict)) |
|
34 | 34 | """ |
|
35 | 35 | p = config.config() |
|
36 | 36 | repo = ctx.repo() |
|
37 | 37 | def read(f, sections=None, remap=None): |
|
38 | 38 | if f in ctx: |
|
39 | 39 | try: |
|
40 | 40 | data = ctx[f].data() |
|
41 | 41 | except IOError as err: |
|
42 | 42 | if err.errno != errno.ENOENT: |
|
43 | 43 | raise |
|
44 | 44 | # handle missing subrepo spec files as removed |
|
45 | 45 | ui.warn(_("warning: subrepo spec file \'%s\' not found\n") % |
|
46 | 46 | repo.pathto(f)) |
|
47 | 47 | return |
|
48 | 48 | p.parse(f, data, sections, remap, read) |
|
49 | 49 | else: |
|
50 | 50 | raise error.Abort(_("subrepo spec file \'%s\' not found") % |
|
51 | 51 | repo.pathto(f)) |
|
52 | 52 | if '.hgsub' in ctx: |
|
53 | 53 | read('.hgsub') |
|
54 | 54 | |
|
55 | 55 | for path, src in ui.configitems('subpaths'): |
|
56 | 56 | p.set('subpaths', path, src, ui.configsource('subpaths', path)) |
|
57 | 57 | |
|
58 | 58 | rev = {} |
|
59 | 59 | if '.hgsubstate' in ctx: |
|
60 | 60 | try: |
|
61 | 61 | for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()): |
|
62 | 62 | l = l.lstrip() |
|
63 | 63 | if not l: |
|
64 | 64 | continue |
|
65 | 65 | try: |
|
66 | 66 | revision, path = l.split(" ", 1) |
|
67 | 67 | except ValueError: |
|
68 | 68 | raise error.Abort(_("invalid subrepository revision " |
|
69 | 69 | "specifier in \'%s\' line %d") |
|
70 | 70 | % (repo.pathto('.hgsubstate'), (i + 1))) |
|
71 | 71 | rev[path] = revision |
|
72 | 72 | except IOError as err: |
|
73 | 73 | if err.errno != errno.ENOENT: |
|
74 | 74 | raise |
|
75 | 75 | |
|
76 | 76 | def remap(src): |
|
77 | 77 | for pattern, repl in p.items('subpaths'): |
|
78 | 78 | # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub |
|
79 | 79 | # does a string decode. |
|
80 | 80 | repl = stringutil.escapestr(repl) |
|
81 | 81 | # However, we still want to allow back references to go |
|
82 | 82 | # through unharmed, so we turn r'\\1' into r'\1'. Again, |
|
83 | 83 | # extra escapes are needed because re.sub string decodes. |
|
84 | 84 | repl = re.sub(br'\\\\([0-9]+)', br'\\\1', repl) |
|
85 | 85 | try: |
|
86 | 86 | src = re.sub(pattern, repl, src, 1) |
|
87 | 87 | except re.error as e: |
|
88 | 88 | raise error.Abort(_("bad subrepository pattern in %s: %s") |
|
89 |
% (p.source('subpaths', pattern), |
|
|
89 | % (p.source('subpaths', pattern), | |
|
90 | stringutil.forcebytestr(e))) | |
|
90 | 91 | return src |
|
91 | 92 | |
|
92 | 93 | state = {} |
|
93 | 94 | for path, src in p[''].items(): |
|
94 | 95 | kind = 'hg' |
|
95 | 96 | if src.startswith('['): |
|
96 | 97 | if ']' not in src: |
|
97 | 98 | raise error.Abort(_('missing ] in subrepository source')) |
|
98 | 99 | kind, src = src.split(']', 1) |
|
99 | 100 | kind = kind[1:] |
|
100 | 101 | src = src.lstrip() # strip any extra whitespace after ']' |
|
101 | 102 | |
|
102 | 103 | if not util.url(src).isabs(): |
|
103 | 104 | parent = _abssource(repo, abort=False) |
|
104 | 105 | if parent: |
|
105 | 106 | parent = util.url(parent) |
|
106 | 107 | parent.path = posixpath.join(parent.path or '', src) |
|
107 | 108 | parent.path = posixpath.normpath(parent.path) |
|
108 | 109 | joined = str(parent) |
|
109 | 110 | # Remap the full joined path and use it if it changes, |
|
110 | 111 | # else remap the original source. |
|
111 | 112 | remapped = remap(joined) |
|
112 | 113 | if remapped == joined: |
|
113 | 114 | src = remap(src) |
|
114 | 115 | else: |
|
115 | 116 | src = remapped |
|
116 | 117 | |
|
117 | 118 | src = remap(src) |
|
118 | 119 | state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind) |
|
119 | 120 | |
|
120 | 121 | return state |
|
121 | 122 | |
|
122 | 123 | def writestate(repo, state): |
|
123 | 124 | """rewrite .hgsubstate in (outer) repo with these subrepo states""" |
|
124 | 125 | lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state) |
|
125 | 126 | if state[s][1] != nullstate[1]] |
|
126 | 127 | repo.wwrite('.hgsubstate', ''.join(lines), '') |
|
127 | 128 | |
|
128 | 129 | def submerge(repo, wctx, mctx, actx, overwrite, labels=None): |
|
129 | 130 | """delegated from merge.applyupdates: merging of .hgsubstate file |
|
130 | 131 | in working context, merging context and ancestor context""" |
|
131 | 132 | if mctx == actx: # backwards? |
|
132 | 133 | actx = wctx.p1() |
|
133 | 134 | s1 = wctx.substate |
|
134 | 135 | s2 = mctx.substate |
|
135 | 136 | sa = actx.substate |
|
136 | 137 | sm = {} |
|
137 | 138 | |
|
138 | 139 | repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx)) |
|
139 | 140 | |
|
140 | 141 | def debug(s, msg, r=""): |
|
141 | 142 | if r: |
|
142 | 143 | r = "%s:%s:%s" % r |
|
143 | 144 | repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r)) |
|
144 | 145 | |
|
145 | 146 | promptssrc = filemerge.partextras(labels) |
|
146 | 147 | for s, l in sorted(s1.iteritems()): |
|
147 | 148 | prompts = None |
|
148 | 149 | a = sa.get(s, nullstate) |
|
149 | 150 | ld = l # local state with possible dirty flag for compares |
|
150 | 151 | if wctx.sub(s).dirty(): |
|
151 | 152 | ld = (l[0], l[1] + "+") |
|
152 | 153 | if wctx == actx: # overwrite |
|
153 | 154 | a = ld |
|
154 | 155 | |
|
155 | 156 | prompts = promptssrc.copy() |
|
156 | 157 | prompts['s'] = s |
|
157 | 158 | if s in s2: |
|
158 | 159 | r = s2[s] |
|
159 | 160 | if ld == r or r == a: # no change or local is newer |
|
160 | 161 | sm[s] = l |
|
161 | 162 | continue |
|
162 | 163 | elif ld == a: # other side changed |
|
163 | 164 | debug(s, "other changed, get", r) |
|
164 | 165 | wctx.sub(s).get(r, overwrite) |
|
165 | 166 | sm[s] = r |
|
166 | 167 | elif ld[0] != r[0]: # sources differ |
|
167 | 168 | prompts['lo'] = l[0] |
|
168 | 169 | prompts['ro'] = r[0] |
|
169 | 170 | if repo.ui.promptchoice( |
|
170 | 171 | _(' subrepository sources for %(s)s differ\n' |
|
171 | 172 | 'use (l)ocal%(l)s source (%(lo)s)' |
|
172 | 173 | ' or (r)emote%(o)s source (%(ro)s)?' |
|
173 | 174 | '$$ &Local $$ &Remote') % prompts, 0): |
|
174 | 175 | debug(s, "prompt changed, get", r) |
|
175 | 176 | wctx.sub(s).get(r, overwrite) |
|
176 | 177 | sm[s] = r |
|
177 | 178 | elif ld[1] == a[1]: # local side is unchanged |
|
178 | 179 | debug(s, "other side changed, get", r) |
|
179 | 180 | wctx.sub(s).get(r, overwrite) |
|
180 | 181 | sm[s] = r |
|
181 | 182 | else: |
|
182 | 183 | debug(s, "both sides changed") |
|
183 | 184 | srepo = wctx.sub(s) |
|
184 | 185 | prompts['sl'] = srepo.shortid(l[1]) |
|
185 | 186 | prompts['sr'] = srepo.shortid(r[1]) |
|
186 | 187 | option = repo.ui.promptchoice( |
|
187 | 188 | _(' subrepository %(s)s diverged (local revision: %(sl)s, ' |
|
188 | 189 | 'remote revision: %(sr)s)\n' |
|
189 | 190 | '(M)erge, keep (l)ocal%(l)s or keep (r)emote%(o)s?' |
|
190 | 191 | '$$ &Merge $$ &Local $$ &Remote') |
|
191 | 192 | % prompts, 0) |
|
192 | 193 | if option == 0: |
|
193 | 194 | wctx.sub(s).merge(r) |
|
194 | 195 | sm[s] = l |
|
195 | 196 | debug(s, "merge with", r) |
|
196 | 197 | elif option == 1: |
|
197 | 198 | sm[s] = l |
|
198 | 199 | debug(s, "keep local subrepo revision", l) |
|
199 | 200 | else: |
|
200 | 201 | wctx.sub(s).get(r, overwrite) |
|
201 | 202 | sm[s] = r |
|
202 | 203 | debug(s, "get remote subrepo revision", r) |
|
203 | 204 | elif ld == a: # remote removed, local unchanged |
|
204 | 205 | debug(s, "remote removed, remove") |
|
205 | 206 | wctx.sub(s).remove() |
|
206 | 207 | elif a == nullstate: # not present in remote or ancestor |
|
207 | 208 | debug(s, "local added, keep") |
|
208 | 209 | sm[s] = l |
|
209 | 210 | continue |
|
210 | 211 | else: |
|
211 | 212 | if repo.ui.promptchoice( |
|
212 | 213 | _(' local%(l)s changed subrepository %(s)s' |
|
213 | 214 | ' which remote%(o)s removed\n' |
|
214 | 215 | 'use (c)hanged version or (d)elete?' |
|
215 | 216 | '$$ &Changed $$ &Delete') % prompts, 0): |
|
216 | 217 | debug(s, "prompt remove") |
|
217 | 218 | wctx.sub(s).remove() |
|
218 | 219 | |
|
219 | 220 | for s, r in sorted(s2.items()): |
|
220 | 221 | prompts = None |
|
221 | 222 | if s in s1: |
|
222 | 223 | continue |
|
223 | 224 | elif s not in sa: |
|
224 | 225 | debug(s, "remote added, get", r) |
|
225 | 226 | mctx.sub(s).get(r) |
|
226 | 227 | sm[s] = r |
|
227 | 228 | elif r != sa[s]: |
|
228 | 229 | prompts = promptssrc.copy() |
|
229 | 230 | prompts['s'] = s |
|
230 | 231 | if repo.ui.promptchoice( |
|
231 | 232 | _(' remote%(o)s changed subrepository %(s)s' |
|
232 | 233 | ' which local%(l)s removed\n' |
|
233 | 234 | 'use (c)hanged version or (d)elete?' |
|
234 | 235 | '$$ &Changed $$ &Delete') % prompts, 0) == 0: |
|
235 | 236 | debug(s, "prompt recreate", r) |
|
236 | 237 | mctx.sub(s).get(r) |
|
237 | 238 | sm[s] = r |
|
238 | 239 | |
|
239 | 240 | # record merged .hgsubstate |
|
240 | 241 | writestate(repo, sm) |
|
241 | 242 | return sm |
|
242 | 243 | |
|
243 | 244 | def precommit(ui, wctx, status, match, force=False): |
|
244 | 245 | """Calculate .hgsubstate changes that should be applied before committing |
|
245 | 246 | |
|
246 | 247 | Returns (subs, commitsubs, newstate) where |
|
247 | 248 | - subs: changed subrepos (including dirty ones) |
|
248 | 249 | - commitsubs: dirty subrepos which the caller needs to commit recursively |
|
249 | 250 | - newstate: new state dict which the caller must write to .hgsubstate |
|
250 | 251 | |
|
251 | 252 | This also updates the given status argument. |
|
252 | 253 | """ |
|
253 | 254 | subs = [] |
|
254 | 255 | commitsubs = set() |
|
255 | 256 | newstate = wctx.substate.copy() |
|
256 | 257 | |
|
257 | 258 | # only manage subrepos and .hgsubstate if .hgsub is present |
|
258 | 259 | if '.hgsub' in wctx: |
|
259 | 260 | # we'll decide whether to track this ourselves, thanks |
|
260 | 261 | for c in status.modified, status.added, status.removed: |
|
261 | 262 | if '.hgsubstate' in c: |
|
262 | 263 | c.remove('.hgsubstate') |
|
263 | 264 | |
|
264 | 265 | # compare current state to last committed state |
|
265 | 266 | # build new substate based on last committed state |
|
266 | 267 | oldstate = wctx.p1().substate |
|
267 | 268 | for s in sorted(newstate.keys()): |
|
268 | 269 | if not match(s): |
|
269 | 270 | # ignore working copy, use old state if present |
|
270 | 271 | if s in oldstate: |
|
271 | 272 | newstate[s] = oldstate[s] |
|
272 | 273 | continue |
|
273 | 274 | if not force: |
|
274 | 275 | raise error.Abort( |
|
275 | 276 | _("commit with new subrepo %s excluded") % s) |
|
276 | 277 | dirtyreason = wctx.sub(s).dirtyreason(True) |
|
277 | 278 | if dirtyreason: |
|
278 | 279 | if not ui.configbool('ui', 'commitsubrepos'): |
|
279 | 280 | raise error.Abort(dirtyreason, |
|
280 | 281 | hint=_("use --subrepos for recursive commit")) |
|
281 | 282 | subs.append(s) |
|
282 | 283 | commitsubs.add(s) |
|
283 | 284 | else: |
|
284 | 285 | bs = wctx.sub(s).basestate() |
|
285 | 286 | newstate[s] = (newstate[s][0], bs, newstate[s][2]) |
|
286 | 287 | if oldstate.get(s, (None, None, None))[1] != bs: |
|
287 | 288 | subs.append(s) |
|
288 | 289 | |
|
289 | 290 | # check for removed subrepos |
|
290 | 291 | for p in wctx.parents(): |
|
291 | 292 | r = [s for s in p.substate if s not in newstate] |
|
292 | 293 | subs += [s for s in r if match(s)] |
|
293 | 294 | if subs: |
|
294 | 295 | if (not match('.hgsub') and |
|
295 | 296 | '.hgsub' in (wctx.modified() + wctx.added())): |
|
296 | 297 | raise error.Abort(_("can't commit subrepos without .hgsub")) |
|
297 | 298 | status.modified.insert(0, '.hgsubstate') |
|
298 | 299 | |
|
299 | 300 | elif '.hgsub' in status.removed: |
|
300 | 301 | # clean up .hgsubstate when .hgsub is removed |
|
301 | 302 | if ('.hgsubstate' in wctx and |
|
302 | 303 | '.hgsubstate' not in (status.modified + status.added + |
|
303 | 304 | status.removed)): |
|
304 | 305 | status.removed.insert(0, '.hgsubstate') |
|
305 | 306 | |
|
306 | 307 | return subs, commitsubs, newstate |
|
307 | 308 | |
|
308 | 309 | def reporelpath(repo): |
|
309 | 310 | """return path to this (sub)repo as seen from outermost repo""" |
|
310 | 311 | parent = repo |
|
311 | 312 | while util.safehasattr(parent, '_subparent'): |
|
312 | 313 | parent = parent._subparent |
|
313 | 314 | return repo.root[len(pathutil.normasprefix(parent.root)):] |
|
314 | 315 | |
|
315 | 316 | def subrelpath(sub): |
|
316 | 317 | """return path to this subrepo as seen from outermost repo""" |
|
317 | 318 | return sub._relpath |
|
318 | 319 | |
|
319 | 320 | def _abssource(repo, push=False, abort=True): |
|
320 | 321 | """return pull/push path of repo - either based on parent repo .hgsub info |
|
321 | 322 | or on the top repo config. Abort or return None if no source found.""" |
|
322 | 323 | if util.safehasattr(repo, '_subparent'): |
|
323 | 324 | source = util.url(repo._subsource) |
|
324 | 325 | if source.isabs(): |
|
325 | 326 | return bytes(source) |
|
326 | 327 | source.path = posixpath.normpath(source.path) |
|
327 | 328 | parent = _abssource(repo._subparent, push, abort=False) |
|
328 | 329 | if parent: |
|
329 | 330 | parent = util.url(util.pconvert(parent)) |
|
330 | 331 | parent.path = posixpath.join(parent.path or '', source.path) |
|
331 | 332 | parent.path = posixpath.normpath(parent.path) |
|
332 | 333 | return bytes(parent) |
|
333 | 334 | else: # recursion reached top repo |
|
334 | 335 | path = None |
|
335 | 336 | if util.safehasattr(repo, '_subtoppath'): |
|
336 | 337 | path = repo._subtoppath |
|
337 | 338 | elif push and repo.ui.config('paths', 'default-push'): |
|
338 | 339 | path = repo.ui.config('paths', 'default-push') |
|
339 | 340 | elif repo.ui.config('paths', 'default'): |
|
340 | 341 | path = repo.ui.config('paths', 'default') |
|
341 | 342 | elif repo.shared(): |
|
342 | 343 | # chop off the .hg component to get the default path form. This has |
|
343 | 344 | # already run through vfsmod.vfs(..., realpath=True), so it doesn't |
|
344 | 345 | # have problems with 'C:' |
|
345 | 346 | return os.path.dirname(repo.sharedpath) |
|
346 | 347 | if path: |
|
347 | 348 | # issue5770: 'C:\' and 'C:' are not equivalent paths. The former is |
|
348 | 349 | # as expected: an absolute path to the root of the C: drive. The |
|
349 | 350 | # latter is a relative path, and works like so: |
|
350 | 351 | # |
|
351 | 352 | # C:\>cd C:\some\path |
|
352 | 353 | # C:\>D: |
|
353 | 354 | # D:\>python -c "import os; print os.path.abspath('C:')" |
|
354 | 355 | # C:\some\path |
|
355 | 356 | # |
|
356 | 357 | # D:\>python -c "import os; print os.path.abspath('C:relative')" |
|
357 | 358 | # C:\some\path\relative |
|
358 | 359 | if util.hasdriveletter(path): |
|
359 | 360 | if len(path) == 2 or path[2:3] not in br'\/': |
|
360 | 361 | path = os.path.abspath(path) |
|
361 | 362 | return path |
|
362 | 363 | |
|
363 | 364 | if abort: |
|
364 | 365 | raise error.Abort(_("default path for subrepository not found")) |
|
365 | 366 | |
|
366 | 367 | def newcommitphase(ui, ctx): |
|
367 | 368 | commitphase = phases.newcommitphase(ui) |
|
368 | 369 | substate = getattr(ctx, "substate", None) |
|
369 | 370 | if not substate: |
|
370 | 371 | return commitphase |
|
371 | 372 | check = ui.config('phases', 'checksubrepos') |
|
372 | 373 | if check not in ('ignore', 'follow', 'abort'): |
|
373 | 374 | raise error.Abort(_('invalid phases.checksubrepos configuration: %s') |
|
374 | 375 | % (check)) |
|
375 | 376 | if check == 'ignore': |
|
376 | 377 | return commitphase |
|
377 | 378 | maxphase = phases.public |
|
378 | 379 | maxsub = None |
|
379 | 380 | for s in sorted(substate): |
|
380 | 381 | sub = ctx.sub(s) |
|
381 | 382 | subphase = sub.phase(substate[s][1]) |
|
382 | 383 | if maxphase < subphase: |
|
383 | 384 | maxphase = subphase |
|
384 | 385 | maxsub = s |
|
385 | 386 | if commitphase < maxphase: |
|
386 | 387 | if check == 'abort': |
|
387 | 388 | raise error.Abort(_("can't commit in %s phase" |
|
388 | 389 | " conflicting %s from subrepository %s") % |
|
389 | 390 | (phases.phasenames[commitphase], |
|
390 | 391 | phases.phasenames[maxphase], maxsub)) |
|
391 | 392 | ui.warn(_("warning: changes are committed in" |
|
392 | 393 | " %s phase from subrepository %s\n") % |
|
393 | 394 | (phases.phasenames[maxphase], maxsub)) |
|
394 | 395 | return maxphase |
|
395 | 396 | return commitphase |
General Comments 0
You need to be logged in to leave comments.
Login now