~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/merge.py

Made the merge internals private

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
 
 
18
17
import os
19
 
import shutil
20
 
import errno
21
18
 
22
 
import bzrlib.osutils
23
 
import bzrlib.revision
24
 
from bzrlib.merge_core import merge_flex, ApplyMerge3, BackupBeforeChange
25
 
from bzrlib.merge_core import WeaveMerge
26
 
from bzrlib.changeset import generate_changeset, ExceptionConflictHandler
27
 
from bzrlib.changeset import Inventory, Diff3Merge, ReplaceContents
 
19
import bzrlib
 
20
from bzrlib._changeset import generate_changeset
 
21
from bzrlib._changeset import Inventory, Diff3Merge
 
22
from bzrlib._merge import MergeConflictHandler
 
23
from bzrlib._merge_core import WeaveMerge
 
24
from bzrlib._merge_core import merge_flex, ApplyMerge3, BackupBeforeChange
28
25
from bzrlib.branch import Branch
 
26
from bzrlib.delta import compare_trees
29
27
from bzrlib.errors import (BzrCommandError,
30
28
                           UnrelatedBranches,
31
29
                           NoCommonAncestor,
32
30
                           NoCommits,
33
31
                           WorkingTreeNotRevision,
34
 
                           NotBranchError,
35
32
                           NotVersionedError,
36
33
                           BzrError)
37
 
from bzrlib.delta import compare_trees
38
 
from bzrlib.trace import mutter, warning, note
39
34
from bzrlib.fetch import greedy_fetch, fetch
 
35
from bzrlib.osutils import pathjoin
 
36
from bzrlib.revision import common_ancestor, MultipleRevisionSources
40
37
from bzrlib.revision import is_ancestor, NULL_REVISION
41
 
from bzrlib.osutils import rename, pathjoin
42
 
from bzrlib.revision import common_ancestor, MultipleRevisionSources
43
 
from bzrlib.errors import NoSuchRevision
44
 
 
45
 
# TODO: Report back as changes are merged in
46
 
 
47
 
# TODO: build_working_dir can be built on something simpler than merge()
48
 
 
49
 
# FIXME: merge() parameters seem oriented towards the command line
50
 
# NOTABUG: merge is a helper for commandline functions.  merge_inner is the
51
 
#          the core functionality.
52
 
 
53
 
# comments from abentley on irc: merge happens in two stages, each
54
 
# of which generates a changeset object
55
 
 
56
 
# stage 1: generate OLD->OTHER,
57
 
# stage 2: use MINE and OLD->OTHER to generate MINE -> RESULT
58
 
 
59
 
class MergeConflictHandler(ExceptionConflictHandler):
60
 
    """Handle conflicts encountered while merging.
61
 
 
62
 
    This subclasses ExceptionConflictHandler, so that any types of
63
 
    conflict that are not explicitly handled cause an exception and
64
 
    terminate the merge.
65
 
    """
66
 
    def __init__(self, this_tree, base_tree, other_tree, ignore_zero=False):
67
 
        ExceptionConflictHandler.__init__(self)
68
 
        self.conflicts = 0
69
 
        self.ignore_zero = ignore_zero
70
 
        self.this_tree = this_tree
71
 
        self.base_tree = base_tree
72
 
        self.other_tree = other_tree
73
 
 
74
 
    def copy(self, source, dest):
75
 
        """Copy the text and mode of a file
76
 
        :param source: The path of the file to copy
77
 
        :param dest: The distination file to create
78
 
        """
79
 
        s_file = file(source, "rb")
80
 
        d_file = file(dest, "wb")
81
 
        for line in s_file:
82
 
            d_file.write(line)
83
 
        os.chmod(dest, 0777 & os.stat(source).st_mode)
84
 
 
85
 
    def dump(self, lines, dest):
86
 
        """Copy the text and mode of a file
87
 
        :param source: The path of the file to copy
88
 
        :param dest: The distination file to create
89
 
        """
90
 
        d_file = file(dest, "wb")
91
 
        for line in lines:
92
 
            d_file.write(line)
93
 
 
94
 
    def add_suffix(self, name, suffix, last_new_name=None, fix_inventory=True):
95
 
        """Rename a file to append a suffix.  If the new name exists, the
96
 
        suffix is added repeatedly until a non-existant name is found
97
 
 
98
 
        :param name: The path of the file
99
 
        :param suffix: The suffix to append
100
 
        :param last_new_name: (used for recursive calls) the last name tried
101
 
        """
102
 
        if last_new_name is None:
103
 
            last_new_name = name
104
 
        new_name = last_new_name+suffix
105
 
        try:
106
 
            rename(name, new_name)
107
 
            if fix_inventory is True:
108
 
                try:
109
 
                    relpath = self.this_tree.relpath(name)
110
 
                except NotBranchError:
111
 
                    relpath = None
112
 
                if relpath is not None:
113
 
                    file_id = self.this_tree.path2id(relpath)
114
 
                    if file_id is not None:
115
 
                        new_path = self.this_tree.relpath(new_name)
116
 
                        rename(new_name, name)
117
 
                        self.this_tree.rename_one(relpath, new_path)
118
 
                        assert self.this_tree.id2path(file_id) == new_path
119
 
        except OSError, e:
120
 
            if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY:
121
 
                raise
122
 
            return self.add_suffix(name, suffix, last_new_name=new_name, 
123
 
                                   fix_inventory=fix_inventory)
124
 
        return new_name
