]> scripts.mit.edu Git - wizard.git/blob - wizard/command/upgrade.py
Implement virtual merging, and stdin in the shell.
[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 # XXX: WARNING EXPERIMENTAL DANGER DANGER WILL ROBINSON
13
14 # need errors for checking DAG integrity (if the user is on a completely
15 # different history tree, stuff is problems)
16
17 def main(argv, baton):
18     options, args = parse_args(argv)
19     dir = args[0]
20     command.chdir(dir)
21     if not os.path.isdir(".git"):
22         raise NotAutoinstallError()
23     try:
24         d = deploy.Deployment(".")
25     except IOError as e:
26         if e.errno == errno.ENOENT:
27             raise NotAutoinstallError()
28         else: raise e
29     repo = d.application.repository
30     # begin the command line process
31     sh = shell.Shell()
32     # setup environment
33     util.set_git_env()
34     # commit their changes
35     if not options.dry_run:
36         pre_upgrade_commit(sh)
37     # perform fetch to update repository state
38     sh.call("git", "fetch", repo)
39     # clone their website to a temporary directory
40     temp_dir = tempfile.mkdtemp(prefix="wizard")
41     temp_wc_dir = os.path.join(temp_dir, "repo")
42     logging.info("Using temporary directory: " + temp_wc_dir)
43     sh.call("git", "clone", "--shared", ".", temp_wc_dir)
44     with util.ChangeDirectory(temp_wc_dir):
45         if options.dry_run:
46             pre_upgrade_commit(sh)
47         # reconfigure the repository path
48         sh.call("git", "remote", "add", "scripts", repo)
49         sh.call("git", "fetch", "scripts")
50         # perform the merge
51         version = sh.call("git", "--git-dir="+repo, "describe", "--tags", "master")[0].rstrip()
52         user_commit = sh.call("git", "rev-parse", "HEAD")[0].rstrip()
53         next_commit = sh.call("git", "rev-parse", version)[0].rstrip()
54         message = "Upgraded autoinstall in %s to %s.\n\n%s" % (util.get_dir_owner(), version, util.get_git_footer())
55         try:
56             message += "\nUpgraded-by: " + util.get_operator_git()
57         except util.NoOperatorInfo:
58             pass
59         try:
60             # naive merge algorithm:
61             # sh.call("git", "merge", "-m", message, "scripts/master")
62             # crazy merge algorithm:
63             def make_virtual_commit(tag, parents = []):
64                 """WARNING: Changes state of Git repository"""
65                 sh.call("git", "checkout", tag, "--")
66                 d.parametrize(temp_wc_dir)
67                 for file in d.application.parametrized_files:
68                     try:
69                         sh.call("git", "add", "--", file)
70                     except shell.CallError:
71                         pass
72                 virtual_tree = sh.call("git", "write-tree")[0].rstrip()
73                 parent_args = itertools.chain(*(["-p", p] for p in parents))
74                 virtual_commit = sh.call("git", "commit-tree", virtual_tree, *parent_args, input="")[0].rstrip()
75                 sh.call("git", "reset", "--hard")
76                 return virtual_commit
77             user_tree = sh.call("git", "rev-parse", "HEAD^{tree}")[0].rstrip()
78             base_virtual_commit = make_virtual_commit("v" + str(d.version))
79             next_virtual_commit = make_virtual_commit(version, [base_virtual_commit])
80             user_virtual_commit = sh.call("git", "commit-tree", user_tree, "-p", base_virtual_commit, input="")[0].rstrip()
81             sh.call("git", "checkout", user_virtual_commit, "--")
82             sh.call("git", "merge", next_virtual_commit) # XXX
83         except shell.CallError:
84             print temp_wc_dir
85             raise MergeFailed
86         # Make it possible to resume here
87         new_tree = sh.call("git", "rev-parse", "HEAD^{tree}")[0].rstrip()
88         final_commit = sh.call("git", "commit-tree", new_tree, "-p", user_commit, "-p", next_commit, input=message)[0].rstrip()
89         sh.call("git", "checkout", "master")
90         sh.call("git", "reset", "--hard", final_commit)
91     # Till now, all of our operations were in a tmp sandbox.
92     if options.dry_run:
93         logging.info("Dry run, bailing.  See results at %s" % temp_wc_dir)
94         return
95     # XXX: frob .htaccess to make site inaccessible
96     # git merge (which performs a fast forward)
97     #   - merge could fail (race)
98     sh.call("git", "pull", temp_wc_dir, "master")
99     # run update script
100     sh.call(".scripts/update")
101     # XXX: frob .htaccess to make site accessible
102     # XXX:  - check if .htaccess changed, first.  Upgrade
103     #       process might have frobbed it.  Don't be
104     #       particularly worried if the segment dissappeared
105
106 def pre_upgrade_commit(sh):
107     try:
108         message = "Pre-commit of %s locker before autoinstall upgrade.\n\n%s" % (util.get_dir_owner(), util.get_git_footer())
109         try:
110             message += "\nPre-commit-by: " + util.get_operator_git()
111         except util.NoOperatorInfo:
112             pass
113         sh.call("git", "commit", "-a", "-m", message)
114     except shell.CallError:
115         logging.info("No changes detected")
116         pass
117
118 def parse_args(argv):
119     usage = """usage: %prog upgrade [ARGS] DIR
120
121 Upgrades an autoinstall to the latest version.  This involves
122 updating files and running .scripts/update.
123
124 WARNING: This is still experimental."""
125     parser = command.WizardOptionParser(usage)
126     parser.add_option("--dry-run", dest="dry_run", action="store_true",
127             default=False, help="Prints would would be run without changing anything")
128     options, args = parser.parse_all(argv)
129     if len(args) > 1:
130         parser.error("too many arguments")
131     elif not args:
132         parser.error("must specify directory")
133     return options, args
134
135 class Error(command.Error):
136     """Base exception for all exceptions raised by upgrade"""
137     pass
138
139 class NotAutoinstallError(Error):
140     def __str__(self):
141         return """
142
143 ERROR: Could not find .git file. Are you sure
144 this is an autoinstalled application? Did you remember
145 to migrate it?
146 """
147
148 class MergeFailed(Error):
149     def __str__(self):
150         return """
151
152 ERROR: Merge failed.  Change directory to the temporary
153 directory and manually resolve the merge.
154 """