Merge lp:~vila/bzr/323111-orphan-non-versioned-files into lp:bzr

Proposed by Vincent Ladeuil
Status: Merged
Merged at revision: 5504
Proposed branch: lp:~vila/bzr/323111-orphan-non-versioned-files
Merge into: lp:bzr
Prerequisite: lp:~vila/bzr/323111-backup-names
Diff against target: 367 lines (+200/-12) (has conflicts)
7 files modified
bzrlib/bzrdir.py (+4/-1)
bzrlib/tests/per_workingtree/test_pull.py (+37/-0)
bzrlib/tests/test_bzrdir.py (+14/-3)
bzrlib/tests/test_transform.py (+70/-0)
bzrlib/transform.py (+57/-8)
doc/en/release-notes/bzr-2.3.txt (+6/-0)
doc/en/whats-new/whats-new-in-2.3.txt (+12/-0)
Text conflict in doc/en/whats-new/whats-new-in-2.3.txt
To merge this branch: bzr merge lp:~vila/bzr/323111-orphan-non-versioned-files
Reviewer Review Type Date Requested Status
Andrew Bennetts Needs Information
Review via email: mp+35110@code.launchpad.net

Description of the change

When a bzr command remove a previously versioned directory, it
sometimes fails because the directory contains unversioned files.

This is a pain.

The idea to address this problem is to 'orphan' such unversioned
files. This is implemented by moving them out of the way into a
directory called 'bzr-orphans' at the root of the working tree
and name them <file>.~#~, <file> being the `basename` of the file
and # being an incremented number.

The initial idea discussed with lifeless on IRC long ago was to
have an even simpler policy for orphaning: keep the original file
name and if one is already present, just erase it.

I went a bit further using backup names since a single bzr
operation could produce several orphans with the same base name
otherwise and that would have meant losing all but the last
orphan with this name.

The implementation first check the children of the conflicting
parent dir. If all of them are unversioned files (leading to an
empty directory) the conflict is resolved by moving all the
children into the orphan directory. The directory can then be
deleted without conflicts.

This is implemented for DiskTreeTransform and forbidden for
TransformPreview where it's not supposed to occur anyway since
there should be no unversioned files in the input tree.

Not that since we rely on TreeTransform, I don't think the
'bzr-orphans' can be outside of the working tree (IMBW) but I
don't think it's a problem for a first implementation.

This triggers bug #634470 revealing a latent bug in subtree
support. Since this is an experimental format so far, poolie
suggested to turn the failing test into an expected failure
instead.

Given that this fixes a behaviour that wasn't exercised by the
test suite, I'd appreciate feedback on real life scenarios that
are fixed or additional test ideas.

Note that this is self-contained into new_orphan() so far.
Possible enhancements includes:

- making the 'bzr-orphans' directory be configurable (it's
  clearly tight to the working tree though),

- allows various orphaning policies (always delete, delete junk
  files (tbd), never orphan (the behaviour before this patch),
  etc)

There is one more submission coming with more cleanups.

To post a comment you must log in.
Revision history for this message
Vincent Ladeuil (vila) wrote :
Download full text (5.1 KiB)

>>>>> John Arbash Meinel <email address hidden> writes:

<snip/>

    >> The implementation first check the children of the conflicting
    >> parent dir. If all of them are unversioned files (leading to an
    >> empty directory) the conflict is resolved by moving all the
    >> children into the orphan directory. The directory can then be
    >> deleted without conflicts.

    > Could we just rename the directory into bzr-orphans?

Not if it already exists obviously.

    > Preserving the clustering would be nice, though I understand
    > flattening it.

bzr-orphans collect *all* the orphans, there could be more than one
directory involved, so special-casing here... in treetransform... I
don't feel like it :)

    > One concern is that directory filling up with useless files after
    > doing a 'bzr switch' 5 or so times across a
    > directory-full-of-python-files deletion/creation.

Damn, I forgot to mention that.

When we discussed it with lifeless, the idea was that the user will
suddenly get a 'bzr-orphans' in his face and will deal with it. This is
still less disturbing than a conflict since it can accumulate several
commands producing orphans. So dealing with it should be as simple as:
have a look, nothing interesting, delete the bzr-orphans directory.

Now there are certainly various ways to enhance that but the proposed
implementation gives us a way to experiment.

Should be bring the subject to the mailing list once this has landed to
ask for a large feedback:

- do you produce many orphans ?
- did you wonder where this 'bzr-orphans' directory came from ?

- would you like warnings emitted when files are orphaned (hmm, should
  we wait feedback for that ? :-)

