##// END OF EJS Templates
commitctx: directly gather p1 and p2 copies in `files`...
marmoute -
r45886:99614011 default
parent child Browse files
Show More
@@ -1,452 +1,447
1 1 # commit.py - fonction to perform commit
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 from __future__ import absolute_import
7 7
8 8 import errno
9 9
10 10 from .i18n import _
11 11 from .node import (
12 12 hex,
13 13 nullid,
14 14 nullrev,
15 15 )
16 16
17 17 from . import (
18 18 context,
19 19 mergestate,
20 20 metadata,
21 21 phases,
22 22 scmutil,
23 23 subrepoutil,
24 24 )
25 25
26 26
27 27 def _write_copy_meta(repo):
28 28 """return a (changelog, filelog) boolean tuple
29 29
30 30 changelog: copy related information should be stored in the changeset
31 31 filelof: copy related information should be written in the file revision
32 32 """
33 33 if repo.filecopiesmode == b'changeset-sidedata':
34 34 writechangesetcopy = True
35 35 writefilecopymeta = True
36 36 else:
37 37 writecopiesto = repo.ui.config(b'experimental', b'copies.write-to')
38 38 writefilecopymeta = writecopiesto != b'changeset-only'
39 39 writechangesetcopy = writecopiesto in (
40 40 b'changeset-only',
41 41 b'compatibility',
42 42 )
43 43 return writechangesetcopy, writefilecopymeta
44 44
45 45
46 46 def commitctx(repo, ctx, error=False, origctx=None):
47 47 """Add a new revision to the target repository.
48 48 Revision information is passed via the context argument.
49 49
50 50 ctx.files() should list all files involved in this commit, i.e.
51 51 modified/added/removed files. On merge, it may be wider than the
52 52 ctx.files() to be committed, since any file nodes derived directly
53 53 from p1 or p2 are excluded from the committed ctx.files().
54 54
55 55 origctx is for convert to work around the problem that bug
56 56 fixes to the files list in changesets change hashes. For
57 57 convert to be the identity, it can pass an origctx and this
58 58 function will use the same files list when it makes sense to
59 59 do so.
60 60 """
61 61 repo = repo.unfiltered()
62 62
63 63 p1, p2 = ctx.p1(), ctx.p2()
64 64 user = ctx.user()
65 65
66 66 with repo.lock(), repo.transaction(b"commit") as tr:
67 67 mn, files = _prepare_files(tr, ctx, error=error, origctx=origctx)
68 68
69 69 extra = ctx.extra().copy()
70 70
71 71 if extra is not None:
72 72 for name in (
73 73 b'p1copies',
74 74 b'p2copies',
75 75 b'filesadded',
76 76 b'filesremoved',
77 77 ):
78 78 extra.pop(name, None)
79 79 if repo.changelog._copiesstorage == b'extra':
80 80 extra = _extra_with_copies(repo, extra, files)
81 81
82 82 # update changelog
83 83 repo.ui.note(_(b"committing changelog\n"))
84 84 repo.changelog.delayupdate(tr)
85 85 n = repo.changelog.add(
86 86 mn,
87 87 files,
88 88 ctx.description(),
89 89 tr,
90 90 p1.node(),
91 91 p2.node(),
92 92 user,
93 93 ctx.date(),
94 94 extra,
95 95 )
96 96 xp1, xp2 = p1.hex(), p2 and p2.hex() or b''
97 97 repo.hook(
98 98 b'pretxncommit', throw=True, node=hex(n), parent1=xp1, parent2=xp2,
99 99 )
100 100 # set the new commit is proper phase
101 101 targetphase = subrepoutil.newcommitphase(repo.ui, ctx)
102 102 if targetphase:
103 103 # retract boundary do not alter parent changeset.
104 104 # if a parent have higher the resulting phase will
105 105 # be compliant anyway
106 106 #
107 107 # if minimal phase was 0 we don't need to retract anything
108 108 phases.registernew(repo, tr, targetphase, [n])
109 109 return n
110 110
111 111
112 112 def _prepare_files(tr, ctx, error=False, origctx=None):
113 113 repo = ctx.repo()
114 114 p1 = ctx.p1()
115 115
116 116 writechangesetcopy, writefilecopymeta = _write_copy_meta(repo)
117 117
118 p1copies, p2copies = None, None
119 if writechangesetcopy:
120 p1copies = ctx.p1copies()
121 p2copies = ctx.p2copies()
122 118 filesadded, filesremoved = None, None
123 119 if ctx.manifestnode():
124 120 # reuse an existing manifest revision
125 121 repo.ui.debug(b'reusing known manifest\n')
126 122 mn = ctx.manifestnode()
127 123 touched = ctx.files()
128 124 if writechangesetcopy:
129 125 filesadded = ctx.filesadded()
130 126 filesremoved = ctx.filesremoved()
131 127 elif not ctx.files():
132 128 repo.ui.debug(b'reusing manifest from p1 (no file change)\n')
133 129 mn = p1.manifestnode()
134 130 touched = []
135 131 else:
136 132 mn, touched, added, removed = _process_files(tr, ctx, error=error)
137 133 if writechangesetcopy:
138 134 filesremoved = removed
139 135 filesadded = added
140 136
141 137 if origctx and origctx.manifestnode() == mn:
142 138 touched = origctx.files()
143 139
144 140 files = metadata.ChangingFiles()
145 141 if touched:
146 142 files.update_touched(touched)
147 if p1copies:
148 files.update_copies_from_p1(p1copies)
149 if p2copies:
150 files.update_copies_from_p2(p2copies)
143 if writechangesetcopy:
144 files.update_copies_from_p1(ctx.p1copies())
145 files.update_copies_from_p2(ctx.p2copies())
151 146 if filesadded:
152 147 files.update_added(filesadded)
153 148 if filesremoved:
154 149 files.update_removed(filesremoved)
155 150
156 151 return mn, files
157 152
158 153
159 154 def _process_files(tr, ctx, error=False):
160 155 repo = ctx.repo()
161 156 p1 = ctx.p1()
162 157 p2 = ctx.p2()
163 158
164 159 writechangesetcopy, writefilecopymeta = _write_copy_meta(repo)
165 160
166 161 m1ctx = p1.manifestctx()
167 162 m2ctx = p2.manifestctx()
168 163 mctx = m1ctx.copy()
169 164
170 165 m = mctx.read()
171 166 m1 = m1ctx.read()
172 167 m2 = m2ctx.read()
173 168
174 169 # check in files
175 170 added = []
176 171 filesadded = []
177 172 removed = list(ctx.removed())
178 173 touched = []
179 174 linkrev = len(repo)
180 175 repo.ui.note(_(b"committing files:\n"))
181 176 uipathfn = scmutil.getuipathfn(repo)
182 177 for f in sorted(ctx.modified() + ctx.added()):
183 178 repo.ui.note(uipathfn(f) + b"\n")
184 179 try:
185 180 fctx = ctx[f]
186 181 if fctx is None:
187 182 removed.append(f)
188 183 else:
189 184 added.append(f)
190 185 m[f], is_touched = _filecommit(
191 186 repo, fctx, m1, m2, linkrev, tr, writefilecopymeta,
192 187 )
193 188 if is_touched:
194 189 touched.append(f)
195 190 if is_touched == 'added':
196 191 filesadded.append(f)
197 192 m.setflag(f, fctx.flags())
198 193 except OSError:
199 194 repo.ui.warn(_(b"trouble committing %s!\n") % uipathfn(f))
200 195 raise
201 196 except IOError as inst:
202 197 errcode = getattr(inst, 'errno', errno.ENOENT)
203 198 if error or errcode and errcode != errno.ENOENT:
204 199 repo.ui.warn(_(b"trouble committing %s!\n") % uipathfn(f))
205 200 raise
206 201
207 202 # update manifest
208 203 removed = [f for f in removed if f in m1 or f in m2]
209 204 drop = sorted([f for f in removed if f in m])
210 205 for f in drop:
211 206 del m[f]
212 207 if p2.rev() != nullrev:
213 208 rf = metadata.get_removal_filter(ctx, (p1, p2, m1, m2))
214 209 removed = [f for f in removed if not rf(f)]
215 210
216 211 touched.extend(removed)
217 212
218 213 files = touched
219 214 mn = _commit_manifest(tr, linkrev, ctx, mctx, m, files, added, drop)
220 215
221 216 return mn, files, filesadded, removed
222 217
223 218
224 219 def _filecommit(
225 220 repo, fctx, manifest1, manifest2, linkrev, tr, includecopymeta,
226 221 ):
227 222 """
228 223 commit an individual file as part of a larger transaction
229 224
230 225 input:
231 226
232 227 fctx: a file context with the content we are trying to commit
233 228 manifest1: manifest of changeset first parent
234 229 manifest2: manifest of changeset second parent
235 230 linkrev: revision number of the changeset being created
236 231 tr: current transation
237 232 individual: boolean, set to False to skip storing the copy data
238 233 (only used by the Google specific feature of using
239 234 changeset extra as copy source of truth).
240 235
241 236 output: (filenode, touched)
242 237
243 238 filenode: the filenode that should be used by this changeset
244 239 touched: one of: None (mean untouched), 'added' or 'modified'
245 240 """
246 241
247 242 fname = fctx.path()
248 243 fparent1 = manifest1.get(fname, nullid)
249 244 fparent2 = manifest2.get(fname, nullid)
250 245 touched = None
251 246 if fparent1 == fparent2 == nullid:
252 247 touched = 'added'
253 248
254 249 if isinstance(fctx, context.filectx):
255 250 # This block fast path most comparisons which are usually done. It
256 251 # assumes that bare filectx is used and no merge happened, hence no
257 252 # need to create a new file revision in this case.
258 253 node = fctx.filenode()
259 254 if node in [fparent1, fparent2]:
260 255 repo.ui.debug(b'reusing %s filelog entry\n' % fname)
261 256 if (
262 257 fparent1 != nullid and manifest1.flags(fname) != fctx.flags()
263 258 ) or (
264 259 fparent2 != nullid and manifest2.flags(fname) != fctx.flags()
265 260 ):
266 261 touched = 'modified'
267 262 return node, touched
268 263
269 264 flog = repo.file(fname)
270 265 meta = {}
271 266 cfname = fctx.copysource()
272 267 fnode = None
273 268
274 269 if cfname and cfname != fname:
275 270 # Mark the new revision of this file as a copy of another
276 271 # file. This copy data will effectively act as a parent
277 272 # of this new revision. If this is a merge, the first
278 273 # parent will be the nullid (meaning "look up the copy data")
279 274 # and the second one will be the other parent. For example:
280 275 #
281 276 # 0 --- 1 --- 3 rev1 changes file foo
282 277 # \ / rev2 renames foo to bar and changes it
283 278 # \- 2 -/ rev3 should have bar with all changes and
284 279 # should record that bar descends from
285 280 # bar in rev2 and foo in rev1
286 281 #
287 282 # this allows this merge to succeed:
288 283 #
289 284 # 0 --- 1 --- 3 rev4 reverts the content change from rev2
290 285 # \ / merging rev3 and rev4 should use bar@rev2
291 286 # \- 2 --- 4 as the merge base
292 287 #
293 288
294 289 cnode = manifest1.get(cfname)
295 290 newfparent = fparent2
296 291
297 292 if manifest2: # branch merge
298 293 if fparent2 == nullid or cnode is None: # copied on remote side
299 294 if cfname in manifest2:
300 295 cnode = manifest2[cfname]
301 296 newfparent = fparent1
302 297
303 298 # Here, we used to search backwards through history to try to find
304 299 # where the file copy came from if the source of a copy was not in
305 300 # the parent directory. However, this doesn't actually make sense to
306 301 # do (what does a copy from something not in your working copy even
307 302 # mean?) and it causes bugs (eg, issue4476). Instead, we will warn
308 303 # the user that copy information was dropped, so if they didn't
309 304 # expect this outcome it can be fixed, but this is the correct
310 305 # behavior in this circumstance.
311 306
312 307 if cnode:
313 308 repo.ui.debug(b" %s: copy %s:%s\n" % (fname, cfname, hex(cnode)))
314 309 if includecopymeta:
315 310 meta[b"copy"] = cfname
316 311 meta[b"copyrev"] = hex(cnode)
317 312 fparent1, fparent2 = nullid, newfparent
318 313 else:
319 314 repo.ui.warn(
320 315 _(
321 316 b"warning: can't find ancestor for '%s' "
322 317 b"copied from '%s'!\n"
323 318 )
324 319 % (fname, cfname)
325 320 )
326 321
327 322 elif fparent1 == nullid:
328 323 fparent1, fparent2 = fparent2, nullid
329 324 elif fparent2 != nullid:
330 325 # is one parent an ancestor of the other?
331 326 fparentancestors = flog.commonancestorsheads(fparent1, fparent2)
332 327 if fparent1 in fparentancestors:
333 328 fparent1, fparent2 = fparent2, nullid
334 329 elif fparent2 in fparentancestors:
335 330 fparent2 = nullid
336 331 elif not fparentancestors:
337 332 # TODO: this whole if-else might be simplified much more
338 333 ms = mergestate.mergestate.read(repo)
339 334 if (
340 335 fname in ms
341 336 and ms[fname] == mergestate.MERGE_RECORD_MERGED_OTHER
342 337 ):
343 338 fparent1, fparent2 = fparent2, nullid
344 339
345 340 # is the file changed?
346 341 text = fctx.data()
347 342 if fparent2 != nullid or meta or flog.cmp(fparent1, text):
348 343 if touched is None: # do not overwrite added
349 344 touched = 'modified'
350 345 fnode = flog.add(text, meta, tr, linkrev, fparent1, fparent2)
351 346 # are just the flags changed during merge?
352 347 elif fname in manifest1 and manifest1.flags(fname) != fctx.flags():
353 348 touched = 'modified'
354 349 fnode = fparent1
355 350 else:
356 351 fnode = fparent1
357 352 return fnode, touched
358 353
359 354
360 355 def _commit_manifest(tr, linkrev, ctx, mctx, manifest, files, added, drop):
361 356 """make a new manifest entry (or reuse a new one)
362 357
363 358 given an initialised manifest context and precomputed list of
364 359 - files: files affected by the commit
365 360 - added: new entries in the manifest
366 361 - drop: entries present in parents but absent of this one
367 362
368 363 Create a new manifest revision, reuse existing ones if possible.
369 364
370 365 Return the nodeid of the manifest revision.
371 366 """
372 367 repo = ctx.repo()
373 368
374 369 md = None
375 370
376 371 # all this is cached, so it is find to get them all from the ctx.
377 372 p1 = ctx.p1()
378 373 p2 = ctx.p2()
379 374 m1ctx = p1.manifestctx()
380 375
381 376 m1 = m1ctx.read()
382 377
383 378 if not files:
384 379 # if no "files" actually changed in terms of the changelog,
385 380 # try hard to detect unmodified manifest entry so that the
386 381 # exact same commit can be reproduced later on convert.
387 382 md = m1.diff(manifest, scmutil.matchfiles(repo, ctx.files()))
388 383 if not files and md:
389 384 repo.ui.debug(
390 385 b'not reusing manifest (no file change in '
391 386 b'changelog, but manifest differs)\n'
392 387 )
393 388 if files or md:
394 389 repo.ui.note(_(b"committing manifest\n"))
395 390 # we're using narrowmatch here since it's already applied at
396 391 # other stages (such as dirstate.walk), so we're already
397 392 # ignoring things outside of narrowspec in most cases. The
398 393 # one case where we might have files outside the narrowspec
399 394 # at this point is merges, and we already error out in the
400 395 # case where the merge has files outside of the narrowspec,
401 396 # so this is safe.
402 397 mn = mctx.write(
403 398 tr,
404 399 linkrev,
405 400 p1.manifestnode(),
406 401 p2.manifestnode(),
407 402 added,
408 403 drop,
409 404 match=repo.narrowmatch(),
410 405 )
411 406 else:
412 407 repo.ui.debug(
413 408 b'reusing manifest from p1 (listed files ' b'actually unchanged)\n'
414 409 )
415 410 mn = p1.manifestnode()
416 411
417 412 return mn
418 413
419 414
420 415 def _extra_with_copies(repo, extra, files):
421 416 """encode copy information into a `extra` dictionnary"""
422 417 p1copies = files.copied_from_p1
423 418 p2copies = files.copied_from_p2
424 419 filesadded = files.added
425 420 filesremoved = files.removed
426 421 files = sorted(files.touched)
427 422 if not _write_copy_meta(repo)[1]:
428 423 # If writing only to changeset extras, use None to indicate that
429 424 # no entry should be written. If writing to both, write an empty
430 425 # entry to prevent the reader from falling back to reading
431 426 # filelogs.
432 427 p1copies = p1copies or None
433 428 p2copies = p2copies or None
434 429 filesadded = filesadded or None
435 430 filesremoved = filesremoved or None
436 431
437 432 extrasentries = p1copies, p2copies, filesadded, filesremoved
438 433 if extra is None and any(x is not None for x in extrasentries):
439 434 extra = {}
440 435 if p1copies is not None:
441 436 p1copies = metadata.encodecopies(files, p1copies)
442 437 extra[b'p1copies'] = p1copies
443 438 if p2copies is not None:
444 439 p2copies = metadata.encodecopies(files, p2copies)
445 440 extra[b'p2copies'] = p2copies
446 441 if filesadded is not None:
447 442 filesadded = metadata.encodefileindices(files, filesadded)
448 443 extra[b'filesadded'] = filesadded
449 444 if filesremoved is not None:
450 445 filesremoved = metadata.encodefileindices(files, filesremoved)
451 446 extra[b'filesremoved'] = filesremoved
452 447 return extra
General Comments 0
You need to be logged in to leave comments. Login now