4 Copyright (C) 2006, 2007, 2008 Holger Hans Peter Freyther
6 Permission is hereby granted, free of charge, to any person obtaining a copy
7 of this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights
9 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 copies of the Software, and to permit persons to whom the Software is
11 furnished to do so, subject to the following conditions:
13 The above copyright notice and this permission notice shall be included in
14 all copies or substantial portions of the Software.
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
39 # Interesting revisions:
40 # Rename with dest==src: 24cba5923360fef7c5cc81d51000e30b90355eb9
41 # Recursive rename: fca159c5c00ae4158c289f5aabce995378d4e41b
42 # Delete+Rename: 91da98265a39c93946e00adf5d7bf92b341de847
47 # Our manifest/tree fifo construct
51 def get_mark(revision
):
53 Get a mark for a specific revision. If the revision is known the former
54 mark will be returned. Otherwise a new mark will be allocated and stored
57 if revision
in status
.marks
:
58 return status
.marks
[revision
]
60 status
.marks
[revision
] = status
.last_mark
61 print >> status
.mark_file
, "%d: %s" % (status
.last_mark
, revision
)
62 status
.mark_file
.flush()
63 return status
.last_mark
65 def has_mark(revision
):
66 return revision
in status
.marks
68 def get_branch_name(revision
):
70 TODO for unnamed branches (e.g. as we lack the certs) we might want to follow
71 the parents until we end up at a item with a branch name and then use the last
72 item without a name...
74 if "branch" in revision
:
75 branch
= revision
["branch"]
77 #branch = "initial-%s" % revision["revision"]
78 branch
= "mtn-rev-%s" % revision
["revision"]
81 def reset_git(ops
, revision
):
83 Find the name of the branch of this revision
85 branch
= get_branch_name(revision
)
88 cmd
+= ["reset refs/heads/%s" % branch
]
89 cmd
+= ["from :%s" % get_mark(revision
["revision"])]
95 Force git to checkpoint the import
102 def get_git_date(revision
):
104 Convert the "date" cert of monotone to a time understandable by git. No timezone
105 conversions are done.
107 dt
= datetime
.datetime
.strptime(revision
["date"], "%Y-%m-%dT%H:%M:%S").strftime("%a, %d %b %Y %H:%M:%S +0000")
110 def is_executable_attribute_set(attributes
, rev
):
111 assert(len(attributes
) % 3 == 0), rev
113 if len(attributes
) >= 3:
114 for i
in range(0, len(attributes
)%3+1):
115 if attributes
[i
] == "attr" and attributes
[i
+1] == "mtn:execute" and attributes
[i
+2] == "true":
119 def build_tree(manifest
, rev
):
120 """Assemble a filesystem tree from a given manifest"""
128 for line
in manifest
:
129 if line
[0] == "file":
130 tree
.files
[line
[1]] = (line
[3], is_executable_attribute_set(line
[4:], rev
))
131 elif line
[0] == "dir":
132 tree
.dirs
[line
[1]] = 1
133 elif line
[0] != "format_version":
134 assert(False), "Rev: %s: Line[0]: '%s'" % (rev
, line
[0])
138 def get_and_cache_tree(ops
, revision
):
139 """Simple FIFO to cache a number of trees"""
140 global cached_tree
, cached_fifo
142 if revision
in cached_tree
:
143 return cached_tree
[revision
]
145 tree
= build_tree([line
for line
in ops
.get_manifest_of(revision
)], revision
)
146 cached_tree
[revision
] = tree
147 cached_fifo
.append(revision
)
150 if len(cached_fifo
) > 100:
151 old_name
= cached_fifo
[0]
152 cached_fifo
= cached_fifo
[1:]
153 del cached_tree
[old_name
]
157 def diff_manifest(old_tree
, new_tree
):
158 """Find additions, modifications and deletions"""
164 for dir in old_tree
.dirs
.keys():
165 if not dir in new_tree
.dirs
:
166 deleted
.add((dir,True))
169 for dir in new_tree
.dirs
.keys():
170 if not dir in old_tree
.dirs
:
174 for file in old_tree
.files
.keys():
175 if not file in new_tree
.files
:
176 deleted
.add((file,False))
178 # Added files, goes to modifications
179 for file in new_tree
.files
.keys():
180 if not file in old_tree
.files
:
181 modified
.add((file, new_tree
.files
[file][0]))
184 # The file changed, either contents or executable attribute
185 old
= old_tree
.files
[file]
186 new
= new_tree
.files
[file]
188 modified
.add((file, new_tree
.files
[file][0]))
190 return (added
, modified
, deleted
)
192 def fast_import(ops
, revision
):
193 """Import a revision into git using git-fast-import.
195 First convert the revision to something git-fast-import
198 assert("revision" in revision
)
199 assert("author" in revision
)
200 assert("committer" in revision
)
201 assert("parent" in revision
)
203 branch
= get_branch_name(revision
)
205 # Use the manifest to find dirs and files
206 current_tree
= get_and_cache_tree(ops
, revision
["revision"])
208 # Now diff the manifests
209 if len(revision
["parent"]) == 0:
212 (added
, modified
, deleted
) = diff_manifest(build_tree([],""), current_tree
)
214 # The first parent is our from.
215 merge_from
= revision
["parent"][0]
216 merge_other
= revision
["parent"][1:]
217 (added
, modified
, deleted
) = diff_manifest(get_and_cache_tree(ops
, merge_from
), current_tree
)
220 # Readd the sanity check to see if we deleted and modified an entry. This
221 # could probably happen if we have more than one parent (on a merge)?
224 if len(revision
["parent"]) == 0:
225 cmd
+= ["reset refs/heads/%s" % branch
]
226 cmd
+= ["commit refs/heads/%s" % branch
]
227 cmd
+= ["mark :%s" % get_mark(revision
["revision"])]
228 cmd
+= ["author <%s> %s" % (revision
["author"], get_git_date(revision
))]
229 cmd
+= ["committer <%s> %s" % (revision
["committer"], get_git_date(revision
))]
230 cmd
+= ["data %d" % len(revision
["changelog"])]
231 cmd
+= ["%s" % revision
["changelog"]]
233 if not merge_from
is None:
234 cmd
+= ["from :%s" % get_mark(merge_from
)]
236 for parent
in merge_other
:
237 cmd
+= ["merge :%s" % get_mark(parent
)]
239 for dir_name
in added
:
240 cmd
+= ["M 644 inline %s" % os
.path
.join(dir_name
, ".mtn2git_empty")]
241 cmd
+= ["data <<EOF"]
245 for (file_name
, file_revision
) in modified
:
246 (mode
, file) = get_file_and_mode(ops
, current_tree
, file_name
, file_revision
, revision
["revision"])
247 cmd
+= ["M %d inline %s" % (mode
, file_name
)]
248 cmd
+= ["data %d" % len(file)]
251 for (path
, is_dir
) in deleted
:
253 cmd
+= ["D %s" % os
.path
.join(path
, ".mtn2git_empty")]
255 cmd
+= ["D %s" % path
]
260 def is_trusted(operations
, revision
):
261 for cert
in operations
.certs(revision
):
262 if cert
[0] != 'key' or cert
[3] != 'ok' or cert
[8] != 'trust' or cert
[9] != 'trusted':
263 print >> sys
.stderr
, "Cert untrusted?, this must be bad", cert
267 def get_file_and_mode(operations
, file_tree
, file_name
, _file_revision
, rev
= None):
268 assert file_name
in file_tree
.files
, "get_file_and_mode: Revision '%s', file_name='%s' " % (rev
, file_name
)
270 (file_revision
, executable
) = file_tree
.files
[file_name
]
272 assert _file_revision
== file_revision
, "Same filerevision for file_name='%s' in rev='%s' (%s,%s)" % (file_name
, rev
, file_revision
, _file_revision
)
279 file = "".join([file for file in operations
.get_file(file_revision
)])
282 def parse_revision(operations
, revision
):
284 Parse a revision as of mtn automate get_revision
286 Return a tuple with the current version, a list of parents,
287 a list of operations and their revision
289 if not is_trusted(operations
, revision
):
290 raise Exception("Revision %s is not trusted!" % revision
)
292 # The order of certain operations, e.g rename matter so don't use a set
293 revision_description
= {}
294 revision_description
["revision"] = revision
295 revision_description
["added_dirs"] = []
296 revision_description
["added_files"] = []
297 revision_description
["removed"] = []
298 revision_description
["modified"] = []
299 revision_description
["renamed"] = []
300 revision_description
["set_attributes"] = []
301 revision_description
["clear_attributes"] = []
305 for line
in operations
.get_revision(revision
):
306 if line
[0] == "format_version":
307 assert(line
[1] == "1")
308 elif line
[0] == "old_revision":
309 if not "parent" in revision_description
:
310 revision_description
["parent"] = []
311 if len(line
[1]) != 0:
312 revision_description
["parent"].append(line
[1])
314 elif line
[0] == "new_manifest":
315 revision_description
["manifest"] = line
[1]
316 elif line
[0] == "clear":
317 revision_description
["clear_attributes"].append((line
[1], line
[3], old_rev
))
318 elif line
[0] == "set":
319 revision_description
["set_attributes"].append((line
[1], line
[3], line
[5], old_rev
))
320 elif line
[0] in ["rename", "patch", "delete", "add_dir", "add_file"]:
323 print >> sys
.stderr
, line
326 for cert
in operations
.certs(revision
):
327 # Known cert names used by mtn, we can ignore them as they can't be converted to git
328 if cert
[5] in ["suspend", "testresult", "file-comment", "comment", "release-candidate"]:
330 elif cert
[5] in ["author", "changelog", "date", "branch", "tag"]:
331 revision_description
[cert
[5]] = cert
[7]
332 if cert
[5] == "author":
333 revision_description
["committer"] = cert
[1]
335 print >> sys
.stderr
, "Unknown Cert: Ignoring", cert
[5], cert
[7]
338 return revision_description
340 def tests(ops
, revs
):
341 """Load a bunch of revisions and exit"""
343 print >> sys
.stderr
, rev
344 fast_import(ops
, parse_revision(ops
, rev
))
348 def main(mtn_cli
, db
, rev
):
350 print >> sys
.stderr
, "You need to specifiy a monotone db"
353 ops
= mtn
.Operations([mtn_cli
, db
])
355 # Double rename in mtn
356 #tests(ops, ["fca159c5c00ae4158c289f5aabce995378d4e41b"])
358 # Rename and remove in OE
359 #tests(ops, ["74db43a4ad2bccd5f2fd59339e4ece0092f8dcb0"])
362 #tests(ops, ["91da98265a39c93946e00adf5d7bf92b341de847"])
364 # Issue with renaming in OE
365 #tests(ops, ["c81294b86c62ee21791776732f72f4646f402445"])
367 # Unterminated inner renames
368 #tests(ops, ["d813a779ef7157f88dade0b8ccef32f28ff34a6e", "4d027b6bcd69e7eb5b64b2e720c9953d5378d845", "af5ffd789f2852e635aa4af88b56a893b7a83a79"])
370 # Broken rename in OE. double replacing of the directory command
371 #tests(ops, ["11f85aab185581dcbff7dce29e44f7c1f0572a27"])
377 branches
= [branch
.name
for branch
in ops
.branches()]
382 for branch
in branches
:
383 heads
= [head
for head
in ops
.heads(branch
)]
385 print >> sys
.stderr
, "Skipping branch '%s' due multiple heads" % (branch
)
388 if branch
in status
.former_heads
:
389 old_heads
= status
.former_heads
[branch
]
394 print >> sys
.stderr
, old_heads
, head
395 all_revs
+= ops
.ancestry_difference(head
, old_heads
)
396 status
.former_heads
[branch
] = heads
399 sorted_revs
= [rev
for rev
in ops
.toposort(all_revs
)]
400 for rev
in sorted_revs
:
402 print >> sys
.stderr
, "B: Already having commit '%s'" % rev
404 print >> sys
.stderr
, "Going to import revision ", rev
405 fast_import(ops
, parse_revision(ops
, rev
))
406 if counter
% 1000 == 0:
410 if __name__
== "__main__":
412 parser
= optparse
.OptionParser()
413 parser
.add_option("-d", "--db", dest
="database",
414 help="The monotone database to use")
415 parser
.add_option("-m", "--marks", dest
="marks", default
="mtn2git-marks",
416 help="The marks allocated by the mtn2git command")
417 parser
.add_option("-t", "--mtn", dest
="mtn", default
="mtn",
418 help="The name of the mtn command to use")
419 parser
.add_option("-s", "--status", dest
="status", default
="mtn2git.status.v2",
420 help="The status file as used by %prog")
421 parser
.add_option("-r", "--revision", dest
="rev", default
=None,
422 help="Import a single revision to help debugging.")
424 (options
,_
) = parser
.parse_args(sys
.argv
)
425 status
.mark_file
= file(options
.marks
, "a")
428 status
.load(options
.status
)
430 print >> sys
.stderr
, "Failed to open the status file"
431 main(options
.mtn
, options
.database
, options
.rev
)
432 status
.store(options
.status
)