monotone-commits-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[Monotone-commits-diffs] net.venge.monotone: 258d377d0c3f35f4a696a125e1a


From: code
Subject: [Monotone-commits-diffs] net.venge.monotone: 258d377d0c3f35f4a696a125e1a8e6bcb3224be7
Date: Mon, 25 Jun 2012 12:43:04 +0200 (CEST)

revision:            258d377d0c3f35f4a696a125e1a8e6bcb3224be7
date:                2012-06-25T10:42:13
author:              address@hidden
branch:              net.venge.monotone
changelog:
propagate from branch 'net.venge.monotone.issue-209.file_attribute' (head 
ce0796c45c33e74a06c1d82f2b12d9261419bee2)
            to branch 'net.venge.monotone' (head 
365dd6645b59c22a23889c01a6b5e632ff2d3e8d)



manifest:
format_version "1"

new_manifest [b736f95c242bcb6d8162a2715a41c8453693ada8]

old_revision [365dd6645b59c22a23889c01a6b5e632ff2d3e8d]

delete "test/func/(imp)_merge((patch_foo_a),_(delete_foo_))"

delete "test/func/(imp)_merge((patch_foo_a),_(delete_foo_))/__driver__.lua"

delete "test/func/merge((drop_a),_(rename_a_b,_patch_b))"

delete "test/func/merge((drop_a),_(rename_a_b,_patch_b))/__driver__.lua"

delete "test/func/merge((patch_a),_(drop_a))"

delete "test/func/merge((patch_a),_(drop_a))/__driver__.lua"

delete "test/func/merge((patch_a),_(drop_a,_add_a))"

delete "test/func/merge((patch_a),_(drop_a,_add_a))/__driver__.lua"

add_dir "test/func/resolve_conflicts_dropped_modified"

add_dir "test/func/resolve_conflicts_dropped_modified_2"

add_dir "test/func/resolve_conflicts_dropped_modified_upstream_vs_local"

add_file "test/func/resolve_conflicts_dropped_modified/__driver__.lua"
 content [e4f973e6cb8e3494c4b498f68f7cd4b2a6d20fa6]

add_file "test/func/resolve_conflicts_dropped_modified/conflicts"
 content [c6a5d84c1e1345aef2a87ad2885fb6b9a03b70b9]

add_file "test/func/resolve_conflicts_dropped_modified/conflicts-orphaned"
 content [12fab989ba071fd2babef30ba3aad1744d4cb2fa]

add_file 
"test/func/resolve_conflicts_dropped_modified/conflicts-orphaned-resolved"
 content [672fd1e54221b5b6360759b9a9a41426ce36ed6d]

add_file "test/func/resolve_conflicts_dropped_modified/conflicts-recreated"
 content [26f633aeeee82440eea41f8e1747f2c4262ce9bb]

add_file 
"test/func/resolve_conflicts_dropped_modified/conflicts-recreated-resolved"
 content [5082532e55e68bc93610d1e8936fe790bb048d3d]

add_file "test/func/resolve_conflicts_dropped_modified/conflicts-resolved"
 content [dd892da237ef4f3a0ee30fa3989374a65d092d68]

add_file "test/func/resolve_conflicts_dropped_modified/show_conflicts"
 content [74f0f311dee5ce970396bed354b6ab0fd97077f0]

add_file "test/func/resolve_conflicts_dropped_modified/show_conflicts-orphaned"
 content [fbe2c5cc2c59c6fec1121e3be9469d370b9ed5cb]

add_file "test/func/resolve_conflicts_dropped_modified_2/__driver__.lua"
 content [6ac477e0740d4af2cae4bc7cb33099e98cd1fb45]

add_file 
"test/func/resolve_conflicts_dropped_modified_upstream_vs_local/__driver__.lua"
 content [d582ad7875b4a98b2a0c60e05b7cfeaa80187f5a]

add_file 
"test/func/resolve_conflicts_dropped_modified_upstream_vs_local/conflicts"
 content [0ab94901aaab08fbed7c8a74c8dd4e44dcd85f46]

patch "NEWS"
 from [8e1f0621a0612b9d910be8c3b0d87829269e72a7]
   to [bcbf710c9e118a148d777b42c0744ba89207a6a6]

patch "doc/monotone.texi"
 from [4e2c045249cb9db0d6b2b9fcfa311993ee659259]
   to [fcb6bf46ff101899bad9b7500c75716a6bc278fc]

patch "src/cmd_conflicts.cc"
 from [86bf4eb2e0608a8b9814cef8b416626b4aa0ca90]
   to [e1b7feca5acd314d85498cabedf888f5293bf30e]

patch "src/cmd_merging.cc"
 from [84b177469ecb60664be23c42c38cc649d23899a7]
   to [0a6aa03896408c4acdb7a1a9cd1682cb98c6f02a]

patch "src/merge_conflict.cc"
 from [1c09dc4a20532ce040c429d7fb5ed2a25a199856]
   to [34fbc891bc8455202d9c0d0a495beb690fd8a5a0]

patch "src/merge_content.cc"
 from [76d5a0997d9217b309d75806f8059d8f56f2ca49]
   to [662433acaf08fc1f3f3417389a9bc3b3d2bd9bf1]

patch "src/merge_content.hh"
 from [c74c14bb8031b9bf67f684d9ddfdf6624f424f01]
   to [f9a3388e5d2bd6d873d352dabf896df32827e983]

patch "src/merge_roster.cc"
 from [98297d6264f77d540fc8e1578b1ebc5b2f36ec38]
   to [5fbc50c114df22f4753f444aa137c06ea0f06195]

patch "src/merge_roster.hh"
 from [cd2da3b06f595187a27622a98580b0636aabea4b]
   to [34d382b0ad333fa0f052e8640a88c1ece03caaa9]

patch "src/roster.cc"
 from [b4cec49faa1928388c7ab0ae1e2f389b202270b0]
   to [3f81121ce80b42565e6e5e4bbe3e6186b85e9b10]

patch "test/func/automate_show_conflicts/__driver__.lua"
 from [f835c5204474b98db1ef43aead8c9c0f97014575]
   to [41adbaaa488c5b3cda193cec8d037b66368f8b55]

patch "test/func/conflict_messages/__driver__.lua"
 from [a681cb85649f5e5ce53bad9b8ef8f2300c22d9d8]
   to [33836bd943bbe8c33abff9b8597db9d4c768b2d6]

patch "test/func/resolve_conflicts_all_resolutions/__driver__.lua"
 from [0a504a8ecd385dadb9a01b7fc2c836e58dbaf6ff]
   to [e491c1be8ceb2a90b1f80f6e29a4430e4ef8a439]

patch "test/func/resolve_conflicts_read_all/__driver__.lua"
 from [c8f14848057bb6eff36844cebd24aff6caed9d08]
   to [ccc41061b22420f5b08eea1271a3b5acf6c270d2]

patch "test/func/update_with_pending_modification/__driver__.lua"
 from [03be28a852978808341205ba2e2f713ce8dcaf35]
   to [de7b5ac13bfad72bce229ee71f3ae972c741f08b]

old_revision [ce0796c45c33e74a06c1d82f2b12d9261419bee2]

add_dir "test/func/pull_branch_vs_db_check"

add_dir "test/func/revert_with_inodeprints"

add_dir "test/func/status_of_ignored"

add_file "test/func/pull_branch_vs_db_check/__driver__.lua"
 content [928dc8f35c4976ccd7501d04bcdfaa7ea656e713]

add_file "test/func/revert_with_inodeprints/__driver__.lua"
 content [d3ad794b0a265fb5fa4eee3e264eefc6931d9340]

add_file "test/func/status_of_ignored/__driver__.lua"
 content [f64919f41c4733dac61b88884b945988faa1c2b0]

patch "doc/monotone.texi"
 from [9246f666cb62944797fd980b529eb95b548aa0dd]
   to [fcb6bf46ff101899bad9b7500c75716a6bc278fc]

patch "src/cmd_ws_commit.cc"
 from [431aff612ada586c56b63247423ae72df1650b88]
   to [78e02629916d649d7eed18480e05ede5f4b19971]

patch "src/work.cc"
 from [36e5dcda8cf09c9054cb88e6165707112ba9ac03]
   to [324996c07f4fc5bda6e32d20eac172b35db759cf]

patch "src/work.hh"
 from [0ca2a0f0ab94c2421db119216af9373b81f38e61]
   to [520ff77cc8bc9655d3826388f06dda29de9292d0]

patch "test/func/netsync_key_hooks/__driver__.lua"
 from [d7fad5a70688d3db512a05ebb0e3643006f8c7e2]
   to [0c2d3f3443fa9ff90615cd5338e322baa5ad44aa]

patch "test/func/restricted_commit_with_inodeprints/__driver__.lua"
 from [b11b4fbd4ed1b88ccb61d2b9ea5347c61a360c47]
   to [48dda63d86651a117b978054e362f4ed056f3e47]
============================================================
--- NEWS	8e1f0621a0612b9d910be8c3b0d87829269e72a7
+++ NEWS	bcbf710c9e118a148d777b42c0744ba89207a6a6
@@ -24,7 +24,21 @@ XXX XXX XX XX:XX:XX UTC 201X
         - All certs for a revision are now output by 'mtn log' with
           'suspend', 'testresult', and custom certs placed under a
           a new 'Other certs' heading.
-           
+
+        - New conflict 'dropped/modified' allows explicitly resolving
+          the case of a file that is dropped on one side of a merge,
+          and modified on the other. Previously, the modifications
+          were always lost; now you have the option of re-adding the
+          file with the modifications during merge conflict
+          resolution.
+
+        - New attribute 'mtn:resolve_conflict' allows specifying a
+          persistent 'drop' conflict resolution for a dropped/modified
+          conflict. This is useful in the case where the conflict will
+          occur again in the future, for example when a file that is
+          maintained in an upstream branch is not needed, and
+          therefore dropped, in a local branch.
+        
         Bugs fixed
 
         - Monotone now compiles against Botan 1.10.x (as well as most of
============================================================
--- doc/monotone.texi	4e2c045249cb9db0d6b2b9fcfa311993ee659259
+++ doc/monotone.texi	fcb6bf46ff101899bad9b7500c75716a6bc278fc
@@ -1,4 +1,4 @@
-\input texinfo   @c -*-texinfo-*-
+0\input texinfo   @c -*-texinfo-*-
 @c %**start of header
 @setfilename monotone.info
 @settitle monotone documentation
@@ -31,7 +31,7 @@
 This manual is for the ``monotone'' distributed version control system.
 This edition documents version @value{VERSION}.
 
-Copyright @copyright{} 2003, 2004, 2011, 2012 Graydon Hoare @*
+Copyright @copyright{} 2003, 2004, 2011 Graydon Hoare @*
 Copyright @copyright{} 2004, 2005, 2006 Nathaniel Smith @*
 Copyright @copyright{} 2005 - 2010 Derek Scherger @*
 Copyright @copyright{} 2005, 2006 Daniel Carosone @*
@@ -3492,6 +3492,50 @@ @subheading Missing Root Conflict
 
 @command{conflicts} does not support resolving this conflict.
 
address@hidden Dropped/Modified file Conflict
+This conflict occurs when a file is dropped in one merge parent,
+and modified in the other.
+
address@hidden supports resolving this conflict; the possible
+resolutions are to drop the file in the result, keep the modified
+version, or keep a user supplied version.
+
+In addition, the attribute @command{mtn:resolve_conflict} may be used
+to specify a @command{drop} resolution for this
+conflict. @command{--resolve-conflicts} must be specified on the merge
+command for the attribute to be processed. Note that a
address@hidden/conflicts} file left over from a previous merge will be
+processed when @command{--resolve-conflicts} is specified; the user must
+delete such files when they are no longer needed.
+
+The attribute is useful in the case
+where the conflict will occur again in the future, for example when a
+file that is maintained in an upstream branch is not needed, and
+therefore dropped, in a local branch. The user only needs to specify
+the conflict resolution once, via the attribute.
+
+Because of the @emph{die-die-die} policy, monotone internally must
+first drop the modified file, and then add a new file with the same
+name and the desired contents. This means history is disconnected;
+when @command{mtn log} is later run for the file, it will stop at this
+merge, not showing the previous history for the file. That history is
+still there; the user can see it by starting @command{mtn log} again
+for the same file but in the parent of the merge.
+
+A special case occurs when the user re-adds the file after dropping
+it, then attempts to merge. In this case, the possible resolutions are
+to keep the re-added version, or keep a user modified version,
+replacing the re-added version (drop is
+not a valid resolution).
+
+There is no such thing as a dropped/modified directory; if the
+directory is empty, the only possible change is rename, which is
+ignored.
+
+If the directory is not empty, that creates a special case of
+dropped/modified file conflict; if the file is kept, it must also be
+renamed to an existing directory.
+
 @subheading Invalid Name Conflict
 
 Monotone reserves the name @file{_MTN} in a workspace root directory
@@ -4969,7 +5013,7 @@ @section Tree
 to be the given revision or the head of the given branch instead of the
 null revision.
 
address@hidden address@hidden mtn merge address@hidden [--message @var{string}] [--message-file @var{filename}] [--[no-]update]
address@hidden address@hidden mtn merge address@hidden [--message @var{string}] [--message-file @var{filename}] [--[no-]update] [--resolve-conflicts]
 See the online help for more options. See @ref{--update}.
 
 This command merges the ``heads'' of @var{branchname} (default the
@@ -4989,7 +5033,8 @@ @section Tree
 algorithm. The process then repeats for each additional head, using
 the result of each previous merge as an input to the next.
 
address@hidden Conflicts} can occur.
address@hidden Conflicts} for conflicts that can occur, and the use of
address@hidden
 
 @item mtn merge_into_dir [--[no-]update] @var{sourcebranch} @var{destbranch} @var{dir}
 This command takes a unique head from @var{sourcebranch} and merges it
@@ -5147,11 +5192,11 @@ @subheading Commands
 convenient way to clean up.
 @end ftable
 
address@hidden Single file conflict resolutions
address@hidden Conflict resolutions
 
-For single file conflicts, there are several possible
-resolutions. Note that @command{resolved_user_left} is used even for
-single file conflicts.
+For single and two file conflicts, there are several possible
+resolutions. In the conflicts file, @command{resolved_user_left} is
+used for single file conflicts.
 
 @ftable @command
 @item interactive address@hidden
@@ -5162,47 +5207,24 @@ @subheading Single file conflict resolut
 specified, @var{file} defaults to @file{_MTN/resolutions/<path>},
 where @file{<path>} is the path to the file that has the conflict.
 
-This inserts a @command{resolved_user_left @var{file}} conflict resolution in the
+This inserts a @command{resolved_user_left @var{file}} or
address@hidden @var{file}} conflict resolution in the
 conflicts file.
 
 @item user @var{file}
 The file contents are replaced by the contents of the specified file.
 
-This inserts a @command{resolved_user_left @var{file}} conflict resolution in the
+This inserts a @command{resolved_user_left @var{file}} or
address@hidden @var{file}} conflict resolution in the
 conflicts file.
 
 @item drop
-The file is dropped in the merge. This is useful for an orphaned file
-conflict.
-
-This inserts a @command{resolved_drop_left} conflict resolution in the
-conflicts file.
-
address@hidden rename @var{filename}
-The file is renamed. This is useful for an orphaned file conflict.
-
-This inserts a @command{resolved_rename_left @var{filename}} conflict resolution
-in the conflicts file.
-
address@hidden ftable
-
address@hidden Two file conflict resolutions
-
-For two file conflicts, the possible resolutions are:
-
address@hidden @command
address@hidden drop
 The file is dropped in the merge.
 
-This inserts a @command{resolved_drop_left} or @command{resolved_drop_right}
-conflict resolution in the conflicts file.
+This inserts a @command{resolved_drop_left} or
address@hidden conflict resolution in the conflicts
+file.
 
address@hidden keep
-The file is kept in the merge.
-
-This inserts a @command{resolved_keep_left} or @command{resolved_keep_right}
-conflict resolution in the conflicts file.
-
 @item rename @var{filename}
 The file is renamed.
 
@@ -5210,13 +5232,11 @@ @subheading Two file conflict resolution
 @command{resolved_rename_right @var{filename}} conflict resolution in
 the conflicts file.
 
address@hidden user @var{file}
-The file contents are replaced by the contents of the specified file.
-The other file in the conflict must be dropped or renamed.
address@hidden keep
+The file is kept in the merge.
 
-This inserts a @command{resolved_user_left @var{file}} or
address@hidden @var{file}} conflict resolution in the
-conflicts file.
+This inserts a @command{resolved_keep_left} or @command{resolved_keep_right}
+conflict resolution in the conflicts file.
 
 @end ftable
 
@@ -10223,6 +10243,8 @@ @section Automation
 
 @itemize
 @item
+FIXME: -- Add reporting and resolution for dropped/modified conflicts.
address@hidden
 11.0 -- Add resolution for orphaned node conflicts. Deleted
 @code{resolved_user} conflict resolution; use @code{resolved_*_left}
 for single file conflicts. Add @code{resolved_keep_left,
@@ -10435,6 +10457,52 @@ @section Automation
    right_file_id [e80910e54d0bdea1b6d295ada320b87aaf9fdc23]
 @end verbatim
 
+File dropped and modified (and possibly renamed):
address@hidden
+        conflict dropped_modified
+   ancestor_name "foo"
+ancestor_file_id [e80910e54d0bdea1b6d295ada320b87aaf9fdc23]
+       left_type "dropped file"
+        left_rev [b0d6953684d49dd6bd345c312d6a0c8fed3078ce]
+       left_name "foo"
+    left_file_id [420cde699a422f7c3d2c8951c46ddfd546db66c0]
+      right_type "modified file"
+      right_name "baz"
+   right_file_id [fe6d523f607e2f2fc0f0defad3bda0351a95a337]
address@hidden verbatim
+Here left_name, left_file_id are from left_rev, just before the file
+was dropped.
+
+File orphaned and modified (and possibly renamed):
address@hidden
+        conflict dropped_modified
+   ancestor_name "foo"
+ancestor_file_id [e80910e54d0bdea1b6d295ada320b87aaf9fdc23]
+       left_type "orphaned file"
+        left_rev [b0d6953684d49dd6bd345c312d6a0c8fed3078ce]
+       left_name "foo"
+    left_file_id [420cde699a422f7c3d2c8951c46ddfd546db66c0]
+      right_type "modified file"
+      right_name "baz"
+   right_file_id [fe6d523f607e2f2fc0f0defad3bda0351a95a337]
address@hidden verbatim
+Orphaned/modified is different from dropped/modified because the
+possible resolutions are different; orphaned requires a rename if the
+file is kept.
+
+File dropped and recreated on one side; modified on the other (and possibly renamed):
address@hidden
+        conflict dropped_modified
+   ancestor_name "foo"
+ancestor_file_id [e80910e54d0bdea1b6d295ada320b87aaf9fdc23]
+       left_type "recreated file"
+       left_name "foo"
+    left_file_id [e80910e54d0bdea1b6d295ada320b87aaf9fdc23]
+      right_type "modified file"
+      right_name "baz"
+   right_file_id [fe6d523f607e2f2fc0f0defad3bda0351a95a337]
address@hidden verbatim
+
 Invalid file name (@file{_MTN} in root directory):
 @verbatim
      conflict invalid_name
============================================================
--- src/cmd_conflicts.cc	86bf4eb2e0608a8b9814cef8b416626b4aa0ca90
+++ src/cmd_conflicts.cc	e1b7feca5acd314d85498cabedf888f5293bf30e
@@ -84,6 +84,93 @@ show_conflicts(database & db, conflicts_
         }
     }
 
