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