7 from wizard import deploy, command, shell
10 options, args = parse_args(argv, baton)
11 # Make sure this is a Wizard repo
12 if not options.generic:
13 d = deploy.ProductionCopy('.')
16 except deploy.AlreadyVersionedError:
18 # Determine upstream commit history
19 # XXX This is a little sloppy (since it also pulls in remotes and
20 # originals), but if the upstream repo has a clean set of remotes,
21 # that shouldn't be a problem.
22 excludes = map(lambda line: line.partition("\t")[0],
23 shell.eval("git", "ls-remote", options.remote).splitlines())
24 # Determine local commits and their parents
27 for line in shell.eval("git", "rev-list", "--parents", "--branches", "--not", *excludes).split("\n"):
28 (commit, _, parent_string) = line.partition(' ')
29 local_commits.add(commit)
30 parents = parent_string.split()
31 all_parents.update(parents)
32 # Determine what commits need mapping
33 needs_map = all_parents - local_commits
34 # Determine the new commits for these maps
36 for hash in needs_map:
37 summary = shell.eval("git", "log", "-1", "--pretty=format:%s", hash)
38 # Find the corresponding commit by grepping for the summary from
39 # "live" tags (which should have been updated to the new history
40 # we are remastering to.) -F == fixed string (no regexing).
41 candidates = shell.eval("git", "rev-list", "-F", "--grep=" + summary, "--tags").splitlines()
42 if len(candidates) != 1:
43 raise "Failed looking for " + hash
44 mapping[hash] = candidates[0]
46 for search, replace in mapping.items():
49 # Bail out if nothing to do
53 # XXX Make this more robust: given our pre-processing, there is a
54 # very specific set of parent IDs we expect to see (not necessarily
55 # the ones in our mapping so far: those are precisely the IDs that
56 # may change, but some will stay the same.) Consider nops. This
57 # might be hard since git-filter-branch manufactures hashes as it
60 # Prepare the parent filter script
61 t = tempfile.NamedTemporaryFile(delete=False)
63 t.write("#!/bin/sed -f\n")
64 for search, replace in mapping.items():
65 t.write("s/%s/%s/g\n" % (search, replace))
67 shell.call("chmod", "a+x", t.name) # necessary?
68 logging.info("Sed script %s", t.name)
71 if options.force: maybe_force = ['--force']
72 extra_args = maybe_force + excludes
73 shell.call("git", "filter-branch", "--parent-filter", t.name, "--",
74 "--branches", "--not", *extra_args,
75 stdout=sys.stdout, stderr=sys.stderr)
80 def parse_args(argv, baton):
81 usage = """usage: %prog remaster [ARGS]
83 Reconciles divergent commit histories by rewriting all parent links to
84 point to the new commits. This only works if we are able to construct a
85 one-to-one correspondence between the old and new commits. This should
86 be automatically invoked by 'wizard upgrade' if a remastering is
89 Consider this history:
93 A--B--C remotes/origin/master
95 Suppose on a fetch, we discover that origin/master has been rebased, and
96 replaced with 'old-master':
100 A--B--C remotes/origin/old-master
102 A'-B'-C' remotes/origin/master
104 We would like to construct a new tree as follows:
106 D'----E' heads/master
108 A'-B'-C' remotes/origin/master
110 Where D/D' and E/E' have identical trees, just different parent commit
111 pointers. This is what 'wizard remaster' does.
113 In order to do this, we need to know two things: (1) which commits in
114 the old history were not provided by the user (the ones to rewrite are
115 'git log master ^origin/master', in the old history before the force
116 update of branch locations), and (2) what the correspondence between
117 the old commits and the new commits are. (1) is determined by looking
118 at all references in the remote repository. (2) is determined by
119 comparing commit messages; a user can also manually add extra mappings
120 if this heuristic fails (not implemented yet).
122 parser = command.WizardOptionParser(usage)
123 parser.add_option("-f", "--force", dest="force", action="store_true",
124 default=False, help="Force overwriting (passed to filter-branch).")
125 parser.add_option("--generic", dest="generic", action="store_true",
126 default=False, help="Allow remastering of non-Wizard repositories.")
127 parser.add_option("--remote", dest="remote", metavar="REMOTE",
128 default="origin", help="Rebased remote to remaster off of.")
129 options, args = parser.parse_all(argv)
131 parser.error("too many arguments")
136 class Error(wizard.Error):
137 """Base error class for this module"""
140 class NotWizardError(Error):
141 """The deployment was not a Wizard installation."""
145 ERROR: This is not a Wizard Git repository! If you really want to
146 use 'wizard remaster' on a non-Wizard repository, pass in
149 class NothingToDo(Error):
150 """No rewriting necessary."""
154 ERROR: Nothing to do!"""