125
 
 
126
 
    def conflict(self, text):
127
 
        warning(text)
128
 
        self.conflicts += 1
129
 
        
130
 
 
131
 
    def merge_conflict(self, new_file, this_path, base_lines, other_lines):
132
 
        """
133
 
        Handle diff3 conflicts by producing a .THIS, .BASE and .OTHER.  The
134
 
        main file will be a version with diff3 conflicts.
135
 
        :param new_file: Path to the output file with diff3 markers
136
 
        :param this_path: Path to the file text for the THIS tree
137
 
        :param base_path: Path to the file text for the BASE tree
138
 
        :param other_path: Path to the file text for the OTHER tree
139
 
        """
140
 
        self.add_suffix(this_path, ".THIS", fix_inventory=False)
141
 
        self.dump(base_lines, this_path+".BASE")
142
 
        self.dump(other_lines, this_path+".OTHER")
143
 
        rename(new_file, this_path)
144
 
        self.conflict("Diff3 conflict encountered in %s" % this_path)
145
 
 
146
 
    def weave_merge_conflict(self, filename, weave, other_i, out_file):
147
 
        """
148
 
        Handle weave conflicts by producing a .THIS, and .OTHER.  The
149
 
        main file will be a version with diff3-style conflicts.
150
 
        """
151
 
        self.add_suffix(filename, ".THIS", fix_inventory=False)
152
 
        out_file.commit()
153
 
        self.dump(weave.get_iter(other_i), filename+".OTHER")
154
 
        self.conflict("Text conflict encountered in %s" % filename)
155
 
 
156
 
    def new_contents_conflict(self, filename, other_contents):
157
 
        """Conflicting contents for newly added file."""
158
 
        other_contents(filename + ".OTHER", self, False)
159
 
        self.conflict("Conflict in newly added file %s" % filename)
160
 
    
161
 
 
162
 
    def target_exists(self, entry, target, old_path):
163
 
        """Handle the case when the target file or dir exists"""
164
 
        moved_path = self.add_suffix(target, ".moved")
165
 
        self.conflict("Moved existing %s to %s" % (target, moved_path))
166
 
 
167
 
    def rmdir_non_empty(self, filename):
168
 
        """Handle the case where the dir to be removed still has contents"""
169
 
        self.conflict("Directory %s not removed because it is not empty"\
170
 
            % filename)
171
 
        return "skip"
172
 
 
173
 
    def rem_contents_conflict(self, filename, this_contents, base_contents):
174
 
        base_contents(filename+".BASE", self)
175
 
        this_contents(filename+".THIS", self)
176
 
        self.conflict("Other branch deleted locally modified file %s" %
177
 
                      filename)
178
 
        return ReplaceContents(this_contents, None)
179
 
 
180
 
    def abs_this_path(self, file_id):
181
 
        """Return the absolute path for a file_id in the this tree."""
182
 
        return self.this_tree.id2abspath(file_id)
183
 
 
184
 
    def add_missing_parents(self, file_id, tree):
185
 
        """If some of the parents for file_id are missing, add them."""
186
 
        entry = tree.inventory[file_id]
187
 
        if entry.parent_id not in self.this_tree:
188
 
            return self.create_all_missing(entry.parent_id, tree)
189
 
        else:
190
 
            return self.abs_this_path(entry.parent_id)
191
 
 
192
 
    def create_all_missing(self, file_id, tree):
193
 
        """Add contents for a file_id and all its parents to a tree."""
194
 
        entry = tree.inventory[file_id]
195
 
        if entry.parent_id is not None and entry.parent_id not in self.this_tree:
196
 
            abspath = self.create_all_missing(entry.parent_id, tree)
197
 
        else:
198
 
            abspath = self.abs_this_path(entry.parent_id)
199
 
        entry_path = pathjoin(abspath, entry.name)
200
 
        if not os.path.isdir(entry_path):
201
 
            self.create(file_id, entry_path, tree)
202
 
        return entry_path
203
 
 
204
 
    def create(self, file_id, path, tree):
205
 
        """Uses tree data to create a filesystem object for the file_id"""
206
 
        from changeset import get_contents
207
 
        get_contents(tree, file_id)(path, self)
208
 
 
209
 
    def missing_for_merge(self, file_id, other_path):
210
 
        """The file_id doesn't exist in THIS, but does in OTHER and BASE"""
211
 
        self.conflict("Other branch modified locally deleted file %s" %
212
 
                      other_path)
213
 
        parent_dir = self.add_missing_parents(file_id, self.other_tree)
214
 
        stem = pathjoin(parent_dir, os.path.basename(other_path))
215
 
        self.create(file_id, stem+".OTHER", self.other_tree)
216
 
        self.create(file_id, stem+".BASE", self.base_tree)
217
 
 
218
 
    def threeway_contents_conflict(filename, this_contents, base_contents,
219
 
                                   other_contents):
220
 
        self.conflict("Three-way conflict merging %s" % filename)
221
 
 
222
 
    def finalize(self):
223
 
        if self.conflicts == 0:
224
 
            if not self.ignore_zero:
225
 
                note("All changes applied successfully.")
226
 
        else:
227
 
            note("%d conflicts encountered." % self.conflicts)
228
 
            
 
38
from bzrlib.trace import mutter, note
 
39
 
229
40
def get_tree(treespec, local_branch=None):
230
41
    location, revno = treespec
231
42
    branch = Branch.open_containing(location)[0]