]> scripts.mit.edu Git - wizard.git/blob - wizard/command/remaster.py
Initial commit of 'wizard remaster'.
[wizard.git] / wizard / command / remaster.py
1 import logging
2 import tempfile
3 import os
4 import sys
5
6 from wizard import command, shell
7
8 def main(argv, baton):
9     options, args = parse_args(argv, baton)
10     # Determine upstream commit history
11     # XXX This is a little sloppy (since it also pulls in remotes and
12     # originals), but if the upstream repo has a clean set of remotes,
13     # that shouldn't be a problem.
14     excludes = map(lambda line: line.partition("\t")[0],
15                    shell.eval("git", "ls-remote", "origin").splitlines())
16     # Determine local commits and their parents
17     local_commits = set()
18     all_parents = set()
19     for line in shell.eval("git", "rev-list", "--parents", "--branches", "--not", *excludes).split("\n"):
20         (commit, _, parent_string) = line.partition(' ')
21         local_commits.add(commit)
22         parents = parent_string.split()
23         all_parents.update(parents)
24     # Determine what commits need mapping
25     needs_map = all_parents - local_commits
26     # Determine the new commits for these maps
27     mapping = {}
28     for hash in needs_map:
29         summary    = shell.eval("git", "log", "-1", "--pretty=format:%s", hash)
30         # Find the corresponding commit by grepping for the summary from
31         # "live" tags (which should have been updated to the new history
32         # we are remastering to.)  -F == fixed string (no regexing).
33         candidates = shell.eval("git", "rev-list", "-F", "--grep=" + summary, "--tags").splitlines()
34         if len(candidates) != 1:
35             raise "Failed looking for " + hash
36         mapping[hash] = candidates[0]
37
38     # XXX Make this more robust: given our pre-processing, there is a
39     # very specific set of parent IDs we expect to see (not necessarily
40     # the ones in our mapping so far: those are precisely the IDs that
41     # may change, but some will stay the same.)  Consider nops.  This
42     # might be hard since git-filter-branch manufactures hashes as it
43     # goes along.
44
45     # Prepare the parent filter script
46     t = tempfile.NamedTemporaryFile(delete=False)
47     try:
48         t.write("#!/bin/sed -f\n")
49         for search, replace in mapping.items():
50             t.write("s/%s/%s/g\n" % (search, replace))
51         t.close()
52         shell.call("chmod", "a+x", t.name) # necessary?
53         logging.info("Sed script %s", t.name)
54         # Do the rewrite
55         shell.call("git", "filter-branch", "--parent-filter", t.name, "--",
56                    "--branches", "--not", *excludes,
57                    stdout=sys.stdout, stderr=sys.stderr)
58     finally:
59         # Cleanup
60         os.unlink(t.name)
61
62 def parse_args(argv, baton):
63     usage = """usage: %prog remaster [ARGS]
64
65 Reconciles divergent commit histories by rewriting all parent links to
66 point to the new commits.  This only works if we are able to construct a
67 one-to-one correspondence between the old and new commits.  This should
68 be automatically invoked by 'wizard upgrade' if a remastering is
69 necessary.
70
71 Consider this history:
72
73       D-----E   heads/master
74      /     /
75     A--B--C     remotes/origin/master
76
77 Suppose on a fetch, we discover that origin/master has been rebased, and
78 replaced with 'old-master':
79
80       D-----E   heads/master
81      /     /
82     A--B--C     remotes/origin/old-master
83
84     A'-B'-C'    remotes/origin/master
85
86 We would like to construct a new tree as follows:
87
88       D'----E'  heads/master
89      /     /
90     A'-B'-C'    remotes/origin/master
91
92 Where D/D' and E/E' have identical trees, just different parent commit
93 pointers.  This is what 'wizard remaster' does.
94
95 In order to do this, we need to know two things: (1) which commits in
96 the old history were not provided by the user (the ones to rewrite are
97 'git log master ^origin/master', in the old history before the force
98 update of branch locations), and (2) what the correspondence between
99 the old commits and the new commits are.  (1) is determined by looking
100 at all references in the remote repository.  (2) is determined by
101 comparing commit messages; a user can also manually add extra mappings
102 if this heuristic fails (not implemented yet).
103 """
104     parser = command.WizardOptionParser(usage)
105     parser.add_option("-f", "--force", dest="force", action="store_true",
106             default=False, help="Force overwriting.")
107     options, args = parser.parse_all(argv)
108     if len(args) > 0:
109         parser.error("too many arguments")
110     return options, args