]> scripts.mit.edu Git - wizard.git/blob - wizard/command/upgrade.py
Implement mass-upgrade.
[wizard.git] / wizard / command / upgrade.py
1 import optparse
2 import sys
3 import os
4 import shutil
5 import logging.handlers
6 import errno
7 import tempfile
8 import itertools
9
10 from wizard import command, deploy, shell, util
11
12 def main(argv, baton):
13     options, args = parse_args(argv, baton)
14     if args:
15         dir = args[0]
16     else:
17         dir = "."
18     shell.drop_priviledges(dir, options.log_file)
19     util.chdir(dir)
20     sh = shell.Shell()
21     util.set_git_env()
22     if options.continue_:
23         temp_wc_dir = os.getcwd()
24         user_commit, next_commit = open(".git/WIZARD_PARENTS", "r").read().split()
25         repo = open(".git/WIZARD_REPO", "r").read()
26         version = open(".git/WIZARD_UPGRADE_VERSION", "r").read()
27         util.chdir(sh.eval("git", "config", "remote.origin.url"))
28         d = deploy.Deployment(".")
29         try:
30             sh.call("git", "status")
31             raise LocalChangesError()
32         except shell.CallError:
33             pass
34     else:
35         d = deploy.Deployment(".")
36         d.verify()
37         d.verifyTag(options.srv_path)
38         d.verifyGit(options.srv_path)
39         d.verifyConfigured()
40         d.verifyVersion()
41         repo = d.application.repository(options.srv_path)
42         version = calculate_newest_version(sh, repo)
43         if version == d.app_version.scripts_tag:
44             raise AlreadyUpgraded
45         if not options.dry_run:
46             perform_pre_commit(sh)
47         temp_wc_dir = perform_tmp_clone(sh)
48         with util.ChangeDirectory(temp_wc_dir):
49             sh.call("git", "remote", "add", "scripts", repo)
50             sh.call("git", "fetch", "scripts")
51             user_commit, next_commit = calculate_parents(sh, version)
52             # save variables so that --continue will work
53             # yeah yeah no trailing newline whatever
54             open(".git/WIZARD_REPO", "w").write(repo)
55             open(".git/WIZARD_UPGRADE_VERSION", "w").write(version)
56             open(".git/WIZARD_PARENTS", "w").write("%s\n%s" % (user_commit, next_commit))
57             perform_merge(sh, repo, d, version)
58     # variables: version, user_commit, next_commit, temp_wc_dir
59     with util.ChangeDirectory(temp_wc_dir):
60         message = make_commit_message(version)
61         new_tree = sh.eval("git", "rev-parse", "HEAD^{tree}")
62         final_commit = sh.eval("git", "commit-tree", new_tree,
63                 "-p", user_commit, "-p", next_commit, input=message, log=True)
64         # a master branch may not necessarily exist if the user
65         # was manually installed to an earlier version
66         try:
67             sh.call("git", "checkout", "-b", "master", "--")
68         except shell.CallError:
69             sh.call("git", "checkout", "master", "--")
70         sh.call("git", "reset", "--hard", final_commit)
71     # Till now, all of our operations were in a tmp sandbox.
72     if options.dry_run:
73         logging.info("Dry run, bailing.  See results at %s" % temp_wc_dir)
74         return
75     # XXX: frob .htaccess to make site inaccessible
76     # XXX: need locking
77     # git merge (which performs a fast forward)
78     #   - merge could fail (race); that's /really/ dangerous.
79     sh.call("git", "pull", temp_wc_dir, "master")
80     # run update script
81     d.application.upgrade(options)
82     # XXX: frob .htaccess to make site accessible
83     # XXX:  - check if .htaccess changed, first.  Upgrade
84     #       process might have frobbed it.  Don't be
85     #       particularly worried if the segment dissappeared
86
87 def make_commit_message(version):
88     message = "Upgraded autoinstall in %s to %s.\n\n%s" % (util.get_dir_owner(), version, util.get_git_footer())
89     try:
90         message += "\nUpgraded-by: " + util.get_operator_git()
91     except util.NoOperatorInfo:
92         pass
93     return message
94
95 def calculate_newest_version(sh, repo):
96     # XXX: put this in Application
97     return sh.eval("git", "--git-dir="+repo, "describe", "--tags", "master")
98
99 def calculate_parents(sh, version):
100     user_commit = sh.eval("git", "rev-parse", "HEAD")
101     next_commit = sh.eval("git", "rev-parse", version)
102     return user_commit, next_commit
103
104 def perform_pre_commit(sh):
105     message = "Pre-commit of %s locker before autoinstall upgrade.\n\n%s" % (util.get_dir_owner(), util.get_git_footer())
106     try:
107         message += "\nPre-commit-by: " + util.get_operator_git()
108     except util.NoOperatorInfo:
109         pass
110     try:
111         sh.call("git", "commit", "-a", "-m", message)
112     except shell.CallError:
113         logging.info("No changes detected")
114
115 def perform_tmp_clone(sh):
116     temp_dir = tempfile.mkdtemp(prefix="wizard")
117     temp_wc_dir = os.path.join(temp_dir, "repo")
118     logging.info("Using temporary directory: " + temp_wc_dir)
119     sh.call("git", "clone", "--shared", ".", temp_wc_dir)
120     return temp_wc_dir
121
122 def perform_merge(sh, repo, d, version):
123     # naive merge algorithm:
124     # sh.call("git", "merge", "-m", message, "scripts/master")
125     # crazy merge algorithm:
126     def make_virtual_commit(tag, parents = []):
127         """WARNING: Changes state of Git repository"""
128         sh.call("git", "checkout", tag, "--")
129         d.parametrize(".")
130         for file in d.application.parametrized_files:
131             try:
132                 sh.call("git", "add", "--", file)
133             except shell.CallError:
134                 pass
135         virtual_tree = sh.eval("git", "write-tree", log=True)
136         parent_args = itertools.chain(*(["-p", p] for p in parents))
137         virtual_commit = sh.eval("git", "commit-tree", virtual_tree,
138                 *parent_args, input="", log=True)
139         sh.call("git", "reset", "--hard")
140         return virtual_commit
141     user_tree = sh.eval("git", "rev-parse", "HEAD^{tree}")
142     base_virtual_commit = make_virtual_commit(d.app_version.scripts_tag)
143     next_virtual_commit = make_virtual_commit(version, [base_virtual_commit])
144     user_virtual_commit = sh.eval("git", "commit-tree", user_tree,
145             "-p", base_virtual_commit, input="", log=True)
146     sh.call("git", "checkout", user_virtual_commit, "--")
147     try:
148         sh.call("git", "merge", next_virtual_commit)
149     except shell.CallError:
150         print os.getcwd()
151         logging.info("Conflict info:\n", sh.eval("git", "diff"))
152         raise MergeFailed
153
154 def parse_args(argv, baton):
155     usage = """usage: %prog upgrade [ARGS] [DIR]
156
157 Upgrades an autoinstall to the latest version.  This involves
158 updating files and running .scripts/update.
159
160 WARNING: This is still experimental."""
161     parser = command.WizardOptionParser(usage)
162     parser.add_option("--dry-run", dest="dry_run", action="store_true",
163             default=False, help="Prints would would be run without changing anything")
164     # notice trailing underscore
165     parser.add_option("--continue", dest="continue_", action="store_true",
166             default=False, help="Continues an upgrade that has had its merge manually "
167             "resolved using the current working directory as the resolved copy.")
168     baton.push(parser, "srv_path")
169     options, args = parser.parse_all(argv)
170     if len(args) > 1:
171         parser.error("too many arguments")
172     return options, args
173
174 class Error(command.Error):
175     """Base exception for all exceptions raised by upgrade"""
176     pass
177
178 class AlreadyUpgraded(Error):
179     def __str__(self):
180         return """
181
182 ERROR: This autoinstall is already at the latest version."""
183
184 class MergeFailed(Error):
185     def __str__(self):
186         return """
187
188 ERROR: Merge failed.  Resolve the merge by cd'ing to the
189 temporary directory, finding conflicted files with `git status`,
190 resolving the files, adding them using `git add`, and then
191 committing your changes with `git commit` (your log message
192 will be ignored), and then running `wizard upgrade --continue`."""
193
194 class LocalChangesError(Error):
195     def __str__(self):
196         return """
197
198 ERROR: Local changes occurred in the install while the merge was
199 being processed so that a pull would not result in a fast-forward.
200 The best way to resolve this is probably to attempt an upgrade again,
201 with git rerere to remember merge resolutions (XXX: not sure if
202 this actually works)."""
203