+  for (std::vector<dropped_modified_conflict>::iterator i = conflicts.result.dropped_modified_conflicts.begin();
+       i != conflicts.result.dropped_modified_conflicts.end();
+       ++i)
+    {
+      dropped_modified_conflict & conflict = *i;
+
+      if (conflict.resolution.first == resolve_conflicts::none)
+        {
+          node_id nid;
+          file_path modified_name;
+
+          if (conflict.left_nid == the_null_node)
+            {
+              // left side dropped, right side modified
+              nid = conflict.right_nid;
+              conflicts.right_roster->get_name(conflict.right_nid, modified_name);
+            }
+          else
+            {
+              // left side modified, right side dropped
+              nid = conflict.left_nid;
+              conflicts.left_roster->get_name(conflict.left_nid, modified_name);
+            }
+
+          P(F("conflict: file '%s'") % modified_name);
+          if (conflict.orphaned)
+            {
+              if (conflict.left_nid == the_null_node)
+                {
+                  P(F("orphaned on the left"));
+                  P(F("modified on the right"));
+                }
+              else
+                {
+                  P(F("modified on the left"));
+                  P(F("orphaned on the right"));
+                }
+            }
+          else
+            {
+              if (conflict.left_nid == the_null_node)
+                {
+                  if (conflict.recreated == the_null_node)
+                    P(F("dropped on the left"));
+                  else
+                    P(F("dropped and recreated on the left"));
+
+                  P(F("modified on the right"));
+                }
+              else
+                {
+                  P(F("modified on the left"));
+
+                  if (conflict.recreated == the_null_node)
+                    P(F("dropped on the right"));
+                  else
+                    P(F("dropped and recreated on the right"));
+                }
+            }
+
+          switch (show_case)
+            {
+            case first:
+              P(F("possible resolutions:"));
+
+              if (conflict.recreated == the_null_node)
+                P(F("resolve_first drop"));
+
+              if (conflict.orphaned)
+                {
+                  P(F("resolve_first rename"));
+                  P(F("resolve_first user_rename \"new_content_name\" \"new_file_name\""));
+                  return;
+                }
+              else
+                {
+                  P(F("resolve_first keep"));
+                  P(F("resolve_first user \"name\""));
+                  return;
+                }
+
+            case remaining:
+              break;
+            }
+        }
+    }
+
   for (std::vector<duplicate_name_conflict>::iterator i = conflicts.result.duplicate_name_conflicts.begin();
        i != conflicts.result.duplicate_name_conflicts.end();
        ++i)
@@ -187,6 +274,8 @@ show_conflicts(database & db, conflicts_
               (*conflicts.left_roster, *conflicts.right_roster, adaptor, false, std::cout);
             conflicts.result.report_multiple_name_conflicts
               (*conflicts.left_roster, *conflicts.right_roster, adaptor, false, std::cout);
+            conflicts.result.report_dropped_modified_conflicts
+              (*conflicts.left_roster, *conflicts.right_roster, adaptor, false, std::cout);
             conflicts.result.report_attribute_conflicts
               (*conflicts.left_roster, *conflicts.right_roster, adaptor, false, std::cout);
           }
@@ -357,6 +446,62 @@ set_first_conflict(database & db,
             }
         }
 
+      for (std::vector<dropped_modified_conflict>::iterator i = conflicts.result.dropped_modified_conflicts.begin();
+           i != conflicts.result.dropped_modified_conflicts.end();
+           ++i)
+        {
+          dropped_modified_conflict & conflict = *i;
+
+          if (conflict.resolution.first == resolve_conflicts::none)
+            {
+              if ("drop" == idx(args,0)())
+                {
+                  E(args.size() == 1, origin::user, F("wrong number of arguments"));
+                  E(conflict.recreated == the_null_node, origin::user, F("recreated files may not be dropped"));
+
+                  conflict.resolution.first  = resolve_conflicts::drop;
+                }
+              else if ("keep" == idx(args,0)())
+                {
+                  E(args.size() == 1, origin::user, F("wrong number of arguments"));
+                  E(!conflict.orphaned, origin::user, F("orphaned files must be renamed"));
+
+                  conflict.resolution.first  = resolve_conflicts::keep;
+                }
+              else if ("user" == idx(args,0)())
+                {
+                  E(args.size() == 2, origin::user, F("wrong number of arguments"));
+                  E(!conflict.orphaned, origin::user, F("orphaned files must be renamed"));
+
+                  conflict.resolution.first  = resolve_conflicts::content_user;
+                  conflict.resolution.second = new_optimal_path(idx(args,1)(), false);
+                }
+              else if ("rename" == idx(args,0)())
+                {
+                  E(args.size() == 2, origin::user, F("wrong number of arguments"));
+                  E(conflict.orphaned, origin::user, F("non-orphaned files cannot be renamed"));
+
+                  conflict.resolution.first  = resolve_conflicts::rename;
+                  conflict.resolution.second = new_optimal_path(idx(args,1)(), false);
+                }
+              else if ("user_rename" == idx(args,0)())
+                {
+                  E(args.size() == 3, origin::user, F("wrong number of arguments"));
+                  E(conflict.orphaned, origin::user, F("non-orphaned files cannot be renamed"));
+
+                  conflict.resolution.first  = resolve_conflicts::content_user_rename;
+                  conflict.resolution.second = new_optimal_path(idx(args,1)(), false);
+                  conflict.rename = file_path_external(utf8(idx(args,2)(), origin::user));
+                }
+              else
+                {
+                  E(false, origin::user,
+                    F(conflict_resolution_not_supported_msg) % idx(args,0) % "dropped_modified");
+                }
+              return;
+            }
+        }
+
       for (std::vector<file_content_conflict>::iterator i = conflicts.result.file_content_conflicts.begin();
            i != conflicts.result.file_content_conflicts.end();
            ++i)
============================================================
--- src/cmd_merging.cc	84b177469ecb60664be23c42c38cc649d23899a7
+++ src/cmd_merging.cc	0a6aa03896408c4acdb7a1a9cd1682cb98c6f02a
@@ -1,5 +1,5 @@
 // Copyright (C) 2002 Graydon Hoare <address@hidden>
-//               2008, 2010 Stephen Leake <address@hidden>
+//               2008, 2010, 2012 Stephen Leake <address@hidden>
 //
 // This program is made available under the GNU GPL version 2.0 or
 // greater. See the accompanying file COPYING for details.
@@ -329,7 +329,7 @@ update(app_state & app,
                                       left_markings, right_markings, paths);
   wca.cache_roster(working_rid, working_roster);
   resolve_merge_conflicts(app.lua, app.opts, *working_roster, chosen_roster,
-                          result, wca, false);
+                          result, wca, nis, false);
 
   // Make sure it worked...
   I(result.is_clean());
@@ -719,6 +719,7 @@ void perform_merge_into_dir(app_state & 
                      right_uncommon_ancestors,
                      result);
 
+        temp_node_id_source nis;
         content_merge_database_adaptor
           dba(db, left_rid, right_rid, left_marking_map, right_marking_map);
 
@@ -728,7 +729,7 @@ void perform_merge_into_dir(app_state & 
           (app.opts, left_rid, left_roster, right_rid, right_roster, result, resolutions_given);
 
         resolve_merge_conflicts(app.lua, app.opts, left_roster, right_roster,
-                                result, dba, resolutions_given);
+                                result, dba, nis, resolutions_given);
 
         {
           dir_t moved_root = left_roster.root();
@@ -872,10 +873,11 @@ CMD(merge_into_workspace, "merge_into_wo
   map<file_id, file_path> paths;
   get_content_paths(*working_roster, paths);
 
+  temp_node_id_source nis;
   content_merge_workspace_adaptor wca(db, lca_id, lca.first,
                                       *left.second, *right.second, paths);
   wca.cache_roster(working_rid, working_roster);
-  resolve_merge_conflicts(app.lua, app.opts, *left.first, *right.first, merge_result, wca, false);
+  resolve_merge_conflicts(app.lua, app.opts, *left.first, *right.first, merge_result, wca, nis, false);
 
   // Make sure it worked...
   I(merge_result.is_clean());
@@ -983,8 +985,8 @@ show_conflicts_core (database & db,
     }
   else
     {
-      P(F("[left]  %s") % l_id);
-      P(F("[right] %s") % r_id);
+      P(F("[left]     %s") % l_id);
+      P(F("[right]    %s") % r_id);
     }
 
   if (is_ancestor(db, l_id, r_id))
@@ -1046,12 +1048,17 @@ show_conflicts_core (database & db,
       content_merge_database_adaptor adaptor(db, l_id, r_id,
                                              l_marking, r_marking);
 
-      {
-        basic_io::printer pr;
-        st.push_binary_pair(syms::ancestor, adaptor.lca.inner());
-        pr.print_stanza(st);
-        output.write(pr.buf.data(), pr.buf.size());
-      }
+      if (basic_io)
+        {
+          basic_io::printer pr;
+          st.push_binary_pair(syms::ancestor, adaptor.lca.inner());
+          pr.print_stanza(st);
+          output.write(pr.buf.data(), pr.buf.size());
+        }
+      else
+        {
+          P(F("[ancestor] %s") % adaptor.lca);
+        }
 
       // The basic_io routines in roster_merge.cc access these rosters via
       // the adaptor.
@@ -1064,6 +1071,7 @@ show_conflicts_core (database & db,
 
       result.report_orphaned_node_conflicts(*l_roster, *r_roster, adaptor, basic_io, output);
       result.report_multiple_name_conflicts(*l_roster, *r_roster, adaptor, basic_io, output);
+      result.report_dropped_modified_conflicts(*l_roster, *r_roster, adaptor, basic_io, output);
       result.report_duplicate_name_conflicts(*l_roster, *r_roster, adaptor, basic_io, output);
 
       result.report_attribute_conflicts(*l_roster, *r_roster, adaptor, basic_io, output);
@@ -1404,7 +1412,7 @@ CMD(pluck, "pluck", "", CMD_REF(workspac
   wca.cache_roster(to_rid, to_roster);
 
   resolve_merge_conflicts(app.lua, app.opts, *working_roster, *to_roster,
-                          result, wca, false);
+                          result, wca, nis, false);
 
   I(result.is_clean());
   // temporary node ids may appear
============================================================
--- src/merge_conflict.cc	1c09dc4a20532ce040c429d7fb5ed2a25a199856
+++ src/merge_conflict.cc	34fbc891bc8455202d9c0d0a495beb690fd8a5a0
@@ -1,5 +1,5 @@
 // Copyright (C) 2005 Nathaniel Smith <address@hidden>
-//               2008, 2009 Stephen Leake <address@hidden>
+//               2008, 2009, 2012 Stephen Leake <address@hidden>
 //
 // This program is made available under the GNU GPL version 2.0 or
 // greater. See the accompanying file COPYING for details.
@@ -50,6 +50,7 @@ namespace
     symbol const conflict("conflict");
     symbol const content("content");
     symbol const directory_loop("directory_loop");
+    symbol const dropped_modified("dropped_modified");
     symbol const duplicate_name("duplicate_name");
     symbol const invalid_name("invalid_name");
     symbol const left("left");
@@ -57,6 +58,7 @@ namespace
     symbol const left_attr_value("left_attr_value");
     symbol const left_file_id("left_file_id");
     symbol const left_name("left_name");
+    symbol const left_rev("left_rev");
     symbol const left_type("left_type");
     symbol const missing_root("missing_root");
     symbol const multiple_names("multiple_names");
@@ -77,6 +79,7 @@ namespace
     symbol const right_attr_value("right_attr_value");
     symbol const right_file_id("right_file_id");
     symbol const right_name("right_name");
+    symbol const right_rev("right_rev");
     symbol const right_type("right_type");
   }
 }
@@ -396,9 +399,9 @@ static void
 enum side_t {left_side, right_side};
 
 static void
-put_resolution(basic_io::stanza & st,
-               side_t side,
-               resolve_conflicts::file_resolution_t const & resolution)
+put_file_resolution(basic_io::stanza & st,
+                    side_t side,
+                    resolve_conflicts::file_resolution_t const & resolution)
 {
   // We output any resolution for any conflict; only valid resolutions
   // should get into the data structures. To enforce that, when reading
@@ -423,6 +426,20 @@ put_resolution(basic_io::stanza & st,
         }
       break;
 
+    case resolve_conflicts::content_user_rename:
+      switch (side)
+        {
+        case left_side:
+          st.push_str_pair(syms::resolved_user_left, resolution.second->as_external());
+          break;
+
+        case right_side:
+          st.push_str_pair(syms::resolved_user_right, resolution.second->as_external());
+          break;
+        }
+      // value for rename is put by caller
+      break;
+
     case resolve_conflicts::content_internal:
       st.push_symbol(syms::resolved_internal);
       break;
@@ -518,7 +535,7 @@ put_content_conflict (basic_io::stanza &
       st.push_file_pair(syms::left_name, left_name);
       st.push_file_pair(syms::right_name, right_name);
     }
-  put_resolution (st, left_side, conflict.resolution);
+  put_file_resolution (st, left_side, conflict.resolution);
 }
 
 static void
@@ -984,7 +1001,7 @@ roster_merge_result::report_orphaned_nod
 
       if (basic_io)
         {
-          put_resolution (st, left_side, conflict.resolution);
+          put_file_resolution (st, left_side, conflict.resolution);
           put_stanza (st, output);
         }
     }
@@ -1047,7 +1064,235 @@ roster_merge_result::report_multiple_nam
     }
 }
 
+void static
+push_dropped_details(content_merge_database_adaptor & db_adaptor,
+                     symbol                           rev_sym,
+                     symbol                           name_sym,
+                     symbol                           file_id_sym,
+                     revision_id                      rev_id,
+                     node_id                          nid,
+                     basic_io::stanza &               st)
+{
+  revision_id dropped_rev_id;
+  file_path   dropped_name;
+  file_id     dropped_file_id;
+  db_adaptor.get_dropped_details(rev_id, nid, dropped_rev_id, dropped_name, dropped_file_id);
+
+  st.push_binary_pair(rev_sym, dropped_rev_id.inner());
+  st.push_str_pair(name_sym, dropped_name.as_external());
+  st.push_binary_pair(file_id_sym, dropped_file_id.inner());
+}
+
 void