trace.warning added.

    > ...

    >> Note that this is self-contained into new_orphan() so far.
    >> Possible enhancements includes:
    >>
    >> - making the 'bzr-orphans' directory be configurable (it's
    >> clearly tight to the working tree though),
    >>
    >> - allows various orphaning policies (always delete, delete junk
    >> files (tbd), never orphan (the behaviour before this patch),
    >> etc)

    > - I think what we really want is a hook point for determining what to do
    > with the file. Similar to the per-file merge hook, something that is a
    > per-file 'ignored file handler'.

Hehe, obviously we may want to add some .bzrjunk support which in turn
will be respected by the orphaning :)

But once junk files are out of the way, yes, such a hook sounds like the
way to go.

    > ...
    > +class TestTransformMissingParent(tests.TestCaseWithTransport):
    > +
    > + def get_tree_transform_with_unversioned_dir(self):

    > ^- I think we usually call these "make_*", since it is creating stuff.

Bah, you know you've been reading test_transform too much when... :)

Fixed.

    > + def new_orphan(self, trans_id, parent_id):
    > + """See TreeTransformBase.new_orphan."""
    > + # Add the orphan dir if it doesn't exist
    > + od_id = self.trans_id_tree_path('bzr-orphans')
    > + if self.final_kind(od_id) is None:
    > + self.create_directory(od_id)
    > + # Find a name that doesn't exist ...

Read more...

Revision history for this message
Vincent Ladeuil (vila) wrote :

>>>>> Vincent Ladeuil <email address hidden> writes:

<snip/>

    >> ^- I don't think this check is actually correct. Since if the
    >> bzr-orphans directory already exists, you'd already have a file present
    >> that you have to watch out for conflicting with.
    >> So you need to check *both* that the path isn't going to be
    >> created (yet) and that it doesn't already exist.

    > *blink* Grr, trying to make the threads easier to review, I separated
    > the addition of _has_named_child which *does* the correct checks.

    > The correct check should be done here by calling _get_backup_name() not
    > osutils.available_backup_name().

    > Fixed in both threads.

Clarification: The code presented in the first submission wasn't
correct. When I realized that, I found _get_backup_name and from there
realized that we had some duplication and some dead code so I create the
3 threads.

So I've now fixed *this* thread to call _get_backup_name and the
following thread will deprecate it introducing tt._available_backup_name
both of them doing the right checks.

So 'fixed in both threads' means: this thread has now the correct fix
and the third one always had the correct fix.

Sorry for the confusion :-/

Revision history for this message
Martin Pool (mbp) wrote :

I think I would like to see a configuration option that will let you retain the old option, so that anyone who thinks it's a step backwards can use it.

http://bazaarvcs.wordpress.com/?p=189

Revision history for this message
Andrew Bennetts (spiv) wrote :

I agree a configuration option would be good to add. As discussed on IRC this would mean both that users that prefer the old behaviour can keep it, and also would be a good sign that the code is modular enough that we could allow plugins to define new policies without needing to significantly rework it.

From John's review:

> > Could we just rename the directory into bzr-orphans?
>
> Not if it already exists obviously.

Well, we can't rename files into bzr-orphans if the name already exists, either. But you could rename it somedir.~2~ or whatever. This would make restoring precious files *much* easier than a flat directory of *.~?~ files. I'd suspect that preserving the directory structure as much as possible will greatly reduce the number of users that will prefer the old behaviour.

