update submodules for GHC.HetMet.GArrow -> Control.GArrow renaming
[ghc-hetmet.git] / utils / fingerprint / fingerprint.py
1 #! /usr/bin/env python
2 # Script to create and restore a git fingerprint of the ghc repositories.
3
4 from   datetime   import datetime
5 from   optparse   import OptionParser
6 import os
7 import os.path
8 import re
9 import subprocess
10 from   subprocess import PIPE, Popen
11 import sys
12
13 def main():
14   opts, args = parseopts(sys.argv[1:])
15   opts.action(opts)
16
17 def create_action(opts):
18   """Action called for the create commmand"""
19   if opts.fpfile:
20     fp = FingerPrint.read(opts.source)
21   else:
22     fp = fingerprint(opts.source)
23   if len(fp) == 0:
24     error("Got empty fingerprint from source: "+str(opts.source))
25   if opts.output_file:
26     print "Writing fingerprint to: ", opts.output_file
27   fp.write(opts.output)
28
29 def restore_action(opts):
30   """Action called for the restore commmand"""
31   def branch_name(filename):
32     return "fingerprint_" + os.path.basename(filename).replace(".", "_")
33   if opts.fpfile:
34     try:
35       fp = FingerPrint.read(opts.source)
36       bn = branch_name(opts.fpfile)
37     except MalformedFingerPrintError:
38       error("Error parsing fingerprint file: "+opts.fpfile)
39     if len(fp) == 0:
40       error("No fingerprint found in fingerprint file: "+opts.fpfile)
41   elif opts.logfile:
42     fp = fingerprint(opts.source)
43     bn = branch_name(opts.logfile)
44     if len(fp) == 0:
45       error("No fingerprint found in build log file: "+opts.logfile)
46   else:
47     error("Must restore from fingerprint or log file")
48   restore(fp, branch_name=bn if opts.branch else None)
49
50 def fingerprint(source=None):
51   """Create a new fingerprint of current repositories.
52
53   The source argument is parsed to look for the expected output
54   from a `sync-all` command. If the source is `None` then the
55   `sync-all` command will be run to get the current fingerprint.
56   """
57   if source is None:
58     sync_all = ["./sync-all", "log", "HEAD^..", "--pretty=oneline"]
59     source  = Popen(sync_all, stdout=PIPE).stdout
60
61   lib = ""
62   commits = {}
63   for line in source.readlines():
64     if line.startswith("=="):
65       lib = line.split()[1].rstrip(":")
66       lib = "." if lib == "running" else lib # hack for top ghc repo
67     elif re.match("[abcdef0-9]{40}", line):
68       commit = line[:40]
69       commits[lib] = commit
70   return FingerPrint(commits)
71
72 def restore(fp, branch_name=None):
73   """Restore the ghc repos to the commits in the fingerprint
74
75   This function performs a checkout of each commit specifed in
76   the fingerprint. If `branch_name` is not None then a new branch
77   will be created for the top ghc repository. We also add an entry
78   to the git config that sets the remote for the new branch as `origin`
79   so that the `sync-all` command can be used from the branch.
80   """
81   checkout = ["git", "checkout"]
82
83   # run checkout in all subdirs
84   for (subdir, commit) in fp:
85     if subdir != ".":
86       cmd = checkout + [commit]
87       print "==", subdir, " ".join(cmd)
88       if os.path.exists(subdir):
89         rc = subprocess.call(cmd, cwd=subdir)
90         if rc != 0:
91           error("Too many errors, aborting")
92       else:
93         sys.stderr.write("WARNING: "+
94           subdir+" is in fingerprint but missing in working directory\n")
95
96   # special handling for top ghc repo
97   # if we are creating a new branch then also add an entry to the
98   # git config so the sync-all command is happy
99   branch_args = ["-b", branch_name] if branch_name else []
100   rc = subprocess.call(checkout + branch_args + [fp["."]])
101   if (rc == 0) and branch_name:
102     branch_config = "branch."+branch_name+".remote"
103     subprocess.call(["git", "config", "--add", branch_config, "origin"])
104
105 actions = {"create" : create_action, "restore" : restore_action}
106 def parseopts(argv):
107   """Parse and check the validity of the command line arguments"""
108   usage = "fingerprint ("+"|".join(sorted(actions.keys()))+") [options]"
109   parser = OptionParser(usage=usage)
110
111   parser.add_option("-d", "--dir", dest="dir",
112     help="write output to directory DIR", metavar="DIR")
113
114   parser.add_option("-o", "--output", dest="output",
115     help="write output to file FILE", metavar="FILE")
116
117   parser.add_option("-l", "--from-log", dest="logfile",
118     help="reconstruct fingerprint from build log", metavar="FILE")
119
120   parser.add_option("-f", "--from-fp", dest="fpfile",
121     help="reconstruct fingerprint from fingerprint file", metavar="FILE")
122
123   parser.add_option("-n", "--no-branch",
124     action="store_false", dest="branch", default=True,
125     help="do not create a new branch when restoring fingerprint")
126
127   parser.add_option("-g", "--ghc-dir", dest="ghcdir",
128     help="perform actions in GHC dir", metavar="DIR")
129
130   opts,args = parser.parse_args(argv)
131   return (validate(opts, args, parser), args)
132
133 def validate(opts, args, parser):
134   """ Validate and prepare the command line options.
135
136   It performs the following actions:
137     * Check that we have a valid action to perform
138     * Check that we have a valid output destination
139     * Opens the output file if needed
140     * Opens the input  file if needed
141   """
142   # Determine the action
143   try:
144     opts.action = actions[args[0]]
145   except (IndexError, KeyError):
146     error("Must specify a valid action", parser)
147
148   # Inputs
149   if opts.logfile and opts.fpfile:
150     error("Must specify only one of -l and -f")
151
152   opts.source = None
153   if opts.logfile:
154     opts.source = file(opts.logfile, "r")
155   elif opts.fpfile:
156     opts.source = file(opts.fpfile, "r")
157
158   # Outputs
159   if opts.dir:
160     fname = opts.output
161     if fname is None:
162       fname = datetime.today().strftime("%Y-%m%-%d_%H-%M-%S") + ".fp"
163     path = os.path.join(opts.dir, fname)
164     opts.output_file = path
165     opts.output = file(path, "w")
166   elif opts.output:
167     opts.output_file = opts.output
168     opts.output = file(opts.output_file, "w")
169   else:
170     opts.output_file = None
171     opts.output = sys.stdout
172
173   # GHC Directory
174   # As a last step change the directory to the GHC directory specified
175   if opts.ghcdir:
176     os.chdir(opts.ghcdir)
177
178   return opts
179
180 def error(msg="fatal error", parser=None, exit=1):
181   """Function that prints error message and exits"""
182   print "ERROR:", msg
183   if parser:
184     parser.print_help()
185   sys.exit(exit)
186
187 class MalformedFingerPrintError(Exception):
188   """Exception raised when parsing a bad fingerprint file"""
189   pass
190
191 class FingerPrint:
192   """Class representing a fingerprint of all ghc git repos.
193
194   A finger print is represented by a dictionary that maps a
195   directory to a commit. The directory "." is used for the top
196   level ghc repository.
197   """
198   def __init__(self, subcommits = {}):
199     self.commits = subcommits
200
201   def __eq__(self, other):
202     if other.__class__ != self.__class__:
203       raise TypeError
204     return self.commits == other.commits
205
206   def __neq__(self, other):
207     not(self == other)
208
209   def __hash__(self):
210     return hash(str(self))
211
212   def __len__(self):
213     return len(self.commits)
214
215   def __repr__(self):
216     return "FingerPrint(" + repr(self.commits) + ")"
217
218   def __str__(self):
219     s = ""
220     for lib in sorted(self.commits.keys()):
221       commit = self.commits[lib]
222       s += "{0}|{1}\n".format(lib, commit)
223     return s
224
225   def __getitem__(self, item):
226     return self.commits[item]
227
228   def __iter__(self):
229     return self.commits.iteritems()
230
231   def write(self, outh):
232       outh.write(str(self))
233       outh.flush()
234
235   @staticmethod
236   def read(inh):
237     """Read a fingerprint from a fingerprint file"""
238     commits = {}
239     for line in inh.readlines():
240       splits = line.strip().split("|", 1)
241       if len(splits) != 2:
242         raise MalformedFingerPrintError(line)
243       lib, commit = splits
244       commits[lib] = commit
245     return FingerPrint(commits)
246
247 if __name__ == "__main__":
248   main()