+roster_merge_result::report_dropped_modified_conflicts(roster_t const & left_roster,
+                                                       roster_t const & right_roster,
+                                                       content_merge_adaptor & adaptor,
+                                                       const bool basic_io,
+                                                       std::ostream & output) const
+{
+  MM(left_roster);
+  MM(right_roster);
+
+  for (size_t i = 0; i < dropped_modified_conflicts.size(); ++i)
+    {
+      dropped_modified_conflict const & conflict = dropped_modified_conflicts[i];
+      MM(conflict);
+
+      node_id nid;
+      file_path modified_name;
+
+      if (conflict.left_nid == the_null_node)
+        {
+          // left side dropped, right side modified
+          I(!roster.is_attached(conflict.right_nid));
+
+          nid = conflict.right_nid;
+          right_roster.get_name(conflict.right_nid, modified_name);
+        }
+      else
+        {
+          // left side modified, right side dropped
+          I(!roster.is_attached(conflict.left_nid));
+
+          nid = conflict.left_nid;
+          left_roster.get_name(conflict.left_nid, modified_name);
+        }
+
+      shared_ptr<roster_t const> lca_roster;
+      revision_id lca_rid;
+      file_path ancestor_name;
+
+      adaptor.get_ancestral_roster(nid, lca_rid, lca_roster);
+      lca_roster->get_name(nid, ancestor_name);
+
+      if (basic_io)
+        {
+          basic_io::stanza st;
+
+          content_merge_database_adaptor & db_adaptor (dynamic_cast<content_merge_database_adaptor &>(adaptor));
+          file_id fid;
+
+          st.push_str_pair(syms::conflict, syms::dropped_modified);
+          st.push_str_pair(syms::ancestor_name, ancestor_name.as_external());
+          db_adaptor.db.get_file_content (db_adaptor.lca, nid, fid);
+          st.push_binary_pair(syms::ancestor_file_id, fid.inner());
+
+          if (conflict.left_nid == the_null_node)
+            {
+              if (conflict.orphaned)
+                {
+                   st.push_str_pair(syms::left_type, "orphaned file");
+                   push_dropped_details(db_adaptor, syms::left_rev, syms::left_name, syms::left_file_id,
+                                        db_adaptor.left_rid, nid, st);
+                }
+              else
+                {
+                  if (conflict.recreated == the_null_node)
+                    {
+                      st.push_str_pair(syms::left_type, "dropped file");
+                      push_dropped_details(db_adaptor, syms::left_rev, syms::left_name, syms::left_file_id,
+                                           db_adaptor.left_rid, nid, st);
+                    }
+                  else
+                    {
+                      st.push_str_pair(syms::left_type, "recreated file");
+                      st.push_str_pair(syms::left_name, modified_name.as_external());
+                      db_adaptor.db.get_file_content (db_adaptor.left_rid, conflict.recreated, fid);
+                      st.push_binary_pair(syms::left_file_id, fid.inner());
+                    }
+                }
+            }
+          else
+            {
+              st.push_str_pair(syms::left_type, "modified file");
+              st.push_str_pair(syms::left_name, modified_name.as_external());
+              db_adaptor.db.get_file_content (db_adaptor.left_rid, nid, fid);
+              st.push_binary_pair(syms::left_file_id, fid.inner());
+            }
+
+          if (conflict.right_nid == the_null_node)
+            {
+              if (conflict.orphaned)
+                {
+                  st.push_str_pair(syms::right_type, "orphaned file");
+                  push_dropped_details(db_adaptor, syms::right_rev, syms::right_name, syms::right_file_id,
+                                       db_adaptor.right_rid, nid, st);
+                }
+              else
+                {
+                  if (conflict.recreated == the_null_node)
+                    {
+                      st.push_str_pair(syms::right_type, "dropped file");
+                      push_dropped_details(db_adaptor, syms::right_rev, syms::right_name, syms::right_file_id,
+                                           db_adaptor.right_rid, nid, st);
+                    }
+                  else
+                    {
+                      st.push_str_pair(syms::right_type, "recreated file");
+                      st.push_str_pair(syms::right_name, modified_name.as_external());
+                      db_adaptor.db.get_file_content (db_adaptor.right_rid, conflict.recreated, fid);
+                      st.push_binary_pair(syms::right_file_id, fid.inner());
+                    }
+                }
+            }
+          else
+            {
+              st.push_str_pair(syms::right_type, "modified file");
+              st.push_str_pair(syms::right_name, modified_name.as_external());
+              db_adaptor.db.get_file_content (db_adaptor.right_rid, nid, fid);
+              st.push_binary_pair(syms::right_file_id, fid.inner());
+            }
+
+          put_file_resolution (st, left_side, conflict.resolution);
+          if (conflict.orphaned)
+            {
+              switch (conflict.resolution.first)
+                {
+                case resolve_conflicts::none:
+                case resolve_conflicts::rename:
+                case resolve_conflicts::drop:
+                  break;
+
+                case resolve_conflicts::content_user_rename:
+                  st.push_str_pair(syms::resolved_rename_left, conflict.rename.as_external());
+                  break;
+
+                default:
+                  I(false);
+                }
+            }
+          put_stanza(st, output);
+        }
+      else
+        {
+          P(F("conflict: file '%s' from revision %s") % ancestor_name % lca_rid);
+          if (conflict.left_nid == the_null_node)
+            {
+              if (conflict.orphaned)
+                {
+                  P(F("orphaned on the left"));
+                }
+              else
+                {
+                  if (conflict.recreated == the_null_node)
+                    P(F("dropped on the left"));
+                  else
+                    P(F("dropped and recreated on the left"));
+                }
+              P(F("modified on the right, named %s") % modified_name);
+            }
+          else
+            {
+              P(F("modified on the left, named %s") % modified_name);
+              if (conflict.orphaned)
+                {
+                  P(F("orphaned on the right"));
+                }
+              else
+                {
+                  if (conflict.recreated == the_null_node)
+                    P(F("dropped on the right"));
+                  else
+                    P(F("dropped and recreated on the right"));
+                }
+            }
+
+          // We can have a resolution from a mtn:resolve_conflict attribute.
+          switch (conflict.resolution.first)
+            {
+            case resolve_conflicts::none:
+              break;
+
+            case resolve_conflicts::content_user:
+              P(F("resolution: user file '%s'") % conflict.resolution.second->as_external());
+              break;
+
+            case resolve_conflicts::content_user_rename:
+              P(F("resolution: user '%s' rename '%s'") %
+                conflict.resolution.second->as_external() %
+                conflict.rename.as_external());
+              break;
+
+            case resolve_conflicts::rename:
+              P(F("resolution: rename '%s'") % conflict.resolution.second->as_external());
+              break;
+
+            case resolve_conflicts::drop:
+              P(F("resolution: drop"));
+              break;
+
+            case resolve_conflicts::keep:
+              P(F("resolution: keep"));
+              break;
+
+            default:
+              I(false);
+            }
+        }
+    }
+}
+
+void
 roster_merge_result::report_duplicate_name_conflicts(roster_t const & left_roster,
                                                      roster_t const & right_roster,
                                                      content_merge_adaptor & adaptor,
@@ -1221,8 +1466,8 @@ roster_merge_result::report_duplicate_na
 
       if (basic_io)
         {
-          put_resolution (st, left_side, conflict.left_resolution);
-          put_resolution (st, right_side, conflict.right_resolution);
+          put_file_resolution (st, left_side, conflict.left_resolution);
+          put_file_resolution (st, right_side, conflict.right_resolution);
           put_stanza(st, output);
         }
     }
@@ -1749,6 +1994,184 @@ static void
 } // read_multiple_name_conflicts
 
 static void