Also, it would be nice to write out a file into bzr-orphans describing the original paths and the new name in bzr-orphans, to make it possible to automatically put the tree back exactly as it was. (Even cooler might be if 'bzr revert' could use that file? That's clearly way out of scope for this patch though...)

I'll follow-up separately with thoughts on the code itself.

Revision history for this message
Andrew Bennetts (spiv) wrote :

The code looks pretty straightforward. Nice!

Here are some issues, though:

174 +class TestOrphan(tests.TestCaseWithTransport):
175 +
176 + # Alternative implementations may want to test:
177 + # - can't create orphan dir
178 + # - orphaning forbidden
179 + # - can't create orphan

I don't quite follow this comment. Which alternative implementations — these aren't per-transform tests? Is this a list of hypothetical alternative orphan-handling policies, or scenarios alternative implementations of orphan handling ought to consider, or something else?

I guess basically I don't understand who the intended audience is for this comment.

222 +lazy_import.lazy_import(globals(), """

Why the change to "from bzrlib import lazy_import", rather than "from bzrlib.lazy_import import lazy_import". We overwhelmingly prefer the latter elsewhere in our code base.

316 + if trans_id in tt._removed_contents:
317 + orphans = []
318 + empty = True
319 + # Find the potential orphans, stop if one item should be kept
320 + for c in tt.by_parent()[trans_id]:
321 + if tt.final_file_id(c) is None:
322 + orphans.append(c)
323 + else:
324 + empty = False
325 + break
326 + if empty:

The 'empty' variable seems redundant. You could just as easily test 'if orphans' (or 'if len(orphans) == 0' if you prefer).

That's all the comments I have so far. I don't have a verdict yet, as I have few too many questions, so I'll mark my review Needs Information for now.

review: Needs Information
Revision history for this message
Vincent Ladeuil (vila) wrote :

> Well, we can't rename files into bzr-orphans if the name already exists,
> either. But you could rename it somedir.~2~ or whatever.

You mean creating bzr-orphans.~1~, bzr-orphans.~2~ ?

> This would make
> restoring precious files *much* easier than a flat directory of *.~?~ files.

This is not exactly a flat structure... directories can be orphaned too.

> I'd suspect that preserving the directory structure as much as possible will
> greatly reduce the number of users that will prefer the old behaviour.

The counter-argument was that if you re-create all the directories leading to the orphan,
it becomes harder to inspect bzr-orphans as you have to drill down so see which files
have been orphaned.

>
> Also, it would be nice to write out a file into bzr-orphans describing the
> original paths and the new name in bzr-orphans, to make it possible to
> automatically put the tree back exactly as it was.

Yup, I thought about that, but as you said below, out-of-scope.

> (Even cooler might be if
> 'bzr revert' could use that file?

That would certainly lead to very complex code: the orphans are not versioned, the user
didn't ask bzr to version them, so we know little about them and we are not prepared
to deal with them (ditto for shelves...)

> That's clearly way out of scope for this
> patch though...)
>
> I'll follow-up separately with thoughts on the code itself.

Revision history for this message
Vincent Ladeuil (vila) wrote :
Download full text (4.1 KiB)

> The code looks pretty straightforward. Nice!
>
> Here are some issues, though:
>
> 174 +class TestOrphan(tests.TestCaseWithTransport):
> 175 +
> 176 + # Alternative implementations may want to test:
> 177 + # - can't create orphan dir
> 178 + # - orphaning forbidden
> 179 + # - can't create orphan
>
> I don't quite follow this comment. Which alternative implementations — these
> aren't per-transform tests?

No :)

> Is this a list of hypothetical alternative
> orphan-handling policies, or scenarios alternative implementations of orphan
> handling ought to consider,

Yes.

> or something else?
>
> I guess basically I don't understand who the intended audience is for this
> comment.

People implementing new orphaning policies and wondering how to test them :)

Which I'm currently doing ;)

So the next submission, addressing the config
variable (https://code.edge.launchpad.net/~vila/bzr/323111-orphan-config-option/+merge/35690)
take care of these comments which were, obviously, addressed to
me as stealth way to avoid implementing it :-D

>
> 222 +lazy_import.lazy_import(globals(), """
>
> Why the change to "from bzrlib import lazy_import", rather than "from
> bzrlib.lazy_import import lazy_import". We overwhelmingly prefer the latter
> elsewhere in our code base.

Except it has been said several times that we shouldn't import
symbols from modules but prefer importing modules and prefix
symbols and that we should migrate to this form progressively.

I've been doing many such cleanups, some addressing weird and
subtle aliasing bugs, including ones related to lazy
imports (lazy imports more or less requires using
<module>.<symbol> in some cases).

There are a few exceptions to this rule:
- symbol_versioning but even there I've commented that we may
  want to rework this module too,
- bzrlib.tests aliasing TestCase, etc symbols from
  bzrlib.tests.TestUtils (but isn't it a sign that they should be
  define into bzrlib.tests ?)

I also found that simplifying the import list at the beginning of
the module makes it clearer and avoid leaving cruft there.

It generally makes it easier to move code from one module to the
others since there are fewer dependencies to update.

It makes the code easier to read (you don't have to go back to
the beginning of the module to know where this apparently global
symbol is coming from).

Another reason from the leaking tests saga:

,----
| I did so by wrapping transport.get_transport() only to realise
| that far too many tests were not using it. I also uncovered a
| nasty reason why we should always use the 'import <module> ;
| <module>.<function>' idiom instead of 'from <module> import
| <function> ; <function>': the later doesn't allow
| wrappers... Obvious a posteriori, but I was quite puzzled before
| I realised that. Those were worth fixing even if we decide to replace
| the wrapping by a hook as mentioned below.
`----

Now, we can make lazy_import another exception... but why ?

And why do I keep bumping into people that don't want to use the
'import module ; module.symbol' given that we laready encounter
several related problems.

The only pro I can find for doing 'from ...

Read more...

Revision history for this message
Marius Kruger (amanica) wrote :

this bzr-orphans dir must be ignored by bzr by default,
i.e. it should not be accidental be added with an unqualified `bzr add` .

we used to have this problem with upgrading, so now the first backup
dir we make is
backup.bzr.~1~ which is ignored by default, and you can have more than one.

so either call it bzr-orphans~ or bzr-orphans~1~ or add bzr-orphans to
the default ignore list. (I think its fine if thats done a followup)

Revision history for this message
Martin Pool (mbp) wrote :

Regarding 'import module' - Vincent, if you want this done
consistently you should put up a proposal to add it to the coding
standards, together with the rationale and the exceptions. Then we
can settle it clearly and there won't be case-by-case arguments or
questions. We could even possibly add rules to pyflakes or something
to warn about violations.

To me 'lazy_import' actually is a reasonable case for importing the
particular symbol, because it's clear where it comes from and it seems
pointlessly redundant to say 'lazy_import.lazy_import'.

--
Martin

Revision history for this message
Andrew Bennetts (spiv) wrote :

Martin Pool wrote:
> Regarding 'import module' - Vincent, if you want this done
> consistently you should put up a proposal to add it to the coding
> standards, together with the rationale and the exceptions. Then we
> can settle it clearly and there won't be case-by-case arguments or
> questions. We could even possibly add rules to pyflakes or something
> to warn about violations.
>
> To me 'lazy_import' actually is a reasonable case for importing the
> particular symbol, because it's clear where it comes from and it seems
> pointlessly redundant to say 'lazy_import.lazy_import'.

Another thing about lazy_import is it usually doesn't make sense to
include it in the main "from bzrlib import (....)" lines, because it is
often used before that point. So it will always tend to have its own
import statement.

I'm also unaware of any actual problems caused by importing the
lazy_import function directly rather than the module, so changing this
convention does seem to have no significant benefit.

Revision history for this message
Martin Pool (mbp) wrote :

On 17 September 2010 14:43, Andrew Bennetts
<email address hidden> wrote:
> I'm also unaware of any actual problems caused by importing the
> lazy_import function directly rather than the module, so changing this
> convention does seem to have no significant benefit.

I'm ok with saying "normally use 'import module'" but I think this
should be one of the exceptions.

--
Martin

Revision history for this message
Andrew Bennetts (spiv) wrote :

Martin Pool wrote:
> On 17 September 2010 14:43, Andrew Bennetts
> <email address hidden> wrote:
> > I'm also unaware of any actual problems caused by importing the
> > lazy_import function directly rather than the module, so changing this
> > convention does seem to have no significant benefit.
>
> I'm ok with saying "normally use 'import module'" but I think this
> should be one of the exceptions.

I agree.

Revision history for this message
Vincent Ladeuil (vila) wrote :

>>>>> Andrew Bennetts <email address hidden> writes:

    > Martin Pool wrote:
    >> On 17 September 2010 14:43, Andrew Bennetts
    >> <email address hidden> wrote:
    >> > I'm also unaware of any actual problems caused by importing the
    >> > lazy_import function directly rather than the module, so changing this
    >> > convention does seem to have no significant benefit.
    >>
    >> I'm ok with saying "normally use 'import module'" but I think this
    >> should be one of the exceptions.

    > I agree.

Pfew, I thought I had to fight for that.
Of course I'm ok with the exception for lazy_import :)

Revision history for this message
Vincent Ladeuil (vila) wrote :

Updated, please review. I'll make another submission for the import policy discussion.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bzrlib/bzrdir.py'
--- bzrlib/bzrdir.py 2010-10-15 14:16:12 +0000
+++ bzrlib/bzrdir.py 2010-10-15 14:16:12 +0000
@@ -1294,7 +1294,10 @@
1294 wt = self.open_workingtree(recommend_upgrade=False)1294 wt = self.open_workingtree(recommend_upgrade=False)
1295 repository = wt.branch.repository1295 repository = wt.branch.repository
1296 empty = repository.revision_tree(_mod_revision.NULL_REVISION)1296 empty = repository.revision_tree(_mod_revision.NULL_REVISION)
1297 wt.revert(old_tree=empty)1297 # We ignore the conflicts returned by wt.revert since we're about to
1298 # delete the wt metadata anyway, all that should be left here are
1299 # detritus. But see bug #634470 about subtree .bzr dirs.
1300 conflicts = wt.revert(old_tree=empty)
1298 self.destroy_workingtree_metadata()1301 self.destroy_workingtree_metadata()
12991302
1300 def destroy_workingtree_metadata(self):1303 def destroy_workingtree_metadata(self):
13011304
=== modified file 'bzrlib/tests/per_workingtree/test_pull.py'
--- bzrlib/tests/per_workingtree/test_pull.py 2010-09-06 10:08:36 +0000
+++ bzrlib/tests/per_workingtree/test_pull.py 2010-10-15 14:16:12 +0000
@@ -17,6 +17,7 @@
1717
18from cStringIO import StringIO18from cStringIO import StringIO
1919
20from bzrlib import tests
20from bzrlib.tests import per_workingtree21from bzrlib.tests import per_workingtree
2122
2223
@@ -62,3 +63,39 @@
62 tree.commit('second')63 tree.commit('second')
63 to_tree.pull(tree.branch)64 to_tree.pull(tree.branch)
64 self.assertEqual('second_root_id', to_tree.get_root_id())65 self.assertEqual('second_root_id', to_tree.get_root_id())
66
67
68class TestPullWithOrphans(per_workingtree.TestCaseWithWorkingTree):
69
70 def make_branch_deleting_dir(self, relpath=None):
71 if relpath is None:
72 relpath = 'trunk'
73 builder = self.make_branch_builder(relpath)
74 builder.start_series()
75
76 # Create an empty trunk
77 builder.build_snapshot('1', None, [
78 ('add', ('', 'root-id', 'directory', ''))])
79 builder.build_snapshot('2', ['1'], [
80 ('add', ('dir', 'dir-id', 'directory', '')),
81 ('add', ('file', 'file-id', 'file', 'trunk content\n')),])
82 builder.build_snapshot('3', ['2'], [
83 ('unversion', 'dir-id'),])
84 builder.finish_series()
85 return builder.get_branch()
86
87 def test_pull_orphans(self):
88 from bzrlib import workingtree
89 if isinstance(self.workingtree_format, workingtree.WorkingTreeFormat2):
90 raise tests.TestSkipped(
91 'WorkingTreeFormat2 does not support missing parent conflicts')
92 trunk = self.make_branch_deleting_dir('trunk')
93 work = trunk.bzrdir.sprout('work', revision_id='2').open_workingtree()
94 # Add some unversioned files in dir
95 self.build_tree(['work/dir/foo',
96 'work/dir/subdir/',
97 'work/dir/subdir/foo'])
98 work.pull(trunk)
99 self.assertLength(0, work.conflicts())
100 # The directory removal should succeed
101 self.failIfExists('work/dir')
65102
=== modified file 'bzrlib/tests/test_bzrdir.py'
--- bzrlib/tests/test_bzrdir.py 2010-10-15 14:16:12 +0000
+++ bzrlib/tests/test_bzrdir.py 2010-10-15 14:16:12 +0000
@@ -792,12 +792,23 @@
792 sub_tree.add('file')792 sub_tree.add('file')
793 tree.commit('Initial commit')793 tree.commit('Initial commit')
794 tree.bzrdir.destroy_workingtree()794 tree.bzrdir.destroy_workingtree()
795 # FIXME: subtree/.bzr is left here which allows the test to pass (or
796 # fail :-( ) -- vila 20100909
795 repo = self.make_repository('repo', shared=True,797 repo = self.make_repository('repo', shared=True,
796 format='dirstate-with-subtree')798 format='dirstate-with-subtree')
797 repo.set_make_working_trees(False)799 repo.set_make_working_trees(False)
798 tree.bzrdir.sprout('repo/tree2')800 # FIXME: we just deleted the workingtree and now we want to use it ????
799 self.failUnlessExists('repo/tree2/subtree')801 # At a minimum, we should use tree.branch below (but this fails too
800 self.failIfExists('repo/tree2/subtree/file')802 # currently) or stop calling this test 'treeless'. Specifically, I've
803 # turn the line below into an assertRaises when 'subtree/.bzr' is
804 # orphaned and sprout tries to access the branch there (which is left
805 # by bzrdir.BzrDirMeta1.destroy_workingtree when it ignores the
806 # [DeletingParent('Not deleting', u'subtree', None)] conflict). See bug
807 # #634470. -- vila 20100909
808 self.assertRaises(errors.NotBranchError,
809 tree.bzrdir.sprout, 'repo/tree2')
810# self.failUnlessExists('repo/tree2/subtree')
811# self.failIfExists('repo/tree2/subtree/file')
801812
802 def make_foo_bar_baz(self):813 def make_foo_bar_baz(self):
803 foo = bzrdir.BzrDir.create_branch_convenience('foo').bzrdir814 foo = bzrdir.BzrDir.create_branch_convenience('foo').bzrdir
804815
=== modified file 'bzrlib/tests/test_transform.py'
--- bzrlib/tests/test_transform.py 2010-10-15 14:16:12 +0000
+++ bzrlib/tests/test_transform.py 2010-10-15 14:16:12 +0000
@@ -28,6 +28,7 @@
28 revision as _mod_revision,28 revision as _mod_revision,
29 rules,29 rules,
30 tests,30 tests,
31 transform,
31 urlutils,32 urlutils,
32 )33 )
33from bzrlib.bzrdir import BzrDir34from bzrlib.bzrdir import BzrDir
@@ -2347,6 +2348,39 @@
2347 self.failUnlessExists('a/b')2348 self.failUnlessExists('a/b')
23482349
23492350
2351class TestTransformMissingParent(tests.TestCaseWithTransport):
2352
2353 def make_tt_with_versioned_dir(self):
2354 wt = self.make_branch_and_tree('.')
2355 self.build_tree(['dir/',])
2356 wt.add(['dir'], ['dir-id'])
2357 wt.commit('Create dir')
2358 tt = TreeTransform(wt)
2359 self.addCleanup(tt.finalize)
2360 return wt, tt
2361
2362 def test_resolve_create_parent_for_versioned_file(self):
2363 wt, tt = self.make_tt_with_versioned_dir()
2364 dir_tid = tt.trans_id_tree_file_id('dir-id')
2365 file_tid = tt.new_file('file', dir_tid, 'Contents', file_id='file-id')
2366 tt.delete_contents(dir_tid)
2367 tt.unversion_file(dir_tid)
2368 conflicts = resolve_conflicts(tt)
2369 # one conflict for the missing directory, one for the unversioned
2370 # parent
2371 self.assertLength(2, conflicts)
2372
2373 def test_resolve_orphan_non_versioned_file(self):
2374 wt, tt = self.make_tt_with_versioned_dir()
2375 dir_tid = tt.trans_id_tree_file_id('dir-id')
2376 tt.new_file('file', dir_tid, 'Contents')
2377 tt.delete_contents(dir_tid)
2378 tt.unversion_file(dir_tid)
2379 conflicts = resolve_conflicts(tt)
2380 # no conflicts or rather: orphaning 'file' resolve the 'dir' conflict
2381 self.assertLength(0, conflicts)
2382
2383
2350A_ENTRY = ('a-id', ('a', 'a'), True, (True, True),2384A_ENTRY = ('a-id', ('a', 'a'), True, (True, True),
2351 ('TREE_ROOT', 'TREE_ROOT'), ('a', 'a'), ('file', 'file'),2385 ('TREE_ROOT', 'TREE_ROOT'), ('a', 'a'), ('file', 'file'),
2352 (False, False))2386 (False, False))
@@ -3227,3 +3261,39 @@
3227 trans_id = tt.trans_id_tree_path('file')3261 trans_id = tt.trans_id_tree_path('file')
3228 self.assertEqual((LINES_ONE,),3262 self.assertEqual((LINES_ONE,),
3229 tt._get_parents_texts(trans_id))3263 tt._get_parents_texts(trans_id))
3264
3265
3266class TestOrphan(tests.TestCaseWithTransport):
3267
3268 # Alternative implementations may want to test:
3269 # - can't create orphan dir
3270 # - orphaning forbidden
3271 # - can't create orphan
3272
3273 def test_no_orphan_for_transform_preview(self):
3274 tree = self.make_branch_and_tree('tree')
3275 tt = transform.TransformPreview(tree)
3276 self.addCleanup(tt.finalize)
3277 self.assertRaises(NotImplementedError, tt.new_orphan, 'foo', 'bar')
3278
3279 def test_new_orphan(self):
3280 wt = self.make_branch_and_tree('.')
3281 self.build_tree(['dir/', 'dir/foo'])
3282 wt.add(['dir'], ['dir-id'])
3283 wt.commit('add dir')
3284 tt = transform.TreeTransform(wt)
3285 self.addCleanup(tt.finalize)
3286 dir_tid = tt.trans_id_tree_path('dir')
3287 foo_tid = tt.trans_id_tree_path('dir/foo')
3288 tt.delete_contents(dir_tid)
3289 tt.unversion_file(dir_tid)
3290 raw_conflicts = tt.find_conflicts()
3291 self.assertLength(1, raw_conflicts)
3292 self.assertEqual(('missing parent', 'new-1'), raw_conflicts[0])
3293 remaining_conflicts = resolve_conflicts(tt)
3294 # Yeah for resolved conflicts !
3295 self.assertLength(0, remaining_conflicts)
3296 # We have a new orphan
3297 self.assertEquals('foo.~1~', tt.final_name(foo_tid))
3298 self.assertEquals('bzr-orphans',
3299 tt.final_name(tt.final_parent(foo_tid)))
32303300
=== modified file 'bzrlib/transform.py'
--- bzrlib/transform.py 2010-10-15 14:16:12 +0000
+++ bzrlib/transform.py 2010-10-15 14:16:12 +0000
@@ -19,8 +19,11 @@
19from stat import S_ISREG, S_IEXEC19from stat import S_ISREG, S_IEXEC
20import time20import time
2121
22from bzrlib.lazy_import import lazy_import22from bzrlib import (
23lazy_import(globals(), """23 errors,
24 lazy_import,
25 )
26lazy_import.lazy_import(globals(), """
24from bzrlib import (27from bzrlib import (
25 annotate,28 annotate,
26 bencode,29 bencode,
@@ -32,6 +35,7 @@
32 multiparent,35 multiparent,
33 osutils,36 osutils,
34 revision as _mod_revision,37 revision as _mod_revision,
38 trace,
35 ui,39 ui,
36 )40 )
37""")41""")
@@ -64,7 +68,6 @@
6468
65ROOT_PARENT = "root-parent"69ROOT_PARENT = "root-parent"
6670
67
68def unique_add(map, key, value):71def unique_add(map, key, value):
69 if key in map:72 if key in map:
70 raise DuplicateKey(key=key)73 raise DuplicateKey(key=key)
@@ -759,6 +762,17 @@
759 self.create_symlink(target, trans_id)762 self.create_symlink(target, trans_id)
760 return trans_id763 return trans_id
761764
765 def new_orphan(self, trans_id, parent_id):
766 """Schedule an item to be orphaned.
767
768 When a directory is about to be removed, its children, if they are not
769 versioned are moved out of the way: they don't have a parent anymore.
770
771 :param trans_id: The trans_id of the existing item.
772 :param parent_id: The parent trans_id of the item.
773 """
774 raise NotImplementedError(self.new_orphan)
775
762 def _affected_ids(self):776 def _affected_ids(self):
763 """Return the set of transform ids affected by the transform"""777 """Return the set of transform ids affected by the transform"""
764 trans_ids = set(self._removed_id)778 trans_ids = set(self._removed_id)
@@ -1271,6 +1285,21 @@
1271 del self._limbo_children_names[trans_id]1285 del self._limbo_children_names[trans_id]
1272 delete_any(self._limbo_name(trans_id))1286 delete_any(self._limbo_name(trans_id))
12731287
1288 def new_orphan(self, trans_id, parent_id):
1289 """See TreeTransformBase.new_orphan."""
1290 # Add the orphan dir if it doesn't exist
1291 orphan_dir = 'bzr-orphans'
1292 od_id = self.trans_id_tree_path(orphan_dir)
1293 if self.final_kind(od_id) is None:
1294 self.create_directory(od_id)
1295 parent_path = self._tree_id_paths[parent_id]
1296 # Find a name that doesn't exist yet in the orphan dir
1297 actual_name = self.final_name(trans_id)
1298 new_name = _get_backup_name(actual_name, self.by_parent(), od_id, self)
1299 self.adjust_path(new_name, od_id, trans_id)
1300 trace.warning('%s has been orphaned in %s'
1301 % (joinpath(parent_path, actual_name), orphan_dir))
1302
12741303
1275class TreeTransform(DiskTreeTransform):1304class TreeTransform(DiskTreeTransform):
1276 """Represent a tree transformation.1305 """Represent a tree transformation.
@@ -1727,6 +1756,9 @@
1727 childpath = joinpath(path, child)1756 childpath = joinpath(path, child)
1728 yield self.trans_id_tree_path(childpath)1757 yield self.trans_id_tree_path(childpath)
17291758
1759 def new_orphan(self, trans_id, parent_id):
1760 raise NotImplementedError(self.new_orphan)
1761
17301762
1731class _PreviewTree(tree.Tree):1763class _PreviewTree(tree.Tree):
1732 """Partial implementation of Tree to support show_diff_trees"""1764 """Partial implementation of Tree to support show_diff_trees"""
@@ -2427,12 +2459,14 @@
2427 for child in tt.iter_tree_children(old_parent):2459 for child in tt.iter_tree_children(old_parent):
2428 tt.adjust_path(tt.final_name(child), new_parent, child)2460 tt.adjust_path(tt.final_name(child), new_parent, child)
24292461
2462
2430def _reparent_transform_children(tt, old_parent, new_parent):2463def _reparent_transform_children(tt, old_parent, new_parent):
2431 by_parent = tt.by_parent()2464 by_parent = tt.by_parent()
2432 for child in by_parent[old_parent]:2465 for child in by_parent[old_parent]:
2433 tt.adjust_path(tt.final_name(child), new_parent, child)2466 tt.adjust_path(tt.final_name(child), new_parent, child)
2434 return by_parent[old_parent]2467 return by_parent[old_parent]
24352468
2469
2436def _content_match(tree, entry, file_id, kind, target_path):2470def _content_match(tree, entry, file_id, kind, target_path):
2437 if entry.kind != kind:2471 if entry.kind != kind:
2438 return False2472 return False
@@ -2799,11 +2833,26 @@
27992833
2800 elif c_type == 'missing parent':2834 elif c_type == 'missing parent':
2801 trans_id = conflict[1]2835 trans_id = conflict[1]
2802 try:2836 if trans_id in tt._removed_contents:
2803 tt.cancel_deletion(trans_id)2837 orphans = []
2804 new_conflicts.add(('deleting parent', 'Not deleting',2838 empty = True
2805 trans_id))2839 # Find the potential orphans, stop if one item should be kept
2806 except KeyError:2840 for c in tt.by_parent()[trans_id]:
2841 if tt.final_file_id(c) is None:
2842 orphans.append(c)
2843 else:
2844 empty = False
2845 break
2846 if empty:
2847 # All children are orphans
2848 for o in orphans:
2849 tt.new_orphan(o, trans_id)
2850 else:
2851 # Cancel the directory deletion
2852 tt.cancel_deletion(trans_id)
2853 new_conflicts.add(('deleting parent', 'Not deleting',
2854 trans_id))
2855 else:
2807 create = True2856 create = True
2808 try:2857 try:
2809 tt.final_name(trans_id)2858 tt.final_name(trans_id)
28102859
=== modified file 'doc/en/release-notes/bzr-2.3.txt'
--- doc/en/release-notes/bzr-2.3.txt 2010-10-15 14:16:12 +0000
+++ doc/en/release-notes/bzr-2.3.txt 2010-10-15 14:16:12 +0000
@@ -171,6 +171,12 @@
171* ``unshelve --preview`` now can show diff in a non-ascii encoding.171* ``unshelve --preview`` now can show diff in a non-ascii encoding.
172 (Andrej A Antonov, #518916)172 (Andrej A Antonov, #518916)
173173
174* When a bzr command remove a previously versioned directory, all
175 unversioned files are moved to a 'bzr-orphans' directory at the working
176 tree root with backup names (<file>.~#~). This was previously creating
177 spurious conflicts during merge, pull or switch operations.
178 (Vincent Ladeuil, #323111)
179
174Documentation180Documentation
175*************181*************
176182
177183
=== modified file 'doc/en/whats-new/whats-new-in-2.3.txt'
--- doc/en/whats-new/whats-new-in-2.3.txt 2010-10-15 11:30:54 +0000
+++ doc/en/whats-new/whats-new-in-2.3.txt 2010-10-15 14:16:12 +0000
@@ -90,6 +90,7 @@
90 ``bzr log -vp -r mainline:annotate:bzrlib/transform.py:500``90 ``bzr log -vp -r mainline:annotate:bzrlib/transform.py:500``
91 (Aaron Bentley)91 (Aaron Bentley)
9292
93<<<<<<< TREE
93Testing/Bug reporting94Testing/Bug reporting
94*********************95*********************
9596
@@ -98,6 +99,17 @@
98 to bug reports. (Vincent Ladeuil)99 to bug reports. (Vincent Ladeuil)
99100
100101
102=======
103
104Improved conflict handling
105**************************
106
107* Deleting a versioned directory containing unversioned files will no
108 longer create a conflict. Instead, the unversioned files will be moved
109 into a 'bzr-orphans' directory at the root of the working tree.
110 (Vincent Ladeuil, #323111)
111
112>>>>>>> MERGE-SOURCE
101Documentation113Documentation
102*************114*************
103115