##// END OF EJS Templates
commitctx: gather more preparation code within the lock context...
marmoute -
r45796:bd751527 default
parent child Browse files
Show More
@@ -1,399 +1,399
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 with repo.lock(), repo.transaction(b"commit") as tr:
66 67 writechangesetcopy, writefilecopymeta = _write_copy_meta(repo)
67 68
68 69 p1copies, p2copies = None, None
69 70 if writechangesetcopy:
70 71 p1copies = ctx.p1copies()
71 72 p2copies = ctx.p2copies()
72 73 filesadded, filesremoved = None, None
73 with repo.lock(), repo.transaction(b"commit") as tr:
74 74 if ctx.manifestnode():
75 75 # reuse an existing manifest revision
76 76 repo.ui.debug(b'reusing known manifest\n')
77 77 mn = ctx.manifestnode()
78 78 files = ctx.files()
79 79 if writechangesetcopy:
80 80 filesadded = ctx.filesadded()
81 81 filesremoved = ctx.filesremoved()
82 82 elif not ctx.files():
83 83 repo.ui.debug(b'reusing manifest from p1 (no file change)\n')
84 84 mn = p1.manifestnode()
85 85 files = []
86 86 else:
87 87 mn, files, added, removed = _process_files(tr, ctx, error=error)
88 88 if writechangesetcopy:
89 89 filesremoved = removed
90 90 filesadded = added
91 91
92 92 if origctx and origctx.manifestnode() == mn:
93 93 files = origctx.files()
94 94
95 95 if not writefilecopymeta:
96 96 # If writing only to changeset extras, use None to indicate that
97 97 # no entry should be written. If writing to both, write an empty
98 98 # entry to prevent the reader from falling back to reading
99 99 # filelogs.
100 100 p1copies = p1copies or None
101 101 p2copies = p2copies or None
102 102 filesadded = filesadded or None
103 103 filesremoved = filesremoved or None
104 104
105 105 # update changelog
106 106 repo.ui.note(_(b"committing changelog\n"))
107 107 repo.changelog.delayupdate(tr)
108 108 n = repo.changelog.add(
109 109 mn,
110 110 files,
111 111 ctx.description(),
112 112 tr,
113 113 p1.node(),
114 114 p2.node(),
115 115 user,
116 116 ctx.date(),
117 117 ctx.extra().copy(),
118 118 p1copies,
119 119 p2copies,
120 120 filesadded,
121 121 filesremoved,
122 122 )
123 123 xp1, xp2 = p1.hex(), p2 and p2.hex() or b''
124 124 repo.hook(
125 125 b'pretxncommit', throw=True, node=hex(n), parent1=xp1, parent2=xp2,
126 126 )
127 127 # set the new commit is proper phase
128 128 targetphase = subrepoutil.newcommitphase(repo.ui, ctx)
129 129 if targetphase:
130 130 # retract boundary do not alter parent changeset.
131 131 # if a parent have higher the resulting phase will
132 132 # be compliant anyway
133 133 #
134 134 # if minimal phase was 0 we don't need to retract anything
135 135 phases.registernew(repo, tr, targetphase, [n])
136 136 return n
137 137
138 138
139 139 def _process_files(tr, ctx, error=False):
140 140 repo = ctx.repo()
141 141 p1 = ctx.p1()
142 142 p2 = ctx.p2()
143 143
144 144 writechangesetcopy, writefilecopymeta = _write_copy_meta(repo)
145 145
146 146 m1ctx = p1.manifestctx()
147 147 m2ctx = p2.manifestctx()
148 148 mctx = m1ctx.copy()
149 149
150 150 m = mctx.read()
151 151 m1 = m1ctx.read()
152 152 m2 = m2ctx.read()
153 153
154 154 # check in files
155 155 added = []
156 156 filesadded = []
157 157 removed = list(ctx.removed())
158 158 touched = []
159 159 linkrev = len(repo)
160 160 repo.ui.note(_(b"committing files:\n"))
161 161 uipathfn = scmutil.getuipathfn(repo)
162 162 for f in sorted(ctx.modified() + ctx.added()):
163 163 repo.ui.note(uipathfn(f) + b"\n")
164 164 try:
165 165 fctx = ctx[f]
166 166 if fctx is None:
167 167 removed.append(f)
168 168 else:
169 169 added.append(f)
170 170 m[f], is_touched = _filecommit(
171 171 repo, fctx, m1, m2, linkrev, tr, writefilecopymeta,
172 172 )
173 173 if is_touched:
174 174 touched.append(f)
175 175 if is_touched == 'added':
176 176 filesadded.append(f)
177 177 m.setflag(f, fctx.flags())
178 178 except OSError:
179 179 repo.ui.warn(_(b"trouble committing %s!\n") % uipathfn(f))
180 180 raise
181 181 except IOError as inst:
182 182 errcode = getattr(inst, 'errno', errno.ENOENT)
183 183 if error or errcode and errcode != errno.ENOENT:
184 184 repo.ui.warn(_(b"trouble committing %s!\n") % uipathfn(f))
185 185 raise
186 186
187 187 # update manifest
188 188 removed = [f for f in removed if f in m1 or f in m2]
189 189 drop = sorted([f for f in removed if f in m])
190 190 for f in drop:
191 191 del m[f]
192 192 if p2.rev() != nullrev:
193 193 rf = metadata.get_removal_filter(ctx, (p1, p2, m1, m2))
194 194 removed = [f for f in removed if not rf(f)]
195 195
196 196 touched.extend(removed)
197 197
198 198 files = touched
199 199 mn = _commit_manifest(tr, linkrev, ctx, mctx, files, added, drop)
200 200
201 201 return mn, files, filesadded, removed
202 202
203 203
204 204 def _filecommit(
205 205 repo, fctx, manifest1, manifest2, linkrev, tr, includecopymeta,
206 206 ):
207 207 """
208 208 commit an individual file as part of a larger transaction
209 209
210 210 input:
211 211
212 212 fctx: a file context with the content we are trying to commit
213 213 manifest1: manifest of changeset first parent
214 214 manifest2: manifest of changeset second parent
215 215 linkrev: revision number of the changeset being created
216 216 tr: current transation
217 217 individual: boolean, set to False to skip storing the copy data
218 218 (only used by the Google specific feature of using
219 219 changeset extra as copy source of truth).
220 220
221 221 output: (filenode, touched)
222 222
223 223 filenode: the filenode that should be used by this changeset
224 224 touched: one of: None (mean untouched), 'added' or 'modified'
225 225 """
226 226
227 227 fname = fctx.path()
228 228 fparent1 = manifest1.get(fname, nullid)
229 229 fparent2 = manifest2.get(fname, nullid)
230 230 touched = None
231 231 if fparent1 == fparent2 == nullid:
232 232 touched = 'added'
233 233
234 234 if isinstance(fctx, context.filectx):
235 235 # This block fast path most comparisons which are usually done. It
236 236 # assumes that bare filectx is used and no merge happened, hence no
237 237 # need to create a new file revision in this case.
238 238 node = fctx.filenode()
239 239 if node in [fparent1, fparent2]:
240 240 repo.ui.debug(b'reusing %s filelog entry\n' % fname)
241 241 if (
242 242 fparent1 != nullid and manifest1.flags(fname) != fctx.flags()
243 243 ) or (
244 244 fparent2 != nullid and manifest2.flags(fname) != fctx.flags()
245 245 ):
246 246 touched = 'modified'
247 247 return node, touched
248 248
249 249 flog = repo.file(fname)
250 250 meta = {}
251 251 cfname = fctx.copysource()
252 252 fnode = None
253 253
254 254 if cfname and cfname != fname:
255 255 # Mark the new revision of this file as a copy of another
256 256 # file. This copy data will effectively act as a parent
257 257 # of this new revision. If this is a merge, the first
258 258 # parent will be the nullid (meaning "look up the copy data")
259 259 # and the second one will be the other parent. For example:
260 260 #
261 261 # 0 --- 1 --- 3 rev1 changes file foo
262 262 # \ / rev2 renames foo to bar and changes it
263 263 # \- 2 -/ rev3 should have bar with all changes and
264 264 # should record that bar descends from
265 265 # bar in rev2 and foo in rev1
266 266 #
267 267 # this allows this merge to succeed:
268 268 #
269 269 # 0 --- 1 --- 3 rev4 reverts the content change from rev2
270 270 # \ / merging rev3 and rev4 should use bar@rev2
271 271 # \- 2 --- 4 as the merge base
272 272 #
273 273
274 274 cnode = manifest1.get(cfname)
275 275 newfparent = fparent2
276 276
277 277 if manifest2: # branch merge
278 278 if fparent2 == nullid or cnode is None: # copied on remote side
279 279 if cfname in manifest2:
280 280 cnode = manifest2[cfname]
281 281 newfparent = fparent1
282 282
283 283 # Here, we used to search backwards through history to try to find
284 284 # where the file copy came from if the source of a copy was not in
285 285 # the parent directory. However, this doesn't actually make sense to
286 286 # do (what does a copy from something not in your working copy even
287 287 # mean?) and it causes bugs (eg, issue4476). Instead, we will warn
288 288 # the user that copy information was dropped, so if they didn't
289 289 # expect this outcome it can be fixed, but this is the correct
290 290 # behavior in this circumstance.
291 291
292 292 if cnode:
293 293 repo.ui.debug(b" %s: copy %s:%s\n" % (fname, cfname, hex(cnode)))
294 294 if includecopymeta:
295 295 meta[b"copy"] = cfname
296 296 meta[b"copyrev"] = hex(cnode)
297 297 fparent1, fparent2 = nullid, newfparent
298 298 else:
299 299 repo.ui.warn(
300 300 _(
301 301 b"warning: can't find ancestor for '%s' "
302 302 b"copied from '%s'!\n"
303 303 )
304 304 % (fname, cfname)
305 305 )
306 306
307 307 elif fparent1 == nullid:
308 308 fparent1, fparent2 = fparent2, nullid
309 309 elif fparent2 != nullid:
310 310 # is one parent an ancestor of the other?
311 311 fparentancestors = flog.commonancestorsheads(fparent1, fparent2)
312 312 if fparent1 in fparentancestors:
313 313 fparent1, fparent2 = fparent2, nullid
314 314 elif fparent2 in fparentancestors:
315 315 fparent2 = nullid
316 316 elif not fparentancestors:
317 317 # TODO: this whole if-else might be simplified much more
318 318 ms = mergestate.mergestate.read(repo)
319 319 if (
320 320 fname in ms
321 321 and ms[fname] == mergestate.MERGE_RECORD_MERGED_OTHER
322 322 ):
323 323 fparent1, fparent2 = fparent2, nullid
324 324
325 325 # is the file changed?
326 326 text = fctx.data()
327 327 if fparent2 != nullid or meta or flog.cmp(fparent1, text):
328 328 if touched is None: # do not overwrite added
329 329 touched = 'modified'
330 330 fnode = flog.add(text, meta, tr, linkrev, fparent1, fparent2)
331 331 # are just the flags changed during merge?
332 332 elif fname in manifest1 and manifest1.flags(fname) != fctx.flags():
333 333 touched = 'modified'
334 334 fnode = fparent1
335 335 else:
336 336 fnode = fparent1
337 337 return fnode, touched
338 338
339 339
340 340 def _commit_manifest(tr, linkrev, ctx, mctx, files, added, drop):
341 341 """make a new manifest entry (or reuse a new one)
342 342
343 343 given an initialised manifest context and precomputed list of
344 344 - files: files affected by the commit
345 345 - added: new entries in the manifest
346 346 - drop: entries present in parents but absent of this one
347 347
348 348 Create a new manifest revision, reuse existing ones if possible.
349 349
350 350 Return the nodeid of the manifest revision.
351 351 """
352 352 repo = ctx.repo()
353 353
354 354 md = None
355 355
356 356 # all this is cached, so it is find to get them all from the ctx.
357 357 p1 = ctx.p1()
358 358 p2 = ctx.p2()
359 359 m1ctx = p1.manifestctx()
360 360
361 361 m1 = m1ctx.read()
362 362
363 363 manifest = mctx.read()
364 364
365 365 if not files:
366 366 # if no "files" actually changed in terms of the changelog,
367 367 # try hard to detect unmodified manifest entry so that the
368 368 # exact same commit can be reproduced later on convert.
369 369 md = m1.diff(manifest, scmutil.matchfiles(repo, ctx.files()))
370 370 if not files and md:
371 371 repo.ui.debug(
372 372 b'not reusing manifest (no file change in '
373 373 b'changelog, but manifest differs)\n'
374 374 )
375 375 if files or md:
376 376 repo.ui.note(_(b"committing manifest\n"))
377 377 # we're using narrowmatch here since it's already applied at
378 378 # other stages (such as dirstate.walk), so we're already
379 379 # ignoring things outside of narrowspec in most cases. The
380 380 # one case where we might have files outside the narrowspec
381 381 # at this point is merges, and we already error out in the
382 382 # case where the merge has files outside of the narrowspec,
383 383 # so this is safe.
384 384 mn = mctx.write(
385 385 tr,
386 386 linkrev,
387 387 p1.manifestnode(),
388 388 p2.manifestnode(),
389 389 added,
390 390 drop,
391 391 match=repo.narrowmatch(),
392 392 )
393 393 else:
394 394 repo.ui.debug(
395 395 b'reusing manifest from p1 (listed files ' b'actually unchanged)\n'
396 396 )
397 397 mn = p1.manifestnode()
398 398
399 399 return mn
General Comments 0
You need to be logged in to leave comments. Login now