+read_dropped_modified_conflict(basic_io::parser & pars,
+                               dropped_modified_conflict & conflict,
+                               roster_t const & left_roster,
+                               roster_t const & right_roster)
+{
+  string tmp;
+
+  pars.esym(syms::ancestor_name); pars.str();
+  pars.esym(syms::ancestor_file_id); pars.hex();
+
+  pars.esym(syms::left_type);
+  pars.str(tmp);
+
+  if (tmp == "dropped file")
+    {
+      pars.esym(syms::left_rev); pars.hex();
+      pars.esym(syms::left_name); pars.str();
+      pars.esym(syms::left_file_id); pars.hex();
+    }
+  else if (tmp == "orphaned file")
+    {
+      pars.esym(syms::left_rev); pars.hex();
+      pars.esym(syms::left_name); pars.str();
+      pars.esym(syms::left_file_id); pars.hex();
+
+      conflict.orphaned = true;
+    }
+  else if (tmp == "recreated file")
+    {
+      pars.esym(syms::left_name); pars.str(tmp);
+      conflict.recreated = left_roster.get_node(file_path_external(utf8(tmp, origin::internal)))->self;
+      pars.esym(syms::left_file_id); pars.hex();
+    }
+  else if (tmp == "modified file")
+    {
+      pars.esym(syms::left_name); pars.str(tmp);
+      conflict.left_nid = left_roster.get_node(file_path_external(utf8(tmp, origin::internal)))->self;
+      pars.esym(syms::left_file_id); pars.hex();
+    }
+  else
+    I(false);
+
+  pars.esym(syms::right_type);
+  pars.str(tmp);
+
+  if (tmp == "dropped file")
+    {
+      pars.esym(syms::right_rev); pars.hex();
+      pars.esym(syms::right_name); pars.str();
+      pars.esym(syms::right_file_id); pars.hex();
+    }
+  else if (tmp == "orphaned file")
+    {
+      pars.esym(syms::right_rev); pars.hex();
+      pars.esym(syms::right_name); pars.str();
+      pars.esym(syms::right_file_id); pars.hex();
+
+      conflict.orphaned = true;
+    }
+  else if (tmp == "recreated file")
+    {
+      pars.esym(syms::right_name); pars.str(tmp);
+      conflict.recreated = right_roster.get_node(file_path_external(utf8(tmp, origin::internal)))->self;
+      pars.esym(syms::right_file_id); pars.hex();
+    }
+  else if (tmp == "modified file")
+    {
+      pars.esym(syms::right_name); pars.str(tmp);
+      conflict.right_nid = right_roster.get_node(file_path_external(utf8(tmp, origin::internal)))->self;
+      pars.esym(syms::right_file_id); pars.hex();
+    }
+  else
+    I(false);
+
+  // check for a resolution
+  if ((!pars.symp (syms::conflict)) && pars.tok.in.lookahead != EOF)
+    {
+      if (pars.symp (syms::resolved_drop_left))
+        {
+          conflict.resolution.first = resolve_conflicts::drop;
+          pars.sym();
+        }
+      else if (pars.symp (syms::resolved_keep_left))
+        {
+          E(!conflict.orphaned, origin::user, F("orphaned files must be renamed"));
+
+          conflict.resolution.first = resolve_conflicts::keep;
+          pars.sym();
+        }
+      else if (pars.symp (syms::resolved_user_left))
+        {
+          conflict.resolution.first = resolve_conflicts::content_user;
+          pars.sym();
+          conflict.resolution.second = new_optimal_path(pars.token, false);
+          pars.str();
+
+          if (conflict.orphaned)
+            {
+              pars.esym (syms::resolved_rename_left);
+              conflict.resolution.first = resolve_conflicts::content_user_rename;
+              pars.str(tmp);
+              conflict.rename = file_path_external_ws(utf8(tmp, origin::user));
+            }
+        }
+      else if (pars.symp (syms::resolved_rename_left))
+        {
+          E(conflict.orphaned, origin::user, F("non-orphaned files cannot be renamed"));
+
+          conflict.resolution.first = resolve_conflicts::rename;
+          pars.sym();
+          conflict.resolution.second = new_optimal_path(pars.token, false);
+          pars.str();
+        }
+      else
+        E(false, origin::user,
+          F(conflict_resolution_not_supported_msg) % pars.token % "dropped_modified");
+    }
+} // read_dropped_modified_conflict
+
+static void
+read_dropped_modified_conflicts(basic_io::parser & pars,
+                             std::vector<dropped_modified_conflict> & conflicts,
+                             roster_t const & left_roster,
+                             roster_t const & right_roster)
+{
+  while (pars.tok.in.lookahead != EOF && pars.symp(syms::dropped_modified))
+    {
+      dropped_modified_conflict conflict(the_null_node, the_null_node);
+
+      pars.sym();
+
+      read_dropped_modified_conflict(pars, conflict, left_roster, right_roster);
+
+      conflicts.push_back(conflict);
+
+      if (pars.tok.in.lookahead != EOF)
+        pars.esym (syms::conflict);
+    }
+} // read_dropped_modified_conflicts
+static void
+validate_dropped_modified_conflicts(basic_io::parser & pars,
+                                    std::vector<dropped_modified_conflict> & conflicts,
+                                    roster_t const & left_roster,
+                                    roster_t const & right_roster)
+{
+  for (std::vector<dropped_modified_conflict>::iterator i = conflicts.begin();
+       i != conflicts.end();
+       ++i)
+    {
+      dropped_modified_conflict & merge_conflict = *i;
+      dropped_modified_conflict file_conflict (the_null_node, the_null_node);
+
+      pars.esym(syms::dropped_modified);
+
+      read_dropped_modified_conflict(pars, file_conflict, left_roster, right_roster);
+
+      // Note that we do not confirm the file ids.
+      E(merge_conflict.left_nid == file_conflict.left_nid &&
+        merge_conflict.right_nid == file_conflict.right_nid &&
+        merge_conflict.recreated == file_conflict.recreated,
+        origin::user,
+        F(conflicts_mismatch_msg));
+
+      merge_conflict.resolution = file_conflict.resolution;
+      merge_conflict.rename     = file_conflict.rename;
+
+      if (pars.tok.in.lookahead != EOF)
+        pars.esym (syms::conflict);
+      else
+        {
+          std::vector<dropped_modified_conflict>::iterator tmp = i;
+          E(++tmp == conflicts.end(), origin::user,
+             F(conflicts_mismatch_msg));
+        }
+    }
+} // validate_dropped_modified_conflicts
+
+static void
 read_duplicate_name_conflict(basic_io::parser & pars,
                              duplicate_name_conflict & conflict,
                              roster_t const & left_roster,
@@ -2110,6 +2533,7 @@ read_conflict_file_core(basic_io::parser
       // order as non-validate, below.
 
       validate_orphaned_node_conflicts(pars, result.orphaned_node_conflicts, left_roster, right_roster);
+      validate_dropped_modified_conflicts(pars, result.dropped_modified_conflicts, left_roster, right_roster);
       validate_duplicate_name_conflicts(pars, result.duplicate_name_conflicts, left_roster, right_roster);
       validate_file_content_conflicts(pars, result.file_content_conflicts, left_roster, right_roster);
     }
@@ -2122,6 +2546,7 @@ read_conflict_file_core(basic_io::parser
       read_directory_loop_conflicts(pars, result.directory_loop_conflicts, left_roster, right_roster);
       read_orphaned_node_conflicts(pars, result.orphaned_node_conflicts, left_roster, right_roster);
       read_multiple_name_conflicts(pars, result.multiple_name_conflicts, left_roster, right_roster);
+      read_dropped_modified_conflicts(pars, result.dropped_modified_conflicts, left_roster, right_roster);
       read_duplicate_name_conflicts(pars, result.duplicate_name_conflicts, left_roster, right_roster);
       read_attribute_conflicts(pars, result.attribute_conflicts, left_roster, right_roster);
       read_file_content_conflicts(pars, result.file_content_conflicts, left_roster, right_roster);
@@ -2212,6 +2637,7 @@ roster_merge_result::write_conflict_file
   report_directory_loop_conflicts(*left_roster, *right_roster, adaptor, true, output);
   report_orphaned_node_conflicts(*left_roster, *right_roster, adaptor, true, output);
   report_multiple_name_conflicts(*left_roster, *right_roster, adaptor, true, output);
+  report_dropped_modified_conflicts(*left_roster, *right_roster, adaptor, true, output);
   report_duplicate_name_conflicts(*left_roster, *right_roster, adaptor, true, output);
   report_attribute_conflicts(*left_roster, *right_roster, adaptor, true, output);
   report_file_content_conflicts(lua, *left_roster, *right_roster, adaptor, true, output);
@@ -2234,6 +2660,13 @@ parse_resolve_conflicts_opts (options co
     {
       resolutions_given = true;
 
+      if (!file_exists(system_path(opts.resolve_conflicts_file)))
+        {
+          // user may specify --resolve-conflicts to enable attr
+          // mtn:resolve_conflict, without _MTN/conflicts.
+          return;
+        }
+
       data dat;
 
       read_data (system_path(opts.resolve_conflicts_file), dat);
@@ -2362,6 +2795,199 @@ static void
 }
 
 static void
+resolve_dropped_modified_user(roster_t &                      roster,
+                              node_id   &                     nid,
+                              file_id                         modified_fid,
+                              dropped_modified_conflict const conflict,
+                              content_merge_adaptor &         adaptor,
+                              temp_node_id_source &           nis)
+{
+  // See comments in keep below on why we drop first
+  roster.drop_detached_node(nid);
+
+  file_data result_data;
+  data result_raw_data;
+  file_id result_fid;
+  read_data(*conflict.resolution.second, result_raw_data);
+
+  result_data = file_data(result_raw_data);
+  calculate_ident(result_data, result_fid);
+
+  nid = roster.create_file_node(result_fid, nis);
+
+  // User could specify no changes
+  if (result_fid != modified_fid)
+    {
+      adaptor.record_file(result_fid, result_data);
+    }
+}
+
+void
+roster_merge_result::resolve_dropped_modified_conflicts(lua_hooks & lua,
+                                                        roster_t const & left_roster,
+                                                        roster_t const & right_roster,
+                                                        content_merge_adaptor & adaptor,
+                                                        temp_node_id_source & nis)
+{
+  MM(left_roster);
+  MM(right_roster);
+  MM(this->roster); // New roster
+
+  // Conflict node is absent in the new roster
+
+  for (std::vector<dropped_modified_conflict>::const_iterator i = dropped_modified_conflicts.begin();
+       i != dropped_modified_conflicts.end();
+       ++i)
+    {
+      dropped_modified_conflict const & conflict = *i;
+      MM(conflict);
+
+      node_id   nid;
+      file_path modified_name;
+      file_id   modified_fid;
+      file_path recreated_name;
+      file_id   recreated_fid;
+
+      if (conflict.left_nid == the_null_node)
+        {
+          nid = conflict.right_nid;
+          right_roster.get_file_details(nid, modified_fid, modified_name);
+
+          if (conflict.recreated != the_null_node)
+            {
+              roster.get_file_details(conflict.recreated, recreated_fid, recreated_name);
+            }
+        }
+      else
+        {
+          nid = conflict.left_nid;
+          left_roster.get_file_details(nid, modified_fid, modified_name);
+
+          if (conflict.recreated != the_null_node)
+            {
+              roster.get_file_details(conflict.recreated, recreated_fid, recreated_name);
+            }
+        }
+
+      switch (conflict.resolution.first)
+        {
+        case resolve_conflicts::none:
+          E(false, origin::user,
+            F("no resolution provided for dropped_modifed '%s'") % modified_name);
+          break;
+
+        case resolve_conflicts::content_user:
+          P(F("replacing content of '%s' with '%s'") %
+            modified_name % conflict.resolution.second->as_external());
+          P(F("history for '%s' will be lost; see user manual Merge Conflicts section") %
+            modified_name);
+
+          if (conflict.recreated == the_null_node)
+            {
+              resolve_dropped_modified_user(roster, nid, modified_fid, conflict, adaptor, nis);
+              attach_node(lua, roster, nid, modified_name);
+            }
+          else
+            {
+              // See comments in keep below on why we drop first
+              roster.drop_detached_node(nid);
+
+              file_id result_fid;
+              file_data parent_data, result_data;
+              data result_raw_data;
+              adaptor.get_version(recreated_fid, parent_data);
+
+              read_data(*conflict.resolution.second, result_raw_data);
+
+              result_data = file_data(result_raw_data);
+              calculate_ident(result_data, result_fid);
+
+              file_t result_node = downcast_to_file_t(roster.get_node_for_update(conflict.recreated));
+              result_node->content = result_fid;
+
+              adaptor.record_file(recreated_fid, result_fid, parent_data, result_data);
+            }
+          break;
+
+        case resolve_conflicts::content_user_rename:
+          I(conflict.rename.as_external().length() != 0);
+          P(F("replacing content of '%s' (renamed to '%s') with '%s'") %
+            modified_name % conflict.rename.as_external() % conflict.resolution.second->as_external());
+          P(F("history for '%s' will be lost; see user manual Merge Conflicts section") %
+            modified_name);
+
+          resolve_dropped_modified_user(roster, nid, modified_fid, conflict, adaptor, nis);
+          attach_node(lua, roster, nid, file_path_internal (conflict.rename.as_internal()));
+          break;
+
+        case resolve_conflicts::drop:
+          P(F("dropping '%s'") % modified_name);
+
+          roster.drop_detached_node(nid);
+          break;
+
+        case resolve_conflicts::rename:
+          P(F("renaming '%s' to '%s'") % modified_name % conflict.resolution.second->as_external());
+          P(F("history for '%s' will be lost; see user manual Merge Conflicts section") %
+            modified_name);
+
+          // See comment in keep below on why we drop first.
+          roster.drop_detached_node(nid);
+          nid = roster.create_file_node(modified_fid, nis);
+          attach_node (lua, roster, nid, file_path_internal (conflict.resolution.second->as_internal()));
+          break;
+
+        case resolve_conflicts::keep:
+          if (conflict.recreated == the_null_node)
+            {
+              P(F("keeping '%s'") % modified_name);
+              P(F("history for '%s' will be lost; see user manual Merge Conflicts section") %
+                modified_name);
+
+              // We'd like to just attach_node here, but that violates a
+              // fundamental design principle of mtn; nodes are born once,
+              // and die once. If we attach here, the node is born, died,
+              // and then born again.
+              //
+              // So we have to drop the old node, and create a new node with
+              // the same contents. That loses history; 'mtn log <path>'
+              // will end here, not showing the history of the original
+              // node.
+              roster.drop_detached_node(nid);
+              nid = roster.create_file_node(modified_fid, nis);
+              attach_node (lua, roster, nid, modified_name);
+            }
+          else
+            {
+              P(F("keeping '%s' from %s") % modified_name % ((conflict.left_nid == the_null_node) ? "right" : "left"));
+              P(F("history for '%s' will be lost; see user manual Merge Conflicts section") %
+                modified_name);
+
+              roster.drop_detached_node(nid);
+
+              // keep the modified content, not the recreated content
+              file_data parent_data, result_data;
+              adaptor.get_version(recreated_fid, parent_data);
+
+              adaptor.get_version(modified_fid, result_data);
+
+              file_t result_node = downcast_to_file_t(roster.get_node_for_update(conflict.recreated));
+              result_node->content = modified_fid;
+
+              adaptor.record_file(recreated_fid, modified_fid, parent_data, result_data);
+            }
+          break;
+
+        default:
+          I(false);
+        }
+
+    } // end for
+
+  dropped_modified_conflicts.clear();
+}
+
+static void
 resolve_duplicate_name_one_side(lua_hooks & lua,
                                 resolve_conflicts::file_resolution_t const & resolution,
                                 resolve_conflicts::file_resolution_t const & other_resolution,
@@ -2433,11 +3059,6 @@ resolve_duplicate_name_one_side(lua_hook
         F("no resolution provided for duplicate_name '%s'") % name);
       break;
 
-    case resolve_conflicts::content_internal:
-      E(false, origin::user,
-        F("invalid resolution for duplicate_name '%s'") % name);
-      break;
-
     default:
       I(false);
     }
============================================================
--- src/merge_content.cc	76d5a0997d9217b309d75806f8059d8f56f2ca49
+++ src/merge_content.cc	662433acaf08fc1f3f3417389a9bc3b3d2bd9bf1
@@ -1,5 +1,5 @@
 // Copyright (C) 2008 Nathaniel Smith <address@hidden>
-//               2008, 2010 Stephen Leake <address@hidden>
+//               2008, 2010, 2012 Stephen Leake <address@hidden>
 //
 // This program is made available under the GNU GPL version 2.0 or
 // greater. See the accompanying file COPYING for details.
@@ -82,6 +82,20 @@ void
 }
 
 void
+content_merge_database_adaptor::record_file(file_id const & ident,
+                                            file_data const & data)
+{
+  L(FL("recording new file %s")
+    % ident);
+
+  transaction_guard guard(db);
+
+  db.put_file(ident, data);
+
+  guard.commit();
+}
+
+void
 content_merge_database_adaptor::record_file(file_id const & parent_ident,
                                             file_id const & merged_ident,
                                             file_data const & parent_data,
@@ -166,6 +180,37 @@ void
 }
 
 void
+content_merge_database_adaptor::get_dropped_details(revision_id & rev_id,
+                                                    node_id       nid,
+                                                    revision_id & dropped_rev_id,
+                                                    file_path   & dropped_name,
+                                                    file_id     & dropped_file_id)
+{
+  set<revision_id> parents;
+  db.get_revision_parents(rev_id, parents);
+
+  for (set<revision_id>::iterator i = parents.begin(); i != parents.end(); i++)
+    {
+      roster_t roster;
+      marking_map marking_map;
+
+      db.get_roster(*i, roster, marking_map);
+      if (roster.has_node(nid))
+        {
+          dropped_rev_id = *i;
+          roster.get_file_details(nid, dropped_file_id, dropped_name);
+          return;
+        }
+      else
+        {
+          set<revision_id> more_parents;
+          db.get_revision_parents(*i, more_parents);
+          parents.insert(more_parents.begin(), more_parents.end());
+        }
+    }
+}
+
+void
 content_merge_database_adaptor::get_version(file_id const & ident,
                                             file_data & dat) const
 {
@@ -203,6 +248,18 @@ void
 }
 
 void
+content_merge_workspace_adaptor::record_file(file_id const & id,
+                                             file_data const & data)
+{
+  L(FL("temporarily recording file %s")
+    % id);
+  // this is an insert instead of a safe_insert because it is perfectly
+  // legal (though rare) to have multiple merges resolve to the same file
+  // contents.
+  temporary_store.insert(make_pair(id, data));
+}
+
+void
 content_merge_workspace_adaptor::record_file(file_id const & parent_id,
                                              file_id const & merged_id,
                                              file_data const & parent_data,
@@ -307,6 +364,13 @@ void
 }
 
 void
+content_merge_checkout_adaptor::record_file(file_id const & ident,
+                                            file_data const & data)
+{
+  I(false);
+}
+
+void
 content_merge_checkout_adaptor::get_ancestral_roster(node_id nid,
                                                      revision_id & rid,
                                                      shared_ptr<roster_t const> & anc)
@@ -337,6 +401,13 @@ void
 }
 
 void
+content_merge_empty_adaptor::record_file(file_id const & ident,
+                                         file_data const & data)
+{
+  I(false);
+}
+
+void
 content_merge_empty_adaptor::record_file(file_id const & parent_ident,
                                          file_id const & merged_ident,
                                          file_data const & parent_data,
@@ -644,6 +715,7 @@ resolve_merge_conflicts(lua_hooks & lua,
                         roster_t const & right_roster,
                         roster_merge_result & result,
                         content_merge_adaptor & adaptor,
+                        temp_node_id_source & nis,
                         const bool resolutions_given)
 {
   if (!result.is_clean())
@@ -652,6 +724,9 @@ resolve_merge_conflicts(lua_hooks & lua,
 
       if (resolutions_given)
         {
+          // We require --resolve-conflicts to enable processing attr
+          // mtn:resolve_conflict.
+
           // If there are any conflicts for which we don't currently support
           // resolutions, give a nice error message.
           char const * const msg = "conflict resolution for %s not yet supported";
@@ -667,10 +742,13 @@ resolve_merge_conflicts(lua_hooks & lua,
           E(result.attribute_conflicts.size() == 0, origin::user,
             F(msg) % "attribute_conflicts");
 
-          // resolve the ones we can.
+          // Resolve the ones we can, if they have resolutions specified. Each
+          // conflict list is deleted once all are resolved.
           result.resolve_orphaned_node_conflicts(lua, left_roster, right_roster, adaptor);
+          result.resolve_dropped_modified_conflicts(lua, left_roster, right_roster, adaptor, nis);
           result.resolve_duplicate_name_conflicts(lua, left_roster, right_roster, adaptor);
-          result.resolve_file_content_conflicts(lua, left_roster, right_roster, adaptor);
+
+          result.resolve_file_content_conflicts (lua, left_roster, right_roster, adaptor);
         }
     }
 
@@ -682,6 +760,7 @@ resolve_merge_conflicts(lua_hooks & lua,
 
       result.report_orphaned_node_conflicts(left_roster, right_roster, adaptor, false, std::cout);
       result.report_multiple_name_conflicts(left_roster, right_roster, adaptor, false, std::cout);
+      result.report_dropped_modified_conflicts(left_roster, right_roster, adaptor, false, std::cout);
       result.report_duplicate_name_conflicts(left_roster, right_roster, adaptor, false, std::cout);
 
       result.report_attribute_conflicts(left_roster, right_roster, adaptor, false, std::cout);
@@ -753,12 +832,13 @@ interactive_merge_and_store(lua_hooks & 
                result);
 
   bool resolutions_given;
+  temp_node_id_source nis;
   content_merge_database_adaptor dba(db, left_rid, right_rid,
                                      left_marking_map, right_marking_map);
 
   parse_resolve_conflicts_opts (opts, left_rid, left_roster, right_rid, right_roster, result, resolutions_given);
 
-  resolve_merge_conflicts(lua, opts, left_roster, right_roster, result, dba, resolutions_given);
+  resolve_merge_conflicts(lua, opts, left_roster, right_roster, result, dba, nis, resolutions_given);
 
   // write new files into the db
   store_roster_merge_result(db,
@@ -777,7 +857,7 @@ store_roster_merge_result(database & db,
 {
   I(result.is_clean());
   roster_t & merged_roster = result.roster;
-  merged_roster.check_sane();
+  merged_roster.check_sane(true); // resolve conflicts can create new nodes
 
   revision_t merged_rev;
   merged_rev.made_for = made_for_database;
============================================================
--- src/merge_content.hh	c74c14bb8031b9bf67f684d9ddfdf6624f424f01
+++ src/merge_content.hh	f9a3388e5d2bd6d873d352dabf896df32827e983
@@ -1,5 +1,5 @@
 // Copyright (C) 2005 Nathaniel Smith <address@hidden>
-//               2008, 2010 Stephen Leake <address@hidden>
+//               2008, 2010, 2012 Stephen Leake <address@hidden>
 //
 // This program is made available under the GNU GPL version 2.0 or
 // greater. See the accompanying file COPYING for details.
@@ -12,6 +12,7 @@
 #define __MERGE_HH__
 
 #include "vocab.hh"
+#include "roster.hh"
 #include "rev_types.hh"
 #include "paths.hh"
 
@@ -30,7 +31,10 @@ content_merge_adaptor
                             file_data const & right_data,
                             file_data const & merged_data) = 0;
 
-  // For use when one side of the merge is dropped
+  // dropped_modified conflict resolution of keep or user creates new node
+  virtual void record_file(file_id const & ident,
+                           file_data const & data) = 0;
+
   virtual void record_file(file_id const & parent_ident,
                            file_id const & merged_ident,
                            file_data const & parent_data,
@@ -69,6 +73,9 @@ content_merge_database_adaptor
                     file_data const & right_data,
                     file_data const & merged_data);
 
+  void record_file(file_id const & ident,
+                   file_data const & data);
+
   void record_file(file_id const & parent_ident,
                    file_id const & merged_ident,
                    file_data const & parent_data,
@@ -81,6 +88,14 @@ content_merge_database_adaptor
                             revision_id & rid,
                             boost::shared_ptr<roster_t const> & anc);
 
+  // Search parents of rev_id (which must be left_rid or right_rid); return
+  // rev, file_path, and file_id for nid just before it was dropped.
+  void get_dropped_details(revision_id & rev_id,
+                           node_id       nid,
+                           revision_id & dropped_rev_id,
+                           file_path   & dropped_name,
+                           file_id     & dropped_file_id);
+
   void get_version(file_id const & ident,
                    file_data & dat) const;
 };
@@ -117,6 +132,9 @@ content_merge_workspace_adaptor
                     file_data const & right_data,
                     file_data const & merged_data);
 
+  void record_file(file_id const & ident,
+                   file_data const & data);
+
   void record_file(file_id const & parent_ident,
                    file_id const & merged_ident,
                    file_data const & parent_data,
@@ -146,6 +164,9 @@ content_merge_checkout_adaptor
                     file_data const & right_data,
                     file_data const & merged_data);
 
+  void record_file(file_id const & ident,
+                   file_data const & data);
+
   void record_file(file_id const & parent_ident,
                    file_id const & merged_ident,
                    file_data const & parent_data,
@@ -172,6 +193,9 @@ content_merge_empty_adaptor
                     file_data const & right_data,
                     file_data const & merged_data);
 
+  void record_file(file_id const & ident,
+                   file_data const & data);
+
   void record_file(file_id const & parent_ident,
                    file_id const & merged_ident,
                    file_data const & parent_data,
@@ -259,6 +283,7 @@ resolve_merge_conflicts(lua_hooks & lua,
                         roster_t const & right_roster,
                         roster_merge_result & result,
                         content_merge_adaptor & adaptor,
+                        temp_node_id_source & nis,
                         bool const resolutions_given);
 
 // traditional resolve-all-conflicts-as-you-go style merging with 3-way merge
============================================================
--- src/merge_roster.cc	98297d6264f77d540fc8e1578b1ebc5b2f36ec38
+++ src/merge_roster.cc	5fbc50c114df22f4753f444aa137c06ea0f06195
@@ -1,4 +1,4 @@
-// Copyright (C) 2008, 2010 Stephen Leake <address@hidden>
+// Copyright (C) 2008, 2010, 2012 Stephen Leake <address@hidden>
 //               2005 Nathaniel Smith <address@hidden>
 //
 // This program is made available under the GNU GPL version 2.0 or
@@ -14,6 +14,7 @@
 #include "sanity.hh"
 #include "safe_map.hh"
 #include "parallel_iter.hh"
+#include "vocab_cast.hh"
 
 #include <sstream>
 
@@ -42,6 +43,8 @@ image(resolve_conflicts::resolution_t re
       return "keep";
     case resolve_conflicts::rename:
       return "rename";
+    case resolve_conflicts::content_user_rename:
+      return "content_user_rename";
     }
   I(false); // keep compiler happy
 }
@@ -89,6 +92,23 @@ template <> void
 }
 
 template <> void
+dump(dropped_modified_conflict const & conflict, string & out)
+{
+  ostringstream oss;
+  oss << "dropped_modified_conflict on node: " <<
+    conflict.left_nid == the_null_node ? conflict.right_nid : conflict.left_nid;
+  oss << " orphaned: " << conflict.orphaned;
+  if (conflict.resolution.first != resolve_conflicts::none)
+    {
+      oss << " resolution: " << image(conflict.resolution.first);
+      oss << " new_content_name: " << conflict.resolution.second;
+      oss << " rename: " << conflict.rename;
+    }
+  oss << "\n";
+  out = oss.str();
+}
+
+template <> void
 dump(duplicate_name_conflict const & conflict, string & out)
 {
   ostringstream oss;
@@ -146,6 +166,7 @@ roster_merge_result::clear()
 
   orphaned_node_conflicts.clear();
   multiple_name_conflicts.clear();
+  dropped_modified_conflicts.clear();
   duplicate_name_conflicts.clear();
 
   attribute_conflicts.clear();
@@ -175,6 +196,7 @@ roster_merge_result::has_non_content_con
     || !directory_loop_conflicts.empty()
     || !orphaned_node_conflicts.empty()
     || !multiple_name_conflicts.empty()
+    || !dropped_modified_conflicts.empty()
     || !duplicate_name_conflicts.empty()
     || !attribute_conflicts.empty();
 }
@@ -183,6 +205,7 @@ roster_merge_result::count_supported_res
 roster_merge_result::count_supported_resolution() const
 {
   return orphaned_node_conflicts.size()
+    + dropped_modified_conflicts.size()
     + file_content_conflicts.size()
     + duplicate_name_conflicts.size();
 }
@@ -209,6 +232,7 @@ dump_conflicts(roster_merge_result const
 
   dump(result.orphaned_node_conflicts, out);
   dump(result.multiple_name_conflicts, out);
+  dump(result.dropped_modified_conflicts, out);
   dump(result.duplicate_name_conflicts, out);
 
   dump(result.attribute_conflicts, out);
@@ -316,34 +340,63 @@ namespace
                    marking_map const & markings,
                    set<revision_id> const & uncommon_ancestors,
                    roster_t const & parent_roster,
-                   roster_t & new_roster)
+                   side_t const present_in,
+                   roster_merge_result & result)
   {
     const_marking_t const & m = markings.get_marking(n->self);
     revision_id const & birth = m->birth_revision;
     if (uncommon_ancestors.find(birth) != uncommon_ancestors.end())
-      create_node_for(n, new_roster);
+      create_node_for(n, result.roster);
     else
       {
-        // In this branch we are NOT inserting the node into the new roster as it
-        // has been deleted from the other side of the merge.
-        // In this case, output a warning if there are changes to the file on the
-        // side of the merge where it still exists.
+        // The node has been deleted from the other side of the merge. If
+        // there are changes to the file on this side of the merge, insert
+        // it into the new roster, but leave it detached, so the conflict
+        // resolutions can deal with it easily. Note that attaching would be
+        // done later; see roster_merge below.
+        //
+        // We also need to look for another node with the same name; user
+        // may have already 'undeleted' the node. But we have to do that
+        // after all result nodes are created and attached.
         set<revision_id> const & content_marks = m->file_content;
-        bool found_one_ignored_content = false;
-        for (set<revision_id>::const_iterator it = content_marks.begin(); it != content_marks.end(); it++)
+        for (set<revision_id>::const_iterator it = content_marks.begin();
+             it != content_marks.end();
+             it++)
           {
             if (uncommon_ancestors.find(*it) != uncommon_ancestors.end())
               {
-                if (!found_one_ignored_content)
+                dropped_modified_conflict conflict;
+                attr_key a_key = typecast_vocab<attr_key>(utf8("mtn:resolve_conflict"));
+                attr_map_t::const_iterator i = n->attrs.find(a_key);
+
+                create_node_for(n, result.roster);
+
+                switch (present_in)
                   {
-                    file_path fp;
-                    parent_roster.get_name(n->self, fp);
-                    W(F("content changes to the file '%s'\n"
-                        "will be ignored during this merge as the file has been\n"
-                        "removed on one side of the merge.  Affected revisions include:") % fp);
+                  case left_side:
+                      conflict = dropped_modified_conflict(n->self, the_null_node);
+                    break;
+                  case right_side:
+                    conflict = dropped_modified_conflict(the_null_node, n->self);
+                    break;
                   }
-                found_one_ignored_content = true;
-                W(F("Revision: %s") % (*it));
+
+                if (i != n->attrs.end() && i->second.first)
+                  {
+                    if (i->second.second == typecast_vocab<attr_value>(utf8("drop")))
+                      {
+                        conflict.resolution.first = resolve_conflicts::drop;
+                      }
+                    else
+                      {
+                        E(false, origin::user,
+                          F("unsupported '%s' conflict resolution in mtn:resolve_conflict attribute") %
+                          i->second.first);
+                      }
+                  }
+
+                result.dropped_modified_conflicts.push_back(conflict);
+                return;
               }
           }
       }
@@ -410,14 +463,33 @@ namespace
       }
     else
       {
+        // We need this in two places
+        std::vector<dropped_modified_conflict>::iterator dropped_modified =
+          find(result.dropped_modified_conflicts.begin(),
+               result.dropped_modified_conflicts.end(),
+               nid);
+
         // orphan:
         if (!result.roster.has_node(parent))
           {
-            orphaned_node_conflict c;
-            c.nid = nid;
-            c.parent_name = make_pair(parent, name);
-            result.orphaned_node_conflicts.push_back(c);
-            return;
+            // If the orphaned node is due to the parent directory being
+            // dropped, and the orphaned node is modified, then it already
+            // has a dropped_modified conflict; add the orphaned information
+            // to that.
+
+            if (result.dropped_modified_conflicts.end() != dropped_modified)
+              {
+                dropped_modified->orphaned = true;
+                return;
+              }
+            else
+              {
+                orphaned_node_conflict c;
+                c.nid = nid;
+                c.parent_name = make_pair(parent, name);
+                result.orphaned_node_conflicts.push_back(c);
+                return;
+              }
           }
 
         dir_t p = downcast_to_dir_t(result.roster.get_node_for_update(parent));
@@ -464,6 +536,12 @@ namespace
             result.directory_loop_conflicts.push_back(c);
             return;
           }
+
+        if (result.dropped_modified_conflicts.end() != dropped_modified)
+          {
+            // conflict already entered, just don't attach
+            return;
+          }
       }
     // hey, we actually made it.  attach the node!
     result.roster.attach_node(nid, parent, name);
@@ -515,13 +593,15 @@ roster_merge(roster_t const & left_paren
           case parallel::in_left:
             insert_if_unborn(i.left_data(),
                              left_markings, left_uncommon_ancestors, left_parent,
-                             result.roster);
+                             left_side, // present_in
+                             result);
             break;
 
           case parallel::in_right:
             insert_if_unborn(i.right_data(),
                              right_markings, right_uncommon_ancestors, right_parent,
-                             result.roster);
+                             right_side, // present_in
+                             result);
             break;
 
           case parallel::in_both:
@@ -711,6 +791,27 @@ roster_merge(roster_t const & left_paren
     I(new_i == result.roster.all_nodes().end());
   }
 
+  // now we can look for dropped_modified conflicts with recreated nodes
+  for (size_t i = 0; i < result.dropped_modified_conflicts.size(); ++i)
+    {
+      dropped_modified_conflict & conflict = result.dropped_modified_conflicts[i];
+
+      file_path modified_name;
+      if (conflict.left_nid == the_null_node)
+        {
+          right_parent.get_name(conflict.right_nid, modified_name);
+        }
+      else
+        {
+          left_parent.get_name(conflict.left_nid, modified_name);
+        }
+
+      if (result.roster.has_node(modified_name))
+        {
+          conflict.recreated = result.roster.get_node(modified_name)->self;
+        }
+    }
+
   // now check for the possible global problems
   if (!result.roster.has_root())
     result.missing_root_conflict = true;
============================================================
--- src/merge_roster.hh	cd2da3b06f595187a27622a98580b0636aabea4b
+++ src/merge_roster.hh	34d382b0ad333fa0f052e8640a88c1ece03caaa9
@@ -1,5 +1,5 @@
 // Copyright (C) 2005, 2010 Nathaniel Smith <address@hidden>
-//               2008, 2009 Stephen Leake <address@hidden>
+//               2008, 2009, 2012 Stephen Leake <address@hidden>
 //
 // This program is made available under the GNU GPL version 2.0 or
 // greater. See the accompanying file COPYING for details.
@@ -29,7 +29,7 @@ namespace resolve_conflicts
 
 namespace resolve_conflicts
 {
-  enum resolution_t {none, content_user, content_internal, drop, keep, rename};
+  enum resolution_t {none, content_user, content_internal, drop, keep, rename, content_user_rename};
 
   typedef std::pair<resolve_conflicts::resolution_t, boost::shared_ptr<any_path> > file_resolution_t;
 
@@ -90,6 +90,41 @@ struct multiple_name_conflict
   std::pair<node_id, path_component> left, right;
 };
 
+// nodes with drop/modified conflicts are left detached in the resulting
+// roster, with null parent and name fields.
+struct dropped_modified_conflict
+{
+  node_id left_nid, right_nid; // the dropped side is the null node, modified is valid.
+
+  bool orphaned; // if true, the dropped side is due to a dropped parent directory
+
+  node_id recreated; // by user, or in a previous drop/modified resolution
+
+  resolve_conflicts::file_resolution_t resolution;
+  file_path rename;
+  // if orphaned is true, the resolutions are 'drop' and 'user rename'; the
+  // latter requires two paths; content in resolution->second, filename in
+  // rename.
+
+  dropped_modified_conflict(node_id left_nid, node_id right_nid) :
+    left_nid(left_nid),
+    right_nid(right_nid),
+    orphaned(false),
+    recreated(the_null_node)
+    // rename is implicitly null
+  {resolution.first = resolve_conflicts::none;}
+
+  dropped_modified_conflict() :
+    left_nid(the_null_node),
+    right_nid(the_null_node),
+    orphaned(false),
+    recreated(the_null_node)
+    // rename is implicitly null
+  {resolution.first = resolve_conflicts::none;}
+
+  bool operator==(node_id n) {return left_nid == n || right_nid == n;}
+};
+
 // this is when two distinct nodes want to have the same name.  these nodes
 // always each merged their names cleanly.  the nodes in the resulting roster
 // are both detached.
@@ -152,6 +187,7 @@ template <> void dump(multiple_name_conf
 
 template <> void dump(orphaned_node_conflict const & conflict, std::string & out);
 template <> void dump(multiple_name_conflict const & conflict, std::string & out);
+template <> void dump(dropped_modified_conflict const & conflict, std::string & out);
 template <> void dump(duplicate_name_conflict const & conflict, std::string & out);
 
 template <> void dump(attribute_conflict const & conflict, std::string & out);
@@ -166,16 +202,18 @@ struct roster_merge_result
   //   - duplicate name conflicts
   //   - orphaned node conflicts
   //   - multiple name conflicts
+  //   - drop/modified conflicts
   //   - directory loop conflicts
   // - attribute conflicts
   // - file content conflicts
 
-  bool missing_root_conflict;
+  bool missing_root_conflict; // there can only be one of these
   std::vector<invalid_name_conflict> invalid_name_conflicts;
   std::vector<directory_loop_conflict> directory_loop_conflicts;
 
   std::vector<orphaned_node_conflict> orphaned_node_conflicts;
   std::vector<multiple_name_conflict> multiple_name_conflicts;
+  std::vector<dropped_modified_conflict> dropped_modified_conflicts;
   std::vector<duplicate_name_conflict> duplicate_name_conflicts;
 
   std::vector<attribute_conflict> attribute_conflicts;
@@ -223,6 +261,17 @@ struct roster_merge_result
                                       bool const basic_io,
                                       std::ostream & output) const;
 
+  void report_dropped_modified_conflicts(roster_t const & left,
+                                         roster_t const & right,
+                                         content_merge_adaptor & adaptor,
+                                         bool const basic_io,
+                                         std::ostream & output) const;
+  void resolve_dropped_modified_conflicts(lua_hooks & lua,
+                                          roster_t const & left_roster,
+                                          roster_t const & right_roster,
+                                          content_merge_adaptor & adaptor,
+                                          temp_node_id_source & nis);
+
   void report_duplicate_name_conflicts(roster_t const & left,
                                        roster_t const & right,
                                        content_merge_adaptor & adaptor,
============================================================
--- src/roster.cc	b4cec49faa1928388c7ab0ae1e2f389b202270b0
+++ src/roster.cc	3f81121ce80b42565e6e5e4bbe3e6186b85e9b10
@@ -1191,6 +1191,8 @@ roster_t::check_sane(bool temp_nodes_ok)
 void
 roster_t::check_sane(bool temp_nodes_ok) const
 {
+  MM(*this);
+
   node_id parent_id(the_null_node);
   const_dir_t parent_dir;
   I(old_locations.empty());
============================================================
--- test/func/(imp)_merge((patch_foo_a),_(delete_foo_))/__driver__.lua	7369a583460c9839c752d40173538c32ea635a0a
+++ /dev/null	
@@ -1,24 +0,0 @@
-
-mtn_setup()
-
-mkdir("foo")
-addfile("foo/a", "blah blah")
-commit()
-base = base_revision()
-
-check(mtn("drop", "--bookkeep-only", "--recursive", "foo"), 0, false, false)
-commit()
-
-remove("foo")
-revert_to(base)
-
-writefile("foo/a", "some other stuff")
-commit()
-
-check(mtn("--branch=testbranch", "merge"), 0, false, false)
-
-check(mtn("checkout", "--revision", base, "test_dir"), 0, false, false)
-check(indir("test_dir", mtn("update", "--branch=testbranch")), 0, false, false)
-
-check(not exists("test_dir/foo/a"))
-check(not exists("test_dir/bar/a"))
============================================================
--- test/func/automate_show_conflicts/__driver__.lua	f835c5204474b98db1ef43aead8c9c0f97014575
+++ test/func/automate_show_conflicts/__driver__.lua	41adbaaa488c5b3cda193cec8d037b66368f8b55
@@ -1,6 +1,9 @@
 -- Create the various non-content conflict cases, check that
 -- 'automate show_conflict' displays them properly.
 --
+-- Except for dropped/modified; see
+-- ../resolve_conflicts_dropped_modified/__driver__.lua
+--
 -- Cases are created in the same way as in conflict_messages/__driver__.lua
 
 mtn_setup()
============================================================
--- test/func/conflict_messages/__driver__.lua	a681cb85649f5e5ce53bad9b8ef8f2300c22d9d8
+++ test/func/conflict_messages/__driver__.lua	33836bd943bbe8c33abff9b8597db9d4c768b2d6
@@ -1,7 +1,10 @@ mtn_setup()
 mtn_setup()
 
--- this test creates the various non-content conflict cases
--- and attempts to merge them to check the various messages
+-- this test creates the various non-content conflict cases and
+-- attempts to merge them to check the various messages.
+--
+-- Except for dropped/modified; see
+-- ../resolve_conflicts_dropped_modified/__driver__.lua
 
 
 function setup(branch)
============================================================
--- test/func/merge((drop_a),_(rename_a_b,_patch_b))/__driver__.lua	7c6a662fa620c7515804cb6e5e513549429015c5
+++ /dev/null	
@@ -1,27 +0,0 @@
-
-mtn_setup()
-
-writefile("original", "some stuff here")
-
-check(mtn("add", "original"), 0, false, false)
-commit()
-base = base_revision()
-
--- drop it
-check(mtn("drop", "--bookkeep-only", "original"), 0, false, false)
-commit()
-
-revert_to(base)
-
--- patch and rename it
-rename("original", "different")
-check(mtn("rename", "--bookkeep-only", "original", "different"), 0, false, false)
-append("different", "more\n")
-commit()
-
-check(mtn("merge"), 0, false, false)
-check(mtn("checkout", "-b", "testbranch", "clean"), 0, false, false)
-
--- check that the file doesn't exist
-check(not exists("clean/original"))
-check(not exists("clean/different"))
============================================================
--- test/func/merge((patch_a),_(drop_a))/__driver__.lua	336ee81dc87b3f5843cfcbc94b0e2c899f965523
+++ /dev/null	
@@ -1,27 +0,0 @@
-
-mtn_setup()
-
-writefile("base", "foo blah")
-writefile("left", "bar blah")
-
-copy("base", "testfile")
-check(mtn("add", "testfile"), 0, false, false)
-commit()
-base = base_revision()
-
-copy("left", "testfile")
-commit()
-left = base_revision()
-
-revert_to(base)
-
-check(mtn("drop", "testfile"), 0, false, false)
-commit()
-
-check(mtn("merge"), 0, false, true)
-
--- check that we're warned about the changes being dropped...
-
-check(qgrep("content changes to the file", "stderr"))
-check(qgrep(left, "stderr"))
-
============================================================
--- test/func/merge((patch_a),_(drop_a,_add_a))/__driver__.lua	9fa8b98418f195441770ae5069b8417245ace48f
+++ /dev/null	
@@ -1,31 +0,0 @@
-
-mtn_setup()
-
--- In this case, the patch should be completely ignored; we shouldn't
--- even try to do a merge.
-
-writefile("base", "foo blah")
-writefile("left", "bar blah")
-writefile("new_right", "baz blah")
-
-copy("base", "testfile")
-check(mtn("add", "testfile"), 0, false, false)
-commit()
-base = base_revision()
-
-copy("left", "testfile")
-commit()
-
-revert_to(base)
-
-remove("testfile")
-check(mtn("drop", "--bookkeep-only", "testfile"), 0, false, false)
-commit()
-
-copy("new_right", "testfile")
-check(mtn("add", "testfile"), 0, false, false)
-commit()
-
-check(mtn("merge"), 0, false, false)
-check(mtn("update"), 0, false, false)
-check(samefile("testfile", "new_right"))
============================================================
--- test/func/resolve_conflicts_all_resolutions/__driver__.lua	0a504a8ecd385dadb9a01b7fc2c836e58dbaf6ff
+++ test/func/resolve_conflicts_all_resolutions/__driver__.lua	e491c1be8ceb2a90b1f80f6e29a4430e4ef8a439
@@ -1,5 +1,8 @@
 -- Test showing and setting all possible conflict resolutions in a
 -- conflict file. Also test 'conflict show_remaining'.
+--
+-- Except for dropped/modified; see
+-- ../resolve_conflicts_dropped_modified/__driver__.lua
 
 mtn_setup()
 get("merge3_hook.lua")
============================================================
--- /dev/null	
+++ test/func/resolve_conflicts_dropped_modified/__driver__.lua	e4f973e6cb8e3494c4b498f68f7cd4b2a6d20fa6
@@ -0,0 +1,366 @@
+-- Test reporting and resolving drop/modified conflicts
+--
+-- other resolve_conflicts_dropped_modified_* tests validate resolving
+-- in extended use cases.
+
+mtn_setup()
+
+-- Create conflicts; modify and rename file in one head, drop in
+-- other.
+-- 
+-- Six conflicts to test the three possible resolutions, with drop on
+-- both left and right. Number in file name is the node number (helps
+-- in debugging; node 1 is the root directory).
+--
+-- The case of a modified file in a dropped directory is tested below.
+
+addfile("file_2", "file_2 base") -- modify/rename left, drop right; drop
+addfile("file_3", "file_3 base") -- drop left, modify/rename right; drop
+addfile("file_4", "file_4 base") -- modify left; modify, rename, and drop right; keep
+addfile("file_5", "file_5 base") -- modify, rename, and drop left; modify right; keep
+addfile("file_6", "file_6 base") -- modify/rename left, drop right; user
+addfile("file_7", "file_7 base") -- drop left, modify/rename right; user
+commit("testbranch", "base")
+base = base_revision()
+
+writefile("file_2", "file_2 left")
+check(mtn("mv", "file_2", "file_2_renamed"), 0, false, false)
+
+check(mtn("drop", "file_3"), 0, false, false)
+
+writefile("file_4", "file_4 left")
+
+writefile("file_5", "file_5 left")
+check(mtn("mv", "file_5", "file_5_renamed"), 0, false, false)
+
+writefile("file_6", "file_6 left")
+check(mtn("mv", "file_6", "file_6_renamed"), 0, false, false)
+
+check(mtn("drop", "file_7"), 0, false, false)
+
+commit("testbranch", "left 1a")
+
+check(mtn("drop", "file_5_renamed"), 0, nil, true)
+commit("testbranch", "left 1b")
+left_1 = base_revision()
+
+revert_to(base)
+
+check(mtn("drop", "file_2"), 0, false, false)
+
+writefile("file_3", "file_3 right")
+check(mtn("mv", "file_3", "file_3_renamed"), 0, false, false)
+
+writefile("file_4", "file_4 right")
+check(mtn("mv", "file_4", "file_4_renamed"), 0, false, false)
+
+writefile("file_5", "file_5 right")
+
+check(mtn("drop", "file_6"), 0, false, false)
+
+writefile("file_7", "file_7 right")
+check(mtn("mv", "file_7", "file_7_renamed"), 0, false, false)
+
+commit("testbranch", "right 1a")
+
+check(mtn("drop", "file_4_renamed"), 0, false, false)
+commit("testbranch", "right 1b")
+right_1 = base_revision()
+
+-- Now start the conflict resolution process. First show the conflicts.
+check(mtn("show_conflicts", left_1, right_1), 0, nil, true)
+canonicalize("stderr")
+check(samefilestd("show_conflicts", "stderr"))
+
+check(mtn("automate", "show_conflicts", left_1, right_1), 0, true, nil)
+canonicalize("stdout")
+check(samefilestd("conflicts", "stdout"))
+
+-- Now store and resolve them one by one.
+check(mtn("conflicts", "store", left_1, right_1), 0, nil, true)
+check(samelines("stderr",
+{"mtn: 6 conflicts with supported resolutions.",
+ "mtn: stored in '_MTN/conflicts'"}))
+
+canonicalize("_MTN/conflicts")
+check(samefilestd("conflicts", "_MTN/conflicts"))
+
+check(mtn("conflicts", "show_first"), 0, nil, true)
+check(samelines("stderr",
+{"mtn: conflict: file 'file_2_renamed'",
+ "mtn: modified on the left",
+ "mtn: dropped on the right",
+ "mtn: possible resolutions:",
+ "mtn: resolve_first drop",
+ "mtn: resolve_first keep",
+ "mtn: resolve_first user \"name\""}))
+
+check(mtn("conflicts", "resolve_first", "drop"), 0, nil, true)
+
+-- check for nice error message if not all dropped_modified conflicts are resolved
+-- we have to use explicit_merge to get left/right to match 'conflicts store'
+check(mtn("explicit_merge", "--resolve-conflicts", left_1, right_1, "testbranch"), 1, nil, true)
+check(qgrep("no resolution provided for", "stderr"))
+             
+check(mtn("conflicts", "show_first"), 0, nil, true)
+check(samelines("stderr",
+{"mtn: conflict: file 'file_3_renamed'",
+ "mtn: dropped on the left",
+ "mtn: modified on the right",
+ "mtn: possible resolutions:",
+ "mtn: resolve_first drop",
+ "mtn: resolve_first keep",
+ "mtn: resolve_first user \"name\""}))
+
+check(mtn("conflicts", "resolve_first", "drop"), 0, nil, true)
+
+check(mtn("conflicts", "show_first"), 0, nil, true)
+check(samelines("stderr",
+{"mtn: conflict: file 'file_4'",
+ "mtn: modified on the left",
+ "mtn: dropped on the right",
+ "mtn: possible resolutions:",
+ "mtn: resolve_first drop",
+ "mtn: resolve_first keep",
+ "mtn: resolve_first user \"name\""}))
+
+check(mtn("conflicts", "resolve_first", "keep"), 0, nil, true)
+
+check(mtn("conflicts", "show_first"), 0, nil, true)
+check(samelines("stderr",
+{"mtn: conflict: file 'file_5'",
+ "mtn: dropped on the left",
+ "mtn: modified on the right",
+ "mtn: possible resolutions:",
+ "mtn: resolve_first drop",
+ "mtn: resolve_first keep",
+ "mtn: resolve_first user \"name\""}))
+
+check(mtn("conflicts", "resolve_first", "keep"), 0, nil, true)
+
+check(mtn("conflicts", "show_first"), 0, nil, true)
+check(samelines("stderr",
+{"mtn: conflict: file 'file_6_renamed'",
+ "mtn: modified on the left",
+ "mtn: dropped on the right",
+ "mtn: possible resolutions:",
+ "mtn: resolve_first drop",
+ "mtn: resolve_first keep",
+ "mtn: resolve_first user \"name\""}))
+
+mkdir("_MTN/resolutions")
+writefile("_MTN/resolutions/file_6_resolved", "file_6 resolved")
+check(mtn("conflicts", "resolve_first", "user", "_MTN/resolutions/file_6_resolved"), 0, nil, true)
+
+check(mtn("conflicts", "show_first"), 0, nil, true)
+check(samelines("stderr",
+{"mtn: conflict: file 'file_7_renamed'",
+ "mtn: dropped on the left",
+ "mtn: modified on the right",
+ "mtn: possible resolutions:",
+ "mtn: resolve_first drop",
+ "mtn: resolve_first keep",
+ "mtn: resolve_first user \"name\""}))
+
+mkdir("_MTN/resolutions")
+writefile("_MTN/resolutions/file_7_resolved", "file_7 resolved")
+check(mtn("conflicts", "resolve_first", "user", "_MTN/resolutions/file_7_resolved"), 0, nil, true)
+
+canonicalize("_MTN/conflicts")
+check(samefilestd("conflicts-resolved", "_MTN/conflicts"))
+
+-- we have to use explicit_merge to get left/right to match 'conflicts store'
+check(mtn("explicit_merge", "--resolve-conflicts", left_1, right_1, "testbranch"), 0, nil, true)
+check(qgrep("dropping 'file_2_renamed'", "stderr"))
+check(qgrep("dropping 'file_3_renamed'", "stderr"))
+check(qgrep("keeping 'file_4'", "stderr"))
+check(qgrep("keeping 'file_5'", "stderr"))
+check(qgrep("replacing content of 'file_6_renamed' with '_MTN/resolutions/file_6_resolved", "stderr"))
+check(qgrep("replacing content of 'file_7_renamed' with '_MTN/resolutions/file_7_resolved", "stderr"))
+check(not qgrep("warning", "stderr"))
+
+-- If a file is renamed (without other change) and dropped,
+-- the change is ignored:
+
+addfile("file_8", "file_8 base") -- rename left, drop right
+commit("testbranch", "base 2")
+base_2 = base_revision()
+
+check(mtn("mv", "file_8", "file_8_renamed"), 0, false, false)
+commit("testbranch", "left 2")
+left_2 = base_revision()
+
+revert_to(base_2)
+
+check(mtn("drop", "file_8"), 0, false, false)
+commit("testbranch", "right 2")
+right_2 = base_revision()
+
+check(mtn("show_conflicts", left_2, right_2), 0, nil, true)
+check(qgrep("0 conflicts", "stderr"))
+
+-- There is no such thing as a dropped/modified directory; if the
+-- directory is empty, the only possible change is rename, which is
+-- ignored.
+--
+-- If the directory is not empty, that creates a dropped/modified file
+-- conflict (not an orphaned file conflict, although that would also
+-- make sense). This used to be the test
+-- "(imp)_merge((patch_foo_a),_(delete_foo_))"
+--
+-- We create three potential conflicts; one ignored, three with different resolutions:
+
+adddir("dir1") -- empty, dropped and renamed (not a conflict; just dropped)
+mkdir("dir2")  -- not empty, dropped, contents modified
+addfile("dir2/file_9", "file_9 base") -- resolved by rename
+addfile("dir2/file_10", "file_10 base") -- resolved by user_rename
+addfile("dir2/file_11", "file_11 base") -- resolved by drop
+commit("testbranch", "base 3")
+base_3 = base_revision()
+
+check(mtn("mv", "dir1", "dir3"), 0, false, false)
+
+writefile("dir2/file_9", "file_9 left")
+writefile("dir2/file_10", "file_10 left")
+writefile("dir2/file_11", "file_11 left")
+commit("testbranch", "left 3")
+left_3 = base_revision()
+
+revert_to(base_3)
+
+check(mtn("drop", "dir1", "--no-recursive"), 0, false, false)
+
+check(mtn("drop", "--recursive", "dir2"), 0, false, false)
+
+commit("testbranch", "right 3")
+right_3 = base_revision()
+
+check(mtn("show_conflicts", left_3, right_3), 0, nil, true)
+canonicalize("stderr")
+check(samefilestd("show_conflicts-orphaned", "stderr"))
+
+-- Show these conflicts can be resolved
+check(mtn("conflicts", "store", left_3, right_3), 0, nil, true)
+
+canonicalize("_MTN/conflicts")
+check(samefilestd("conflicts-orphaned", "_MTN/conflicts"))
+
+check(mtn("conflicts", "show_first"), 0, nil, true)
+check(samelines("stderr",
+{"mtn: conflict: file 'dir2/file_10'",
+ "mtn: modified on the left",
+ "mtn: orphaned on the right",
+ "mtn: possible resolutions:",
+ "mtn: resolve_first drop",
+ "mtn: resolve_first rename",
+ "mtn: resolve_first user_rename \"new_content_name\" \"new_file_name\""}))
+
+mkdir("_MTN")
+mkdir("_MTN/resolutions")
+writefile("_MTN/resolutions/file_10", "file_10 user")
+check(mtn("conflicts", "resolve_first", "user_rename", "_MTN/resolutions/file_10", "file_10"), 0, nil, true)
+
+check(mtn("conflicts", "resolve_first", "drop"), 0, nil, nil)
+
+check(mtn("conflicts", "resolve_first", "rename", "file_9"), 0, nil, nil)
+
+check(samefilestd("conflicts-orphaned-resolved", "_MTN/conflicts"))
+
+check(mtn("explicit_merge", "--resolve-conflicts", left_3, right_3, "testbranch"), 0, nil, true)
+check(samelines("stderr",
+{"mtn: [left]  4228fbd8003cdd89e7eea51fcef10c3f91d78f69",
+ "mtn: [right] 6cb6438a490a1ad4c69ff6cac23c75a903cd9cfd",
+ "mtn: replacing content of 'dir2/file_10' (renamed to 'file_10') with '_MTN/resolutions/file_10'",
+ "mtn: history for 'dir2/file_10' will be lost; see user manual Merge Conflicts section",
+ "mtn: dropping 'dir2/file_11'",
+ "mtn: renaming 'dir2/file_9' to 'file_9'",
+ "mtn: history for 'dir2/file_9' will be lost; see user manual Merge Conflicts section",
+ "mtn: [merged] 5cafe5405ed31c81f9061be62e38f25aeaaea9c5"}))
+ 
+-- A special case; drop then re-add vs modify. This used to be the test
+-- "merge((patch_a),_(drop_a,_add_a))"
+addfile("file_10", "file_10 base") -- modify in left; drop, add in right
+addfile("file_11", "file_11 base") -- drop, add in left; modify in right
+commit("testbranch", "base 4")
+base_4 = base_revision()
+
+writefile("file_10", "file_10 left")
+
+check(mtn("drop", "file_11"), 0, false, false)
+commit("testbranch", "left 4a")
+
+addfile("file_11", "file_11 left re-add")
+commit("testbranch", "left 4b")
+left_4 = base_revision()
+
+revert_to(base_4)
+
+check(mtn("drop", "file_10"), 0, false, false)
+writefile("file_11", "file_11 right")
+commit("testbranch", "right 4a")
+
+addfile("file_10", "file_10 right re-add")
+commit("testbranch", "right 4b")
+right_4 = base_revision()
+
+check(mtn("show_conflicts", left_4, right_4), 0, nil, true)
+check(samelines("stderr",
+{"mtn: [left]     9485fe891d5e23d6dc30140228cd02840ee719e9",
+ "mtn: [right]    9a8192d3bf263cbd5782791e823b837d42af6902",
+ "mtn: [ancestor] 209e4118bda3960b2f83e48b2368e981ab748ee5",
+ "mtn: conflict: file 'file_10' from revision 209e4118bda3960b2f83e48b2368e981ab748ee5",
+ "mtn: modified on the left, named file_10",
+ "mtn: dropped and recreated on the right",
+ "mtn: conflict: file 'file_11' from revision 209e4118bda3960b2f83e48b2368e981ab748ee5",
+ "mtn: dropped and recreated on the left",
+ "mtn: modified on the right, named file_11",
+ "mtn: 2 conflicts with supported resolutions."}))
+
+check(mtn("conflicts", "store", left_4, right_4), 0, nil, true)
+check(samefilestd("conflicts-recreated", "_MTN/conflicts"))
+
+-- drop is not a valid resolution in this case
+check(mtn("conflicts", "show_first"), 0, nil, true)
+check(samelines("stderr",
+{"mtn: conflict: file 'file_10'",
+ "mtn: modified on the left",
+ "mtn: dropped and recreated on the right",
+ "mtn: possible resolutions:",
+ "mtn: resolve_first keep",
+ "mtn: resolve_first user \"name\""}))
+
+check(mtn("conflicts", "resolve_first", "drop"), 1, nil, true)
+check(samelines("stderr", {"mtn: misuse: recreated files may not be dropped"}))
+
+check(mtn("conflicts", "resolve_first", "keep"), 0, nil, nil)
+
+check(mtn("conflicts", "show_first"), 0, nil, true)
+check(samelines("stderr",
+{"mtn: conflict: file 'file_11'",
+ "mtn: dropped and recreated on the left",
+ "mtn: modified on the right",
+ "mtn: possible resolutions:",
+ "mtn: resolve_first keep",
+ "mtn: resolve_first user \"name\""}))
+
+mkdir("_MTN")
+mkdir("_MTN/resolutions")
+writefile("_MTN/resolutions/file_11", "file_11 user")
+check(mtn("conflicts", "resolve_first", "user", "_MTN/resolutions/file_11"), 0, nil, nil)
+
+check(samefilestd("conflicts-recreated-resolved", "_MTN/conflicts"))
+
+check(mtn("explicit_merge", "--resolve-conflicts", left_4, right_4, "testbranch"), 0, nil, true)
+check(samelines("stderr",
+{"mtn: [left]  9485fe891d5e23d6dc30140228cd02840ee719e9",
+ "mtn: [right] 9a8192d3bf263cbd5782791e823b837d42af6902",
+ "mtn: keeping 'file_10' from left",
+ "mtn: history for 'file_10' will be lost; see user manual Merge Conflicts section",
+ "mtn: replacing content of 'file_11' with '_MTN/resolutions/file_11'",
+ "mtn: history for 'file_11' will be lost; see user manual Merge Conflicts section",
+ "mtn: [merged] 306eb31064512a8a2f4d316ff7a7ec32a1f64f4c"}))
+
+check(mtn("update"), 0, nil, true)
+check(samelines("file_10", {"file_10 left"}))
+
+-- end of file
============================================================
--- /dev/null	
+++ test/func/resolve_conflicts_dropped_modified/conflicts	c6a5d84c1e1345aef2a87ad2885fb6b9a03b70b9
@@ -0,0 +1,69 @@
+    left [7b2ef4343b0717bcd122498a1a0b7ff7acffb64c]
+   right [ca7922b510f9daf5c4b28c6788315ee82eb9a7f0]
+ancestor [c2fe3623ce72d248154425dc7db2ddcc397c9aca]
+
+        conflict dropped_modified
+   ancestor_name "file_2"
+ancestor_file_id [4fd0fa24812427ee6c13a839d2a90bc0c6fc0091]
+       left_type "modified file"
+       left_name "file_2_renamed"
+    left_file_id [afbd804a8c606a93f9d8bc0fdacc1db9f34b4548]
+      right_type "dropped file"
+       right_rev [c2fe3623ce72d248154425dc7db2ddcc397c9aca]
+      right_name "file_2"
+   right_file_id [4fd0fa24812427ee6c13a839d2a90bc0c6fc0091]
+
+        conflict dropped_modified
+   ancestor_name "file_3"
+ancestor_file_id [311aac8e6f1fb6fca84da5153aa6d5a1c6faff79]
+       left_type "dropped file"
+        left_rev [c2fe3623ce72d248154425dc7db2ddcc397c9aca]
+       left_name "file_3"
+    left_file_id [311aac8e6f1fb6fca84da5153aa6d5a1c6faff79]
+      right_type "modified file"
+      right_name "file_3_renamed"
+   right_file_id [da7ea65160c9c92f4ed120568229342fe7daa924]
+
+        conflict dropped_modified
+   ancestor_name "file_4"
+ancestor_file_id [861174e6639f2991d9c065785cd9679be0c774f1]
+       left_type "modified file"
+       left_name "file_4"
+    left_file_id [57f26e8057760f356762c405bdc1f89b0a9bfed2]
+      right_type "dropped file"
+       right_rev [4125c3aea991e97a3063c3e3f425a47d58e7c8da]
+      right_name "file_4_renamed"
+   right_file_id [259dbd8291bd18ba3fdb9adb3776eb26f94b1230]
+
+        conflict dropped_modified
+   ancestor_name "file_5"
+ancestor_file_id [d141bda733292622ebce4c231cbb0da44ac59f40]
+       left_type "dropped file"
+        left_rev [b0d6953684d49dd6bd345c312d6a0c8fed3078ce]
+       left_name "file_5_renamed"
+    left_file_id [420cde699a422f7c3d2c8951c46ddfd546db66c0]
+      right_type "modified file"
+      right_name "file_5"
+   right_file_id [e7eb31ab48c2e42126f44ef78ffdb27f388333b0]
+
+        conflict dropped_modified
+   ancestor_name "file_6"
+ancestor_file_id [d5531643d3b5aee3e10eceabbdfecf167148a2d9]
+       left_type "modified file"
+       left_name "file_6_renamed"
+    left_file_id [1f62c734b799474943bfdda12b062f61024dc059]
+      right_type "dropped file"
+       right_rev [c2fe3623ce72d248154425dc7db2ddcc397c9aca]
+      right_name "file_6"
+   right_file_id [d5531643d3b5aee3e10eceabbdfecf167148a2d9]
+
+        conflict dropped_modified
+   ancestor_name "file_7"
+ancestor_file_id [1a9d3059360fd5f04d0cec05875c8e376da0eaef]
+       left_type "dropped file"
+        left_rev [c2fe3623ce72d248154425dc7db2ddcc397c9aca]
+       left_name "file_7"
+    left_file_id [1a9d3059360fd5f04d0cec05875c8e376da0eaef]
+      right_type "modified file"
+      right_name "file_7_renamed"
+   right_file_id [9b362e2754ea1f943497d5a31de3899271ee5a8b]
============================================================
--- /dev/null	
+++ test/func/resolve_conflicts_dropped_modified/conflicts-orphaned	12fab989ba071fd2babef30ba3aad1744d4cb2fa
@@ -0,0 +1,36 @@
+    left [4228fbd8003cdd89e7eea51fcef10c3f91d78f69]
+   right [6cb6438a490a1ad4c69ff6cac23c75a903cd9cfd]
+ancestor [44c4d408ecf65b6b45fa2c6fa2a51e5b7485d8e1]
+
+        conflict dropped_modified
+   ancestor_name "dir2/file_10"
+ancestor_file_id [7368a4340573dca149c05db6f49638fafee766d0]
+       left_type "modified file"
+       left_name "dir2/file_10"
+    left_file_id [080c590e6e671b1b9ca0e752e1bc468c5167e2a9]
+      right_type "orphaned file"
+       right_rev [44c4d408ecf65b6b45fa2c6fa2a51e5b7485d8e1]
+      right_name "dir2/file_10"
+   right_file_id [7368a4340573dca149c05db6f49638fafee766d0]
+
+        conflict dropped_modified
+   ancestor_name "dir2/file_11"
+ancestor_file_id [498b49fddbd0418f62eb19d2096de816f3e34116]
+       left_type "modified file"
+       left_name "dir2/file_11"
+    left_file_id [ea74c930ce75ac43380f520f9cdd4e85f56ed049]
+      right_type "orphaned file"
+       right_rev [44c4d408ecf65b6b45fa2c6fa2a51e5b7485d8e1]
+      right_name "dir2/file_11"
+   right_file_id [498b49fddbd0418f62eb19d2096de816f3e34116]
+
+        conflict dropped_modified
+   ancestor_name "dir2/file_9"
+ancestor_file_id [6259d1132e063364597e6ee84a431b432f0e9cf9]
+       left_type "modified file"
+       left_name "dir2/file_9"
+    left_file_id [d1c21ac76fd433b5f1daead0438938b45f9e13a8]
+      right_type "orphaned file"
+       right_rev [44c4d408ecf65b6b45fa2c6fa2a51e5b7485d8e1]
+      right_name "dir2/file_9"
+   right_file_id [6259d1132e063364597e6ee84a431b432f0e9cf9]
============================================================
--- /dev/null	
+++ test/func/resolve_conflicts_dropped_modified/conflicts-orphaned-resolved	672fd1e54221b5b6360759b9a9a41426ce36ed6d
@@ -0,0 +1,40 @@
+    left [4228fbd8003cdd89e7eea51fcef10c3f91d78f69]
+   right [6cb6438a490a1ad4c69ff6cac23c75a903cd9cfd]
+ancestor [44c4d408ecf65b6b45fa2c6fa2a51e5b7485d8e1]
+
+            conflict dropped_modified
+       ancestor_name "dir2/file_10"
+    ancestor_file_id [7368a4340573dca149c05db6f49638fafee766d0]
+           left_type "modified file"
+           left_name "dir2/file_10"
+        left_file_id [080c590e6e671b1b9ca0e752e1bc468c5167e2a9]
+          right_type "orphaned file"
+           right_rev [44c4d408ecf65b6b45fa2c6fa2a51e5b7485d8e1]
+          right_name "dir2/file_10"
+       right_file_id [7368a4340573dca149c05db6f49638fafee766d0]
+  resolved_user_left "_MTN/resolutions/file_10"
+resolved_rename_left "file_10"
+
+          conflict dropped_modified
+     ancestor_name "dir2/file_11"
+  ancestor_file_id [498b49fddbd0418f62eb19d2096de816f3e34116]
+         left_type "modified file"
+         left_name "dir2/file_11"
+      left_file_id [ea74c930ce75ac43380f520f9cdd4e85f56ed049]
+        right_type "orphaned file"
+         right_rev [44c4d408ecf65b6b45fa2c6fa2a51e5b7485d8e1]
+        right_name "dir2/file_11"
+     right_file_id [498b49fddbd0418f62eb19d2096de816f3e34116]
+resolved_drop_left 
+
+            conflict dropped_modified
+       ancestor_name "dir2/file_9"
+    ancestor_file_id [6259d1132e063364597e6ee84a431b432f0e9cf9]
+           left_type "modified file"
+           left_name "dir2/file_9"
+        left_file_id [d1c21ac76fd433b5f1daead0438938b45f9e13a8]
+          right_type "orphaned file"
+           right_rev [44c4d408ecf65b6b45fa2c6fa2a51e5b7485d8e1]
+          right_name "dir2/file_9"
+       right_file_id [6259d1132e063364597e6ee84a431b432f0e9cf9]
+resolved_rename_left "file_9"
============================================================
--- /dev/null	
+++ test/func/resolve_conflicts_dropped_modified/conflicts-recreated	26f633aeeee82440eea41f8e1747f2c4262ce9bb
@@ -0,0 +1,23 @@
+    left [9485fe891d5e23d6dc30140228cd02840ee719e9]
+   right [9a8192d3bf263cbd5782791e823b837d42af6902]
+ancestor [209e4118bda3960b2f83e48b2368e981ab748ee5]
+
+        conflict dropped_modified
+   ancestor_name "file_10"
+ancestor_file_id [7368a4340573dca149c05db6f49638fafee766d0]
+       left_type "modified file"
+       left_name "file_10"
+    left_file_id [080c590e6e671b1b9ca0e752e1bc468c5167e2a9]
+      right_type "recreated file"
+      right_name "file_10"
+   right_file_id [59db7ed2afabb782b5a0215d825a86271eb96b8d]
+
+        conflict dropped_modified
+   ancestor_name "file_11"
+ancestor_file_id [498b49fddbd0418f62eb19d2096de816f3e34116]
+       left_type "recreated file"
+       left_name "file_11"
+    left_file_id [bbf158a696465c2feb9ea22fac35ff7088f07ba0]
+      right_type "modified file"
+      right_name "file_11"
+   right_file_id [935f9a7af1da88e7fe541690076c48bac2108052]
============================================================
--- /dev/null	
+++ test/func/resolve_conflicts_dropped_modified/conflicts-recreated-resolved	5082532e55e68bc93610d1e8936fe790bb048d3d
@@ -0,0 +1,25 @@
+    left [9485fe891d5e23d6dc30140228cd02840ee719e9]
+   right [9a8192d3bf263cbd5782791e823b837d42af6902]
+ancestor [209e4118bda3960b2f83e48b2368e981ab748ee5]
+
+          conflict dropped_modified
+     ancestor_name "file_10"
+  ancestor_file_id [7368a4340573dca149c05db6f49638fafee766d0]
+         left_type "modified file"
+         left_name "file_10"
+      left_file_id [080c590e6e671b1b9ca0e752e1bc468c5167e2a9]
+        right_type "recreated file"
+        right_name "file_10"
+     right_file_id [59db7ed2afabb782b5a0215d825a86271eb96b8d]
+resolved_keep_left 
+
+          conflict dropped_modified
+     ancestor_name "file_11"
+  ancestor_file_id [498b49fddbd0418f62eb19d2096de816f3e34116]
+         left_type "recreated file"
+         left_name "file_11"
+      left_file_id [bbf158a696465c2feb9ea22fac35ff7088f07ba0]
+        right_type "modified file"
+        right_name "file_11"
+     right_file_id [935f9a7af1da88e7fe541690076c48bac2108052]
+resolved_user_left "_MTN/resolutions/file_11"
============================================================
--- /dev/null	
+++ test/func/resolve_conflicts_dropped_modified/conflicts-resolved	dd892da237ef4f3a0ee30fa3989374a65d092d68
@@ -0,0 +1,75 @@
+    left [7b2ef4343b0717bcd122498a1a0b7ff7acffb64c]
+   right [ca7922b510f9daf5c4b28c6788315ee82eb9a7f0]
+ancestor [c2fe3623ce72d248154425dc7db2ddcc397c9aca]
+
+          conflict dropped_modified
+     ancestor_name "file_2"
+  ancestor_file_id [4fd0fa24812427ee6c13a839d2a90bc0c6fc0091]
+         left_type "modified file"
+         left_name "file_2_renamed"
+      left_file_id [afbd804a8c606a93f9d8bc0fdacc1db9f34b4548]
+        right_type "dropped file"
+         right_rev [c2fe3623ce72d248154425dc7db2ddcc397c9aca]
+        right_name "file_2"
+     right_file_id [4fd0fa24812427ee6c13a839d2a90bc0c6fc0091]
+resolved_drop_left 
+
+          conflict dropped_modified
+     ancestor_name "file_3"
+  ancestor_file_id [311aac8e6f1fb6fca84da5153aa6d5a1c6faff79]
+         left_type "dropped file"
+          left_rev [c2fe3623ce72d248154425dc7db2ddcc397c9aca]
+         left_name "file_3"
+      left_file_id [311aac8e6f1fb6fca84da5153aa6d5a1c6faff79]
+        right_type "modified file"
+        right_name "file_3_renamed"
+     right_file_id [da7ea65160c9c92f4ed120568229342fe7daa924]
+resolved_drop_left 
+
+          conflict dropped_modified
+     ancestor_name "file_4"
+  ancestor_file_id [861174e6639f2991d9c065785cd9679be0c774f1]
+         left_type "modified file"
+         left_name "file_4"
+      left_file_id [57f26e8057760f356762c405bdc1f89b0a9bfed2]
+        right_type "dropped file"
+         right_rev [4125c3aea991e97a3063c3e3f425a47d58e7c8da]
+        right_name "file_4_renamed"
+     right_file_id [259dbd8291bd18ba3fdb9adb3776eb26f94b1230]
+resolved_keep_left 
+
+          conflict dropped_modified
+     ancestor_name "file_5"
+  ancestor_file_id [d141bda733292622ebce4c231cbb0da44ac59f40]
+         left_type "dropped file"
+          left_rev [b0d6953684d49dd6bd345c312d6a0c8fed3078ce]
+         left_name "file_5_renamed"
+      left_file_id [420cde699a422f7c3d2c8951c46ddfd546db66c0]
+        right_type "modified file"
+        right_name "file_5"
+     right_file_id [e7eb31ab48c2e42126f44ef78ffdb27f388333b0]
+resolved_keep_left 
+
+          conflict dropped_modified
+     ancestor_name "file_6"
+  ancestor_file_id [d5531643d3b5aee3e10eceabbdfecf167148a2d9]
+         left_type "modified file"
+         left_name "file_6_renamed"
+      left_file_id [1f62c734b799474943bfdda12b062f61024dc059]
+        right_type "dropped file"
+         right_rev [c2fe3623ce72d248154425dc7db2ddcc397c9aca]
+        right_name "file_6"
+     right_file_id [d5531643d3b5aee3e10eceabbdfecf167148a2d9]
+resolved_user_left "_MTN/resolutions/file_6_resolved"
+
+          conflict dropped_modified
+     ancestor_name "file_7"
+  ancestor_file_id [1a9d3059360fd5f04d0cec05875c8e376da0eaef]
+         left_type "dropped file"
+          left_rev [c2fe3623ce72d248154425dc7db2ddcc397c9aca]
+         left_name "file_7"
+      left_file_id [1a9d3059360fd5f04d0cec05875c8e376da0eaef]
+        right_type "modified file"
+        right_name "file_7_renamed"
+     right_file_id [9b362e2754ea1f943497d5a31de3899271ee5a8b]
+resolved_user_left "_MTN/resolutions/file_7_resolved"
============================================================
--- /dev/null	
+++ test/func/resolve_conflicts_dropped_modified/show_conflicts	74f0f311dee5ce970396bed354b6ab0fd97077f0
@@ -0,0 +1,22 @@
+mtn: [left]     7b2ef4343b0717bcd122498a1a0b7ff7acffb64c
+mtn: [right]    ca7922b510f9daf5c4b28c6788315ee82eb9a7f0
+mtn: [ancestor] c2fe3623ce72d248154425dc7db2ddcc397c9aca
+mtn: conflict: file 'file_2' from revision c2fe3623ce72d248154425dc7db2ddcc397c9aca
+mtn: modified on the left, named file_2_renamed
+mtn: dropped on the right
+mtn: conflict: file 'file_3' from revision c2fe3623ce72d248154425dc7db2ddcc397c9aca
+mtn: dropped on the left
+mtn: modified on the right, named file_3_renamed
+mtn: conflict: file 'file_4' from revision c2fe3623ce72d248154425dc7db2ddcc397c9aca
+mtn: modified on the left, named file_4
+mtn: dropped on the right
+mtn: conflict: file 'file_5' from revision c2fe3623ce72d248154425dc7db2ddcc397c9aca
+mtn: dropped on the left
+mtn: modified on the right, named file_5
+mtn: conflict: file 'file_6' from revision c2fe3623ce72d248154425dc7db2ddcc397c9aca
+mtn: modified on the left, named file_6_renamed
+mtn: dropped on the right
+mtn: conflict: file 'file_7' from revision c2fe3623ce72d248154425dc7db2ddcc397c9aca
+mtn: dropped on the left
+mtn: modified on the right, named file_7_renamed
+mtn: 6 conflicts with supported resolutions.
============================================================
--- /dev/null	
+++ test/func/resolve_conflicts_dropped_modified/show_conflicts-orphaned	fbe2c5cc2c59c6fec1121e3be9469d370b9ed5cb
@@ -0,0 +1,13 @@
+mtn: [left]     4228fbd8003cdd89e7eea51fcef10c3f91d78f69
+mtn: [right]    6cb6438a490a1ad4c69ff6cac23c75a903cd9cfd
+mtn: [ancestor] 44c4d408ecf65b6b45fa2c6fa2a51e5b7485d8e1
+mtn: conflict: file 'dir2/file_10' from revision 44c4d408ecf65b6b45fa2c6fa2a51e5b7485d8e1
+mtn: modified on the left, named dir2/file_10
+mtn: orphaned on the right
+mtn: conflict: file 'dir2/file_11' from revision 44c4d408ecf65b6b45fa2c6fa2a51e5b7485d8e1
+mtn: modified on the left, named dir2/file_11
+mtn: orphaned on the right
+mtn: conflict: file 'dir2/file_9' from revision 44c4d408ecf65b6b45fa2c6fa2a51e5b7485d8e1
+mtn: modified on the left, named dir2/file_9
+mtn: orphaned on the right
+mtn: 3 conflicts with supported resolutions.
============================================================
--- /dev/null	
+++ test/func/resolve_conflicts_dropped_modified_2/__driver__.lua	6ac477e0740d4af2cae4bc7cb33099e98cd1fb45
@@ -0,0 +1,90 @@
+-- Verify that we can resolve this extended use case involving a
+-- dropped_modified conflict:
+-- 
+--     A
+--    / \
+--   M1  D
+--   | \ |
+--   M2  P
+--    \ /
+--     Q
+--
+-- The file is modified and merged into the dropped branch twice.
+
+mtn_setup()
+
+addfile("file_2", "file_2 base") -- modify/rename left, drop right; drop
+commit("testbranch", "base")
+base = base_revision()
+
+writefile("file_2", "file_2 left 1")
+
+commit("testbranch", "left 1")
+left_1 = base_revision()
+
+revert_to(base)
+
+check(mtn("drop", "file_2"), 0, false, false)
+
+commit("testbranch", "right 1")
+right_1 = base_revision()
+
+check(mtn("show_conflicts", left_1, right_1), 0, nil, true)
+check(samelines
+("stderr",
+ {"mtn: [left]     506d8ed51b06c0080e8bb307155a88637045b532",
+  "mtn: [right]    a2889488ed1801a904d0219ec9939dfc2e9be033",
+  "mtn: [ancestor] f80ff103551d0313647d6c84990bc9db6b158dac",
+  "mtn: conflict: file 'file_2' from revision f80ff103551d0313647d6c84990bc9db6b158dac",
+  "mtn: modified on the left, named file_2",
+  "mtn: dropped on the right",
+  "mtn: 1 conflict with supported resolutions."}))
+
+check(mtn("conflicts", "store", left_1, right_1), 0, nil, true)
+
+check(mtn("conflicts", "resolve_first", "keep"), 0, nil, true)
+
+check(mtn("explicit_merge", "--resolve-conflicts", left_1, right_1, "testbranch"), 0, nil, true)
+check(samelines
+("stderr",
+ {"mtn: [left]  506d8ed51b06c0080e8bb307155a88637045b532",
+  "mtn: [right] a2889488ed1801a904d0219ec9939dfc2e9be033",
+  "mtn: keeping 'file_2'",
+  "mtn: history for 'file_2' will be lost; see user manual Merge Conflicts section",
+  "mtn: [merged] 3df3126220588440def7b08f488ca35eaa94f1b6"}))
+
+check(mtn("update"), 0, nil, true)
+check(samelines("file_2", {"file_2 left 1"}))
+
+right_2 = base_revision()
+
+-- round 2; modify the file again
+revert_to(left_1)
+
+writefile("file_2", "file_2 left 2")
+
+commit("testbranch", "left 2")
+left_2 = base_revision()
+
+check(mtn("show_conflicts", left_2, right_2), 0, nil, true)
+check(samelines
+("stderr",
+ {"mtn: [left]     5a144a43f03692e389f3ddd4c510a4d9754061d5",
+  "mtn: [right]    3df3126220588440def7b08f488ca35eaa94f1b6",
+  "mtn: [ancestor] 506d8ed51b06c0080e8bb307155a88637045b532",
+  "mtn: conflict: file 'file_2' from revision 506d8ed51b06c0080e8bb307155a88637045b532",
+  "mtn: modified on the left, named file_2",
+  "mtn: dropped and recreated on the right",
+  "mtn: 1 conflict with supported resolutions."}))
+
+check(mtn("conflicts", "store", left_2, right_2), 0, nil, true)
+
+check(mtn("conflicts", "resolve_first", "keep"), 0, nil, true)
+
+check(mtn("explicit_merge", "--resolve-conflicts", left_2, right_2, "testbranch"), 0, nil, true)
+check(qgrep("mtn: keeping 'file_2' from left", "stderr"))
+
+check(mtn("update"), 0, nil, true)
+check(samelines("file_2", {"file_2 left 2"}))
+
+-- end of file
============================================================
--- /dev/null	
+++ test/func/resolve_conflicts_dropped_modified_upstream_vs_local/__driver__.lua	d582ad7875b4a98b2a0c60e05b7cfeaa80187f5a
@@ -0,0 +1,104 @@
+-- Show a problematic use case involving a dropped_modified conflict,
+-- and how it can be resolved with the 'mtn:resolve_conflict'
+-- attribute.
+--
+-- There is an upstream branch, and a local branch. The local branch
+-- deletes a file that the upstream branch continues to modify. We
+-- periodically merge from upstream to local to get other changes, but
+-- never merge in the other direction.
+--
+-- The dropped file causes new dropped_modified conflicts at each
+-- propagate. We decided to always drop, so we apply the
+-- 'mtn:resolve_conflict' attribute.
+
+mtn_setup()
+
+addfile("file_2", "file_2 base")
+commit("testbranch", "base")
+base = base_revision()
+
+writefile("file_2", "file_2 upstream 1")
+
+commit("testbranch", "upstream 1")
+upstream_1 = base_revision()
+
+revert_to(base)
+
+check(mtn("drop", "file_2"), 0, false, false)
+
+commit("testbranch", "local 1")
+local_1 = base_revision()
+
+check(mtn("show_conflicts", upstream_1, local_1), 0, nil, true)
+check(samelines
+("stderr",
+ {"mtn: [left]     1e700864de7a2cbb1cf85c26f5e1e4ca335d2bc2",
+  "mtn: [right]    a2889488ed1801a904d0219ec9939dfc2e9be033",
+  "mtn: [ancestor] f80ff103551d0313647d6c84990bc9db6b158dac",
+  "mtn: conflict: file 'file_2' from revision f80ff103551d0313647d6c84990bc9db6b158dac",
+  "mtn: modified on the left, named file_2",
+  "mtn: dropped on the right",
+  "mtn: 1 conflict with supported resolutions."}))
+
+check(mtn("conflicts", "store", upstream_1, local_1), 0, nil, true)
+
+check(mtn("conflicts", "resolve_first", "drop"), 0, nil, true)
+
+check(mtn("explicit_merge", "--resolve-conflicts", upstream_1, local_1, "testbranch"), 0, nil, true)
+check(samelines
+("stderr",
+ {"mtn: [left]  1e700864de7a2cbb1cf85c26f5e1e4ca335d2bc2",
+  "mtn: [right] a2889488ed1801a904d0219ec9939dfc2e9be033",
+  "mtn: dropping 'file_2'",
+  "mtn: [merged] dd1ba606b52fddb4431da3760ff65b65f6509a48"}))
+
+check(mtn("update"), 0, nil, true)
+check(not exists("file_2"))
+
+local_2 = base_revision()
+
+-- round 2; upstream modifies the file again, and records the drop
+-- conflict resolution for future merges to downstream.
+revert_to(upstream_1)
+
+writefile("file_2", "file_2 upstream 2")
+
+check(mtn("attr", "set", "file_2", "mtn:resolve_conflict", "drop"), 0, nil, nil)
+
+commit("testbranch", "upstream 2")
+upstream_2 = base_revision()
+
+-- Since the attribute specifies the conflict resolution, we don't need to specify one ourselves
+
+-- We require --resolve-conflicts here, mostly for consistency with
+-- 1.0 behavior (where resolving non-content conflicts requires
+-- --resolve-conflict)
+check(mtn("merge"), 1, nil, true)
+check(qgrep("mtn: misuse: merge failed due to unresolved conflicts", "stderr"))
+
+check(mtn("merge", "--resolve-conflicts"), 0, nil, true)
+check(qgrep("mtn: dropping 'file_2'", "stderr"))
+
+check(mtn("update"), 0, nil, true)
+check(not exists("file_2"))
+
+-- Show that 'show_conflicts' reports the attribute resolution properly.
+check(mtn("show_conflicts", upstream_2, local_2), 0, nil, true)
+check(samelines
+("stderr",
+ {"mtn: [left]     c0ed8c29ffad149af1c948969e8e80d270999b13",
+  "mtn: [right]    dd1ba606b52fddb4431da3760ff65b65f6509a48",
+  "mtn: [ancestor] 1e700864de7a2cbb1cf85c26f5e1e4ca335d2bc2",
+  "mtn: conflict: file 'file_2' from revision 1e700864de7a2cbb1cf85c26f5e1e4ca335d2bc2",
+  "mtn: modified on the left, named file_2",
+  "mtn: dropped on the right",
+  "mtn: resolution: drop",
+  "mtn: 1 conflict with supported resolutions."}))
+
+-- 'conflicts store' is not needed unless there are other conflicts,
+-- or the user wants to override the attribute. Show that it reports
+-- the attr resolution properly.
+check(mtn("conflicts", "store", upstream_2, local_2), 0, nil, true)
+check(samefilestd("conflicts", "_MTN/conflicts"))
+
+-- end of file
============================================================
--- /dev/null	
+++ test/func/resolve_conflicts_dropped_modified_upstream_vs_local/conflicts	0ab94901aaab08fbed7c8a74c8dd4e44dcd85f46
@@ -0,0 +1,15 @@
+    left [c0ed8c29ffad149af1c948969e8e80d270999b13]
+   right [dd1ba606b52fddb4431da3760ff65b65f6509a48]
+ancestor [1e700864de7a2cbb1cf85c26f5e1e4ca335d2bc2]
+
+          conflict dropped_modified
+     ancestor_name "file_2"
+  ancestor_file_id [7fc990de4797bd6534a5c1deb344e11964f6b353]
+         left_type "modified file"
+         left_name "file_2"
+      left_file_id [b7e3240a78dc6afce4507f5a18ab516963e72022]
+        right_type "dropped file"
+         right_rev [1e700864de7a2cbb1cf85c26f5e1e4ca335d2bc2]
+        right_name "file_2"
+     right_file_id [7fc990de4797bd6534a5c1deb344e11964f6b353]
+resolved_drop_left 
============================================================
--- test/func/resolve_conflicts_read_all/__driver__.lua	c8f14848057bb6eff36844cebd24aff6caed9d08
+++ test/func/resolve_conflicts_read_all/__driver__.lua	ccc41061b22420f5b08eea1271a3b5acf6c270d2
@@ -4,6 +4,9 @@ mtn_setup()
 -- that "conflicts show_remaining" can properly process the output of
 -- "conflicts store".
 --
+-- Except for dropped/modified; see
+-- ../resolve_conflicts_dropped_modified/__driver__.lua
+--
 -- test case generation borrowed from conflict_messages test.
 
 
============================================================
--- test/func/update_with_pending_modification/__driver__.lua	03be28a852978808341205ba2e2f713ce8dcaf35
+++ test/func/update_with_pending_modification/__driver__.lua	de7b5ac13bfad72bce229ee71f3ae972c741f08b
@@ -1,3 +1,4 @@
+-- Test the case of update where there is a dropped/modified conflict
 
 mtn_setup()
 
@@ -5,28 +6,25 @@ commit()
 addfile("file1", "contents of file1")
 commit()
 
--- store the newly created revision id
 REV1=base_revision()
 
--- check(mtn("--branch", "testbranch", "co", "codir"), 0, false, false)
--- writefile("codir/file2", "contents of file2")
-
--- add another file and commit
+-- add another file, then change it
 addfile("file2", "contents of file2")
 commit()
 
--- change that new file
 writefile("file2", "new contents of file2")
 
--- .. and upadte to the previous revision, which didn't have file2.
--- At the moment, this simply drops file2 and all changes to it.
---
--- See bug #15058
+-- update to the previous revision, which didn't have file2. This
+-- looks like file2 is dropped on one side (in rev 1), and modified on
+-- the other (the workspace), so we get a dropped/modified conflict.
 
-xfail(mtn("update", "-r", REV1), 1, true, true)
+check(mtn("update", "-r", REV1), 1, nil, true)
+check(qgrep("mtn: conflict: file 'file2'", "stderr"))
+check(qgrep("mtn: modified on the left, named file2", "stderr"))
+check(qgrep("mtn: dropped on the right", "stderr"))
+check(qgrep("mtn: misuse: merge failed due to unresolved conflicts", "stderr"))
 
--- IMO, the correct curse of action should be to fail updating due to
--- a conflict.
-check(exists("file2"))
-check(samelines("file2", {"new contents of file2"}))
+-- Since this is a workspace merge, we can't resolve the conflict; the
+-- modified file must be committed first.
 
+-- end of file
============================================================
--- doc/monotone.texi	9246f666cb62944797fd980b529eb95b548aa0dd
+++ doc/monotone.texi	fcb6bf46ff101899bad9b7500c75716a6bc278fc
@@ -1,4 +1,4 @@
-\input texinfo   @c -*-texinfo-*-
+0\input texinfo   @c -*-texinfo-*-
 @c %**start of header
 @setfilename monotone.info
 @settitle monotone documentation
@@ -3251,7 +3251,8 @@ @section Merge Conflicts
 with new conflicts and resolutions, to merge the remaining heads.
 
 For the special case of file content conflicts, a merge command
-invoked without @option{--resolve-conflicts} will attempt to start an
+invoked without @option{--resolve-conflicts} will attempt to use an
+internal content merger; if that fails, it will attempt to start an
 external interactive merge tool; the user must then resolve the conflicts
 and terminate the merge tool, letting monotone continue with the
 merge. This process is repeated for each file content conflict. See
@@ -3277,7 +3278,7 @@ @subsection Conflict Types
 Monotone versions files, directories, and file attributes explicitly,
 and it tracks individual file and directory identity from birth to
 death so that name changes throughout the full life-cycle can be
-tracked exactly.  Partly because of these qualities, monotone notices
+tracked exactly. Partly because of these qualities, monotone notices
 several types of conflicts that other version control systems may not.
 
 The two most common conflicts are described first, then all other
@@ -3294,17 +3295,21 @@ @subheading File Content Conflict
 has a @code{mtn:manual_merge} attribute with value @code{true}. If
 not, it uses an internal merge algorithm to detect whether the changes
 are to the same lines of the file. If they are not, monotone will use
-the result of the internal merge as the new file version.
+the result of the internal merge as the new file version. Note that
+this may not always be what the user wants; if the same line is added
+at two different places in the file, it will be in the result twice.
 
address@hidden:manual_merge} is set @code{true} when a file is
address@hidden for all files for which the @code{binary_file} hook
-returns true; see @ref{attr_init_functions}.
address@hidden:manual_merge} is automatically set @code{true} when a file
+is @command{add}ed for which the @code{binary_file} hook returns true;
+see @ref{attr_init_functions}. The user may also set the
address@hidden:manual_merge} attribute manually; see @address@hidden attr}}.
 
 If @code{mtn:manual_merge} is present and @code{true}, or if the
 changes are to the same lines of the file, and neither
 @option{--resolve-conflicts} nor @option{--non-interactive} was
 specified, the @ref{merge3} hook is called, with the content of both
