diff --git a/contrib/purge/purge.py b/contrib/purge/purge.py --- a/contrib/purge/purge.py +++ b/contrib/purge/purge.py @@ -31,7 +31,8 @@ from mercurial import hg, util from mercurial.i18n import _ import os -def dopurge(ui, repo, dirs=None, act=True, abort_on_err=False, eol='\n'): +def dopurge(ui, repo, dirs=None, act=True, abort_on_err=False, eol='\n', + force=False): def error(msg): if abort_on_err: raise util.Abort(msg) @@ -49,14 +50,19 @@ def dopurge(ui, repo, dirs=None, act=Tru directories = [] files = [] + missing = [] roots, match, anypats = util.cmdmatcher(repo.root, repo.getcwd(), dirs) for src, f, st in repo.dirstate.statwalk(files=roots, match=match, ignored=True, directories=True): if src == 'd': directories.append(f) + elif src == 'm': + missing.append(f) elif src == 'f' and f not in repo.dirstate: files.append(f) + _check_missing(ui, repo, missing, force) + directories.sort() for f in files: @@ -69,6 +75,43 @@ def dopurge(ui, repo, dirs=None, act=Tru ui.note(_('Removing directory %s\n') % f) remove(os.rmdir, f) +def _check_missing(ui, repo, missing, force=False): + """Abort if there is the chance of having problems with name-mangling fs + + In a name mangling filesystem (e.g. a case insensitive one) + dirstate.walk() can yield filenames different from the ones + stored in the dirstate. This already confuses the status and + add commands, but with purge this may cause data loss. + + To prevent this, _check_missing will abort if there are missing + files. The force option will let the user skip the check if he + knows it is safe. + + Even with the force option this function will check if any of the + missing files is still available in the working dir: if so there + may be some problem with the underlying filesystem, so it + aborts unconditionally.""" + + found = [f for f in missing if util.lexists(repo.wjoin(f))] + + if found: + if not ui.quiet: + ui.warn(_("The following tracked files weren't listed by the " + "filesystem, but could still be found:\n")) + for f in found: + ui.warn("%s\n" % f) + if util.checkfolding(repo.path): + ui.warn(_("This is probably due to a case-insensitive " + "filesystem\n")) + raise util.Abort(_("purging on name mangling filesystems is not " + "yet fully supported")) + + if missing and not force: + raise util.Abort(_("there are missing files in the working dir and " + "purge still has problems with them due to name " + "mangling filesystems. " + "Use --force if you know what you are doing")) + def purge(ui, repo, *dirs, **opts): '''removes files not tracked by mercurial @@ -100,13 +143,15 @@ def purge(ui, repo, *dirs, **opts): if eol == '\0': # --print0 implies --print act = False - dopurge(ui, repo, dirs, act, abort_on_err, eol) + force = bool(opts['force']) + dopurge(ui, repo, dirs, act, abort_on_err, eol, force) cmdtable = { 'purge': (purge, [('a', 'abort-on-err', None, _('abort if an error occurs')), + ('f', 'force', None, _('purge even when missing files are detected')), ('p', 'print', None, _('print the file names instead of deleting them')), ('0', 'print0', None, _('end filenames with NUL, for use with xargs' ' (implies -p)'))], diff --git a/tests/test-purge b/tests/test-purge --- a/tests/test-purge +++ b/tests/test-purge @@ -74,3 +74,26 @@ touch ignored hg purge -p hg purge -v ls + +echo % abort with missing files until we support name mangling filesystems +touch untracked_file +rm r1 +# hide error messages to avoid changing the output when the text changes +hg purge -p 2> /dev/null +if [ $? -ne 0 ]; then + echo "refused to run" +fi +if [ -f untracked_file ]; then + echo "untracked_file still around" +fi +hg purge -p --force +hg purge -v 2> /dev/null +if [ $? -ne 0 ]; then + echo "refused to run" +fi +if [ -f untracked_file ]; then + echo "untracked_file still around" +fi +hg purge -v --force +hg revert --all --quiet +ls diff --git a/tests/test-purge.out b/tests/test-purge.out --- a/tests/test-purge.out +++ b/tests/test-purge.out @@ -47,3 +47,12 @@ ignored Removing file ignored directory r1 +% abort with missing files until we support name mangling filesystems +refused to run +untracked_file still around +untracked_file +refused to run +untracked_file still around +Removing file untracked_file +directory +r1