6 # GNU General Public License version 2 or any later version. |
6 # GNU General Public License version 2 or any later version. |
7 |
7 |
8 from node import hex, nullid, nullrev, short |
8 from node import hex, nullid, nullrev, short |
9 from i18n import _ |
9 from i18n import _ |
10 import os, sys, errno, re, glob, tempfile |
10 import os, sys, errno, re, glob, tempfile |
11 import util, scmutil, templater, patch, error, templatekw |
11 import util, scmutil, templater, patch, error, templatekw, wdutil |
12 import match as matchmod |
12 import match as matchmod |
13 import similar, revset, subrepo |
13 import similar, revset, subrepo |
|
14 |
|
15 expandpats = wdutil.expandpats |
|
16 match = wdutil.match |
|
17 matchall = wdutil.matchall |
|
18 matchfiles = wdutil.matchfiles |
|
19 addremove = wdutil.addremove |
|
20 dirstatecopy = wdutil.dirstatecopy |
14 |
21 |
15 revrangesep = ':' |
22 revrangesep = ':' |
16 |
23 |
17 def parsealiases(cmd): |
24 def parsealiases(cmd): |
18 return cmd.lstrip("^").split("|") |
25 return cmd.lstrip("^").split("|") |
240 if hasattr(pat, 'read') and 'r' in mode: |
247 if hasattr(pat, 'read') and 'r' in mode: |
241 return pat |
248 return pat |
242 return open(make_filename(repo, pat, node, total, seqno, revwidth, |
249 return open(make_filename(repo, pat, node, total, seqno, revwidth, |
243 pathname), |
250 pathname), |
244 mode) |
251 mode) |
245 |
|
246 def expandpats(pats): |
|
247 if not util.expandglobs: |
|
248 return list(pats) |
|
249 ret = [] |
|
250 for p in pats: |
|
251 kind, name = matchmod._patsplit(p, None) |
|
252 if kind is None: |
|
253 try: |
|
254 globbed = glob.glob(name) |
|
255 except re.error: |
|
256 globbed = [name] |
|
257 if globbed: |
|
258 ret.extend(globbed) |
|
259 continue |
|
260 ret.append(p) |
|
261 return ret |
|
262 |
|
263 def match(repo, pats=[], opts={}, globbed=False, default='relpath'): |
|
264 if pats == ("",): |
|
265 pats = [] |
|
266 if not globbed and default == 'relpath': |
|
267 pats = expandpats(pats or []) |
|
268 m = matchmod.match(repo.root, repo.getcwd(), pats, |
|
269 opts.get('include'), opts.get('exclude'), default, |
|
270 auditor=repo.auditor) |
|
271 def badfn(f, msg): |
|
272 repo.ui.warn("%s: %s\n" % (m.rel(f), msg)) |
|
273 m.bad = badfn |
|
274 return m |
|
275 |
|
276 def matchall(repo): |
|
277 return matchmod.always(repo.root, repo.getcwd()) |
|
278 |
|
279 def matchfiles(repo, files): |
|
280 return matchmod.exact(repo.root, repo.getcwd(), files) |
|
281 |
|
282 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None): |
|
283 if dry_run is None: |
|
284 dry_run = opts.get('dry_run') |
|
285 if similarity is None: |
|
286 similarity = float(opts.get('similarity') or 0) |
|
287 # we'd use status here, except handling of symlinks and ignore is tricky |
|
288 added, unknown, deleted, removed = [], [], [], [] |
|
289 audit_path = scmutil.pathauditor(repo.root) |
|
290 m = match(repo, pats, opts) |
|
291 for abs in repo.walk(m): |
|
292 target = repo.wjoin(abs) |
|
293 good = True |
|
294 try: |
|
295 audit_path(abs) |
|
296 except (OSError, util.Abort): |
|
297 good = False |
|
298 rel = m.rel(abs) |
|
299 exact = m.exact(abs) |
|
300 if good and abs not in repo.dirstate: |
|
301 unknown.append(abs) |
|
302 if repo.ui.verbose or not exact: |
|
303 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs)) |
|
304 elif repo.dirstate[abs] != 'r' and (not good or not os.path.lexists(target) |
|
305 or (os.path.isdir(target) and not os.path.islink(target))): |
|
306 deleted.append(abs) |
|
307 if repo.ui.verbose or not exact: |
|
308 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs)) |
|
309 # for finding renames |
|
310 elif repo.dirstate[abs] == 'r': |
|
311 removed.append(abs) |
|
312 elif repo.dirstate[abs] == 'a': |
|
313 added.append(abs) |
|
314 copies = {} |
|
315 if similarity > 0: |
|
316 for old, new, score in similar.findrenames(repo, |
|
317 added + unknown, removed + deleted, similarity): |
|
318 if repo.ui.verbose or not m.exact(old) or not m.exact(new): |
|
319 repo.ui.status(_('recording removal of %s as rename to %s ' |
|
320 '(%d%% similar)\n') % |
|
321 (m.rel(old), m.rel(new), score * 100)) |
|
322 copies[new] = old |
|
323 |
|
324 if not dry_run: |
|
325 wctx = repo[None] |
|
326 wlock = repo.wlock() |
|
327 try: |
|
328 wctx.remove(deleted) |
|
329 wctx.add(unknown) |
|
330 for new, old in copies.iteritems(): |
|
331 wctx.copy(old, new) |
|
332 finally: |
|
333 wlock.release() |
|
334 |
|
335 def updatedir(ui, repo, patches, similarity=0): |
|
336 '''Update dirstate after patch application according to metadata''' |
|
337 if not patches: |
|
338 return [] |
|
339 copies = [] |
|
340 removes = set() |
|
341 cfiles = patches.keys() |
|
342 cwd = repo.getcwd() |
|
343 if cwd: |
|
344 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()] |
|
345 for f in patches: |
|
346 gp = patches[f] |
|
347 if not gp: |
|
348 continue |
|
349 if gp.op == 'RENAME': |
|
350 copies.append((gp.oldpath, gp.path)) |
|
351 removes.add(gp.oldpath) |
|
352 elif gp.op == 'COPY': |
|
353 copies.append((gp.oldpath, gp.path)) |
|
354 elif gp.op == 'DELETE': |
|
355 removes.add(gp.path) |
|
356 |
|
357 wctx = repo[None] |
|
358 for src, dst in copies: |
|
359 dirstatecopy(ui, repo, wctx, src, dst, cwd=cwd) |
|
360 if (not similarity) and removes: |
|
361 wctx.remove(sorted(removes), True) |
|
362 |
|
363 for f in patches: |
|
364 gp = patches[f] |
|
365 if gp and gp.mode: |
|
366 islink, isexec = gp.mode |
|
367 dst = repo.wjoin(gp.path) |
|
368 # patch won't create empty files |
|
369 if gp.op == 'ADD' and not os.path.lexists(dst): |
|
370 flags = (isexec and 'x' or '') + (islink and 'l' or '') |
|
371 repo.wwrite(gp.path, '', flags) |
|
372 util.setflags(dst, islink, isexec) |
|
373 addremove(repo, cfiles, similarity=similarity) |
|
374 files = patches.keys() |
|
375 files.extend([r for r in removes if r not in files]) |
|
376 return sorted(files) |
|
377 |
|
378 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None): |
|
379 """Update the dirstate to reflect the intent of copying src to dst. For |
|
380 different reasons it might not end with dst being marked as copied from src. |
|
381 """ |
|
382 origsrc = repo.dirstate.copied(src) or src |
|
383 if dst == origsrc: # copying back a copy? |
|
384 if repo.dirstate[dst] not in 'mn' and not dryrun: |
|
385 repo.dirstate.normallookup(dst) |
|
386 else: |
|
387 if repo.dirstate[origsrc] == 'a' and origsrc == src: |
|
388 if not ui.quiet: |
|
389 ui.warn(_("%s has not been committed yet, so no copy " |
|
390 "data will be stored for %s.\n") |
|
391 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd))) |
|
392 if repo.dirstate[dst] in '?r' and not dryrun: |
|
393 wctx.add([dst]) |
|
394 elif not dryrun: |
|
395 wctx.copy(origsrc, dst) |
|
396 |
252 |
397 def copy(ui, repo, pats, opts, rename=False): |
253 def copy(ui, repo, pats, opts, rename=False): |
398 # called with the repo lock held |
254 # called with the repo lock held |
399 # |
255 # |
400 # hgsep => pathname that uses "/" to separate directories |
256 # hgsep => pathname that uses "/" to separate directories |