-conflicting versions and their common ancestor.
+conflicting versions and their common ancestor. When the hook returns,
+the merge proceeds to the next conflict.
 
 Alternatively, you can use your favorite merge tool asychronously with
 the merge, and specify the result file in the conflicts file, using
@@ -3485,7 +3490,7 @@ @subheading Missing Root Conflict
 See the @command{pivot_root} command for more information on moving
 another directory to the project root.
 
address@hidden does not yet support resolving this conflict.
address@hidden does not support resolving this conflict.
 
 @subheading Dropped/Modified file Conflict
 This conflict occurs when a file is dropped in one merge parent,
@@ -5558,6 +5563,10 @@ @section Workspace
 This command also moves any attributes on @var{src} to @var{dst}; see
 @ref{File Attributes} for more details.
 
+Note that you cannot rename a branch. You can accomplish something
+similar by creating a new branch with the desired name, using the
address@hidden approve} command to add a branch name to the desired revision.
+
 @anchor{mtn address@hidden mtn revert @var{pathname...}
 @itemx mtn revert --missing @var{pathname...}
 See the online help for more options.
@@ -6356,7 +6365,7 @@ @section Review
 @section Review
 
 @ftable @command
address@hidden mtn approve @var{rev} address@hidden [--[no-]update]
address@hidden address@hidden mtn approve @var{rev} address@hidden [--[no-]update]
 This command puts @var{rev} on the branch @var{branchname} (defaults
 to the workspace branch).
 
============================================================
--- src/cmd_ws_commit.cc	431aff612ada586c56b63247423ae72df1650b88
+++ src/cmd_ws_commit.cc	78e02629916d649d7eed18480e05ede5f4b19971
@@ -1,4 +1,4 @@
-// Copyright (C) 2010, 2011 Stephen Leake <address@hidden>
+// Copyright (C) 2010, 2011, 2012 Stephen Leake <address@hidden>
 // Copyright (C) 2002 Graydon Hoare <address@hidden>
 //
 // This program is made available under the GNU GPL version 2.0 or
@@ -530,9 +530,8 @@ revert(app_state & app,
   revision_t remaining;
   make_revision_for_workspace(parent_id(parents.begin()), preserved, remaining);
 
-  // Race.
   work.put_work_rev(remaining);
-  work.maybe_update_inodeprints(db);
+  work.maybe_update_inodeprints(db, mask);
 }
 
 CMD(revert, "revert", "", CMD_REF(workspace), N_("[PATH]..."),
@@ -1801,7 +1800,7 @@ void perform_commit(app_state & app,
       % prog_name);
   }
 
-  work.maybe_update_inodeprints(db);
+  work.maybe_update_inodeprints(db, mask);
 
   {
     // Tell lua what happened. Yes, we might lose some information
============================================================
--- src/work.cc	36e5dcda8cf09c9054cb88e6165707112ba9ac03
+++ src/work.cc	324996c07f4fc5bda6e32d20eac172b35db759cf
@@ -1,4 +1,4 @@
-// Copyright (C) 2009, 2010 Stephen Leake <address@hidden>
+// Copyright (C) 2009, 2010, 2012 Stephen Leake <address@hidden>
 // Copyright (C) 2002 Graydon Hoare <address@hidden>
 //
 // This program is made available under the GNU GPL version 2.0 or
@@ -900,15 +900,29 @@ workspace::maybe_update_inodeprints(data
 void
 workspace::maybe_update_inodeprints(database & db)
 {
+  maybe_update_inodeprints(db, node_restriction());
+}
+
+void
+workspace::maybe_update_inodeprints(database & db,
+                                    node_restriction const & mask)
+{
   if (!in_inodeprints_mode())
     return;
 
+  // We update the cache only for files that are included in the
+  // restriction. The only guarantee that inodeprints mode makes is that if
+  // a file's current inodeprint matches its cached inodeprint then it has
+  // not changed. i.e. for a missing file, the cache would not be updated
+  // but the old cached value can't possibly equal the current value since
+  // the file does not exist and cannot have an inodeprint.
+
   inodeprint_map ipm_new;
   temp_node_id_source nis;
   roster_t new_roster;
 
   get_current_roster_shape(db, nis, new_roster);
-  update_current_roster_from_filesystem(new_roster);
+  update_current_roster_from_filesystem(new_roster, mask);
 
   parent_map parents;
   get_parent_rosters(db, parents);
@@ -917,6 +931,10 @@ workspace::maybe_update_inodeprints(data
   for (node_map::const_iterator i = new_nodes.begin(); i != new_nodes.end(); ++i)
     {
       node_id nid = i->first;
+
+      if (!mask.includes(new_roster, nid))
+        continue;
+
       if (!is_file_t(i->second))
         continue;
       file_t new_file = downcast_to_file_t(i->second);
============================================================
--- src/work.hh	0ca2a0f0ab94c2421db119216af9373b81f38e61
+++ src/work.hh	520ff77cc8bc9655d3826388f06dda29de9292d0
@@ -1,3 +1,4 @@
+// Copyright (C) 2012 Stephen Leake <address@hidden>
 // Copyright (C) 2002 Graydon Hoare <address@hidden>
 //
 // This program is made available under the GNU GPL version 2.0 or
@@ -287,6 +288,8 @@ public:
 
   void enable_inodeprints();
   void maybe_update_inodeprints(database &);
+  void maybe_update_inodeprints(database &,
+                                node_restriction const & mask);
 
   // the 'ignore file', .mtn-ignore in the root of the workspace, contains a
   // set of regular expressions that match pathnames.  any file or directory
============================================================
--- test/func/netsync_key_hooks/__driver__.lua	d7fad5a70688d3db512a05ebb0e3643006f8c7e2
+++ test/func/netsync_key_hooks/__driver__.lua	0c2d3f3443fa9ff90615cd5338e322baa5ad44aa
@@ -67,5 +67,10 @@ server({}, 1, "you have multiple private
 end
 
 server({}, 1, "you have multiple private keys")
-server({"--rcfile", "server-hooks.lua"}, -SIGTERM, "beginning service on localhost")
 
+-- SIGTERM gives unreliable process status on Windows
+if(ostype == "Windows") then
+   partial_skip = true
+else         
+   server({"--rcfile", "server-hooks.lua"}, -SIGTERM, "beginning service on localhost")
+end
============================================================
--- /dev/null	
+++ test/func/pull_branch_vs_db_check/__driver__.lua	928dc8f35c4976ccd7501d04bcdfaa7ea656e713
@@ -0,0 +1,37 @@
+-- Test for issue 201
+--
+-- original problem:
+--
+-- mtn pull with branch pattern does not pull branch certs from
+-- propagate that don't match pattern. So if there are revs that are
+-- pulled because of parent relationships, but only have branch certs
+-- that don't match the pattern, mtn db check reports missing branch
+-- certs as serious problem.
+
+includecommon("netsync.lua")
+mtn_setup("testbranch")
+netsync.setup()
+
+addfile("testfile", "blah stuff")
+commit()
+ver0 = base_revision()
+
+addfile("testfile2", "some more data")
+commit("otherbranch")
+ver1 = base_revision()
+
+check(mtn("propagate", "testbranch", "otherbranch"), 0, nil, true)
+
+netsync.pull("otherbranch")
+
+-- Now ver0 has no branch cert in mtn2:
+check(mtn("ls", "certs", ver0), 0, true, nil)
+check(qgrep("Name  : branch", "stdout"))
+
+check(mtn2("ls", "certs", ver0), 0, true, nil)
+check(not qgrep("Name  : branch", "stdout"))
+
+-- and db check says this is a serious problem, but we'd like it not to.
+xfail(mtn2("db", "check"), 0, nil, true)
+check(qgrep("missing branch cert", "stderr"))
+check(qgrep("error: serious problems detected", "stderr"))
============================================================
--- test/func/restricted_commit_with_inodeprints/__driver__.lua	b11b4fbd4ed1b88ccb61d2b9ea5347c61a360c47
+++ test/func/restricted_commit_with_inodeprints/__driver__.lua	48dda63d86651a117b978054e362f4ed056f3e47
@@ -1,23 +1,13 @@ mtn_setup()
 
 mtn_setup()
 
--- this test is a bug report. the problem is that inodeprints tries to update its
--- cache for all files in the complete manifest, but a restricted commit can
--- succeed with missing files if they are excluded. subsequently the inodeprint
--- update fails because it can't build a complete manifest due to the missing
--- files.
+-- This test is a bug report, now fixed. The problem was:
+-- 
+-- inodeprints tries to update its cache for all files in the complete
+-- manifest, but a restricted commit can succeed with missing files if
+-- they are excluded. subsequently the inodeprint update fails because
+-- it can't build a complete manifest due to the missing files.
 
--- one solution is to let inodeprints update its cache only for files that are
--- included in the restriction, which seems to be safe. the only gaurentee that
--- inodeprints mode makes is that if a file's current inodeprint matches its
--- cached inodprint then it has not changed. i.e. for a missing file, the cache
--- would not be updated but the old cached value can't possibly equal the current
--- value since the file does not exist and cannot have an inodeprint.
-
--- also, it may be a useful feature (?) to allow refresh_inodeprints to update
--- its cache for a restricted set of files by allowing paths on the command line
--- to establish a restriction.
-
 addfile("file1", "file1")
 
 commit()
@@ -32,6 +22,5 @@ remove("file1")
 remove("file1")
 
 -- restricted commit of file2 succeeds with file1 missing
--- but the corresponding inodeprint update fails
 
-xfail_if(true, mtn("commit", "--message=file2", "file2"),  0, false, false)
+check(mtn("commit", "--message=file2", "file2"),  0, false, false)
============================================================
--- /dev/null	
+++ test/func/revert_with_inodeprints/__driver__.lua	d3ad794b0a265fb5fa4eee3e264eefc6931d9340
@@ -0,0 +1,21 @@
+-- Test for issue 207; partial revert succeeds without inodeprints,
+-- fails with. Now fixed.
+
+mtn_setup()
+
+addfile("file1", "file1")
+addfile("file2", "file2")
+
+commit()
+
+-- enable inodeprints mode
+check(mtn("refresh_inodeprints"), 0, false, false)
+
+-- create two missing files
+
+remove("file1")
+remove("file2")
+
+-- revert only one file
+
+check(mtn("revert", "file2"),  0, false, false)
============================================================
--- /dev/null	
+++ test/func/status_of_ignored/__driver__.lua	f64919f41c4733dac61b88884b945988faa1c2b0
@@ -0,0 +1,27 @@
+-- Test for issue 193
+--
+-- Original problem:
+--
+-- mtn status foo.bak produces confusing output, assuming '*.bak' is
+-- in the list of ignore file patterns (it is in the default list).
+--
+-- The problem report states this was noticed in mtn 1.0 (released
+-- in 2011), but annotate says the relevant fix was added in 2008.
+
+mtn_setup()
+
+-- add a real file, to see what a 'normal' status report looks like
+addfile("file_1", "file_1")
+commit()
+
+check(mtn("status", "file_1"), 0, true, nil)
+check(qgrep("no changes", "stdout"))
+
+-- Now gives an error:
+check(mtn("status", "foo.bak"), 1, nil, true)
+check(qgrep("warning: restriction includes unknown path 'foo.bak'", "stderr"))
+
+-- Try again with an existing foo.bak
+writefile("foo.bak", "foo.bak")
+check(mtn("status", "foo.bak"), 1, nil, true)
+check(qgrep("warning: restriction includes unknown path 'foo.bak'", "stderr"))

reply via email to

[Prev in Thread] Current Thread [Next in Thread]