]> scripts.mit.edu Git - wizard.git/blob - wizard/command/remaster.py
b07401486bda7e16d8ca697d9e5f19143d85596c
[wizard.git] / wizard / command / remaster.py
1 import logging
2 import tempfile
3 import os
4 import sys
5
6 import wizard
7 from wizard import deploy, command, shell
8
9 def main(argv, baton):
10     options, args = parse_args(argv, baton)
11     # Make sure this is a Wizard repo
12     if not options.generic:
13         d = deploy.ProductionCopy('.')
14         try:
15             d.verify()
16         except deploy.AlreadyVersionedError:
17             raise NotWizardError
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
25     local_commits = set()
26     all_parents = set()
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
35     mapping = {}
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]
45     # Delete no-ops
46     for search, replace in mapping.items():
47         if search == replace:
48             del mapping[search]
49     # Bail out if nothing to do
50     if not mapping:
51         raise NothingToDo
52
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
58     # goes along.
59
60     # Prepare the parent filter script
61     t = tempfile.NamedTemporaryFile(delete=False)
62     try:
63         t.write("#!/bin/sed -f\n")
64         for search, replace in mapping.items():
65             t.write("s/%s/%s/g\n" % (search, replace))
66         t.close()
67         shell.call("chmod", "a+x", t.name) # necessary?
68         logging.info("Sed script %s", t.name)
69         # Do the rewrite
70         maybe_force = []
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)
76     finally:
77         # Cleanup
78         os.unlink(t.name)
79
80 def parse_args(argv, baton):
81     usage = """usage: %prog remaster [ARGS]
82
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
87 necessary.
88
89 Consider this history:
90
91       D-----E   heads/master
92      /     /
93     A--B--C     remotes/origin/master
94
95 Suppose on a fetch, we discover that origin/master has been rebased, and
96 replaced with 'old-master':
97
98       D-----E   heads/master
99      /     /
100     A--B--C     remotes/origin/old-master
101
102     A'-B'-C'    remotes/origin/master
103
104 We would like to construct a new tree as follows:
105
106       D'----E'  heads/master
107      /     /
108     A'-B'-C'    remotes/origin/master
109
110 Where D/D' and E/E' have identical trees, just different parent commit
111 pointers.  This is what 'wizard remaster' does.
112
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).
121 """
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)
130     if len(args) > 0:
131         parser.error("too many arguments")
132     return options, args
133
134 ## -- Exceptions --
135
136 class Error(wizard.Error):
137     """Base error class for this module"""
138     pass
139
140 class NotWizardError(Error):
141     """The deployment was not a Wizard installation."""
142     def __str__(self):
143         return """
144
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
147 '--generic'."""
148
149 class NothingToDo(Error):
150     """No rewriting necessary."""
151     def __str__(self):
152         return """
153
154 ERROR: Nothing to do!"""