Add revision command.
[cdist.git] / cdist
1 #!/usr/bin/python
2
3 #    Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
4 #
5 #    This program is free software; you can redistribute it and/or modify
6 #    it under the terms of the GNU General Public License as published by
7 #    the Free Software Foundation; either version 2 of the License, or
8 #    (at your option) any later version.
9 #
10 #    This program is distributed in the hope that it will be useful,
11 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #    GNU General Public License for more details.
14
15 #    You should have received a copy of the GNU General Public License
16 #    along with this program; if not, write to the Free Software
17 #    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18
19 import os
20 import sys
21 import shutil
22 import glob
23 import tempfile
24 import argparse
25 import datetime
26 import subprocess
27 import re
28 import copy
29 import inspect
30
31 TEMPORARY_DIRECTORY = '/tmp'
32
33 class Error(Exception):
34     def __init__(self, value):
35         self.value = value
36     def __str__(self):
37         return '\x1b[31m%s\x1b[0m' % repr(self.value)
38     def __repr__(self):
39         return str(self)
40
41 #
42 # Configuration
43 #
44
45 class Option(object):
46     def __init__(self, key, default=None):
47         self.key = key
48         self.value = default
49
50     def offer(self, key, value):
51         if key == self.key:
52             self.value = value
53
54 class BoolOption(object):
55     def __init__(self, key):
56         self.key = key
57         self.value = False
58
59     def offer(self, key, value):
60         if key == self.key:
61             self.value = (value == 'yes' or value == '1' or value == 'true')
62
63 class Config:
64     def __init__(self):
65         self.options = [ Option('linux_chroot_prefix'),
66                          Option('windows_environment_prefix'),
67                          Option('mingw_prefix'),
68                          Option('git_prefix'),
69                          Option('osx_build_host'),
70                          Option('osx_environment_prefix'),
71                          Option('osx_sdk_prefix'),
72                          Option('osx_sdk'),
73                          Option('parallel', 4) ]
74
75         try:
76             f = open('%s/.config/cdist' % os.path.expanduser('~'), 'r')
77             while 1:
78                 l = f.readline()
79                 if l == '':
80                     break
81
82                 if len(l) > 0 and l[0] == '#':
83                     continue
84
85                 s = l.strip().split()
86                 if len(s) == 2:
87                     for k in self.options:
88                         k.offer(s[0], s[1])
89         except:
90             raise
91
92     def get(self, k):
93         for o in self.options:
94             if o.key == k:
95                 return o.value
96
97         raise Error('Required setting %s not found' % k)
98
99 config = Config()
100
101 #
102 # Utility bits
103
104
105 def log(m):
106     if not args.quiet:
107         print '\x1b[33m* %s\x1b[0m' % m
108
109 def copytree(a, b):
110     log('copy %s -> %s' % (a, b))
111     shutil.copytree(a, b)
112
113 def copyfile(a, b):
114     log('copy %s -> %s' % (a, b))
115     shutil.copyfile(a, b)
116
117 def rmdir(a):
118     log('remove %s' % a)
119     os.rmdir(a)
120
121 def rmtree(a):
122     log('remove %s' % a)
123     shutil.rmtree(a, ignore_errors=True)
124
125 def command(c, can_fail=False):
126     log(c)
127     r = os.system(c)
128     if (r >> 8) and not can_fail:
129         raise Error('command %s failed' % c)
130
131 def command_and_read(c):
132     log(c)
133     p = subprocess.Popen(c.split(), stdout=subprocess.PIPE)
134     f = os.fdopen(os.dup(p.stdout.fileno()))
135     return f
136
137 def read_wscript_variable(directory, variable):
138     f = open('%s/wscript' % directory, 'r')
139     while 1:
140         l = f.readline()
141         if l == '':
142             break
143         
144         s = l.split()
145         if len(s) == 3 and s[0] == variable:
146             f.close()
147             return s[2][1:-1]
148
149     f.close()
150     return None
151
152 #
153 # Version
154 #
155
156 class Version:
157     def __init__(self, s):
158         self.devel = False
159
160         if s.startswith("'"):
161             s = s[1:]
162         if s.endswith("'"):
163             s = s[0:-1]
164         
165         if s.endswith('devel'):
166             s = s[0:-5]
167             self.devel = True
168
169         if s.endswith('pre'):
170             s = s[0:-3]
171
172         p = s.split('.')
173         self.major = int(p[0])
174         self.minor = int(p[1])
175         if len(p) == 3:
176             self.micro = int(p[2])
177         else:
178             self.micro = 0
179
180     def bump_minor(self):
181         self.minor += 1
182         self.micro = 0
183
184     def bump_micro(self):
185         self.micro += 1
186
187     def to_devel(self):
188         self.devel = True
189
190     def to_release(self):
191         self.devel = False
192
193     def __str__(self):
194         s = '%d.%d.%d' % (self.major, self.minor, self.micro)
195         if self.devel:
196             s += 'devel'
197
198         return s
199
200 #
201 # Targets
202 #
203
204 class Target(object):
205     # @param directory directory to work in; if None we will use a temporary directory
206     # Temporary directories will be removed after use; specified directories will not
207     def __init__(self, platform, directory=None):
208         self.platform = platform
209         self.parallel = int(config.get('parallel'))
210
211         if directory is None:
212             self.directory = tempfile.mkdtemp('', 'tmp', TEMPORARY_DIRECTORY)
213             self.rmdir = True
214         else:
215             self.directory = directory
216             self.rmdir = False
217
218         # Environment variables that we will use when we call cscripts
219         self.variables = {}
220         self.debug = False
221
222     def build_dependencies(self, project):
223         cwd = os.getcwd()
224         if 'dependencies' in project.cscript:
225             for d in project.cscript['dependencies'](self):
226                 log('Building dependency %s %s of %s' % (d[0], d[1], project.name))
227                 dep = Project(d[0], '.', d[1])
228                 dep.checkout(self)
229                 self.build_dependencies(dep)
230
231                 # Make the options to pass in from the option_defaults of the thing
232                 # we are building and any options specified by the parent.
233                 options = {}
234                 if 'option_defaults' in dep.cscript:
235                     options = dep.cscript['option_defaults']()
236                     if len(d) > 2:
237                         for k, v in d[2].iteritems():
238                             options[k] = v
239
240                 self.build(dep, options)
241
242         os.chdir(cwd)
243
244     def build(self, project, options=None):
245         variables = copy.copy(self.variables)
246         if len(inspect.getargspec(project.cscript['build']).args) == 2:
247             project.cscript['build'](self, options)
248         else:
249             project.cscript['build'](self)
250         self.variables = variables
251
252     def package(self, project):
253         project.checkout(self)
254         self.build_dependencies(project)
255         self.build(project)
256         return project.cscript['package'](self, project.version)
257
258     def test(self, project):
259         project.checkout(self)
260         self.build_dependencies(project)
261         self.build(project)
262         project.cscript['test'](self)
263
264     def set(self, a, b):
265         self.variables[a] = b
266
267     def unset(self, a):
268         del(self.variables[a])
269
270     def get(self, a):
271         return self.variables[a]
272
273     def append_with_space(self, k, v):
274         if not k in self.variables:
275             self.variables[k] = v
276         else:
277             self.variables[k] = '%s %s' % (self.variables[k], v)
278
279     def variables_string(self, escaped_quotes=False):
280         e = ''
281         for k, v in self.variables.iteritems():
282             if escaped_quotes:
283                 v = v.replace('"', '\\"')
284             e += '%s=%s ' % (k, v)
285         return e
286
287     def cleanup(self):
288         if self.rmdir:
289             rmtree(self.directory)
290
291
292 # Windows
293 #
294
295 class WindowsTarget(Target):
296     def __init__(self, bits, directory=None):
297         super(WindowsTarget, self).__init__('windows', directory)
298         self.bits = bits
299
300         self.windows_prefix = '%s/%d' % (config.get('windows_environment_prefix'), self.bits)
301         if not os.path.exists(self.windows_prefix):
302             raise Error('windows prefix %s does not exist' % self.windows_prefix)
303             
304         if self.bits == 32:
305             self.mingw_name = 'i686'
306         else:
307             self.mingw_name = 'x86_64'
308
309         mingw_path = '%s/%d/bin' % (config.get('mingw_prefix'), self.bits)
310         self.mingw_prefixes = ['/%s/%d' % (config.get('mingw_prefix'), self.bits), '%s/%d/%s-w64-mingw32' % (config.get('mingw_prefix'), bits, self.mingw_name)]
311
312         self.set('PKG_CONFIG_LIBDIR', '%s/lib/pkgconfig' % self.windows_prefix)
313         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/bin/pkgconfig' % (self.directory, self.directory))
314         self.set('PATH', '%s/bin:%s:%s' % (self.windows_prefix, mingw_path, os.environ['PATH']))
315         self.set('CC', '%s-w64-mingw32-gcc' % self.mingw_name)
316         self.set('CXX', '%s-w64-mingw32-g++' % self.mingw_name)
317         self.set('LD', '%s-w64-mingw32-ld' % self.mingw_name)
318         self.set('RANLIB', '%s-w64-mingw32-ranlib' % self.mingw_name)
319         self.set('WINRC', '%s-w64-mingw32-windres' % self.mingw_name)
320         cxx = '-I%s/include -I%s/include' % (self.windows_prefix, self.directory)
321         link = '-L%s/lib -L%s/lib' % (self.windows_prefix, self.directory)
322         for p in self.mingw_prefixes:
323             cxx += ' -I%s/include' % p
324             link += ' -L%s/lib' % p
325         self.set('CXXFLAGS', '"%s"' % cxx)
326         self.set('CPPFLAGS', '')
327         self.set('LINKFLAGS', '"%s"' % link)
328
329     def command(self, c):
330         log('host -> %s' % c)
331         command('%s %s' % (self.variables_string(), c))
332
333 #
334 # Linux
335 #
336
337 class LinuxTarget(Target):
338     def __init__(self, distro, version, bits, directory=None):
339         super(LinuxTarget, self).__init__('linux', directory)
340         self.distro = distro
341         self.version = version
342         self.bits = bits
343         # e.g. ubuntu-14.04-64
344         self.chroot = '%s-%s-%d' % (self.distro, self.version, self.bits)
345         # e.g. /home/carl/Environments/ubuntu-14.04-64
346         self.chroot_prefix = '%s/%s' % (config.get('linux_chroot_prefix'), self.chroot)
347
348         self.set('CXXFLAGS', '-I%s/include' % self.directory)
349         self.set('CPPFLAGS', '')
350         self.set('LINKFLAGS', '-L%s/lib' % self.directory)
351         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:/usr/local/lib/pkgconfig' % self.directory)
352         self.set('PATH', '%s:/usr/local/bin' % (os.environ['PATH']))
353
354     def command(self, c):
355         command('%s schroot -c %s -p -- %s' % (self.variables_string(), self.chroot, c))
356
357 #
358 # OS X
359 #
360
361 class OSXTarget(Target):
362     def __init__(self, directory=None):
363         super(OSXTarget, self).__init__('osx', directory)
364
365     def command(self, c):
366         command('%s %s' % (self.variables_string(False), c))
367
368
369 class OSXSingleTarget(OSXTarget):
370     def __init__(self, bits, directory=None):
371         super(OSXSingleTarget, self).__init__(directory)
372         self.bits = bits
373
374         if bits == 32:
375             arch = 'i386'
376         else:
377             arch = 'x86_64'
378
379         flags = '-isysroot %s/MacOSX%s.sdk -arch %s' % (config.get('osx_sdk_prefix'), config.get('osx_sdk'), arch)
380         enviro = '%s/%d' % (config.get('osx_environment_prefix'), bits)
381
382         # Environment variables
383         self.set('CFLAGS', '"-I%s/include -I%s/include %s"' % (self.directory, enviro, flags))
384         self.set('CPPFLAGS', '')
385         self.set('CXXFLAGS', '"-I%s/include -I%s/include %s"' % (self.directory, enviro, flags))
386         self.set('LDFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
387         self.set('LINKFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
388         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/lib/pkgconfig:/usr/lib/pkgconfig' % (self.directory, enviro))
389         self.set('PATH', '$PATH:/usr/bin:/sbin:/usr/local/bin:%s/bin' % enviro)
390         self.set('MACOSX_DEPLOYMENT_TARGET', config.get('osx_sdk'))
391
392     def package(self, project):
393         raise Error('cannot package non-universal OS X versions')
394
395
396 class OSXUniversalTarget(OSXTarget):
397     def __init__(self, directory=None):
398         super(OSXUniversalTarget, self).__init__(directory)
399         self.parts = []
400         self.parts.append(OSXSingleTarget(32, os.path.join(self.directory, '32')))
401         self.parts.append(OSXSingleTarget(64, os.path.join(self.directory, '64')))
402
403     def package(self, project):
404         for p in self.parts:
405             project.checkout(p)
406             p.build_dependencies(project)
407             p.build(project)
408
409         return project.cscript['package'](self, project.version)
410     
411
412 #
413 # Source
414 #
415
416 class SourceTarget(Target):
417     def __init__(self):
418         super(SourceTarget, self).__init__('source')
419
420     def command(self, c):
421         log('host -> %s' % c)
422         command('%s %s' % (self.variables_string(), c))
423
424     def cleanup(self):
425         rmtree(self.directory)
426
427     def package(self, project):
428         project.checkout(self)
429         name = read_wscript_variable(os.getcwd(), 'APPNAME')
430         command('./waf dist')
431         return os.path.abspath('%s-%s.tar.bz2' % (name, project.version))
432
433
434 # @param s Target string:
435 #       windows-{32,64}
436 #    or ubuntu-version-{32,64}
437 #    or debian-version-{32,64}
438 #    or centos-version-{32,64}
439 #    or osx-{32,64}
440 #    or source      
441 # @param debug True to build with debugging symbols (where possible)
442 def target_factory(s, debug, work):
443     target = None
444     if s.startswith('windows-'):
445         target = WindowsTarget(int(s.split('-')[1]), work)
446     elif s.startswith('ubuntu-') or s.startswith('debian-') or s.startswith('centos-'):
447         p = s.split('-')
448         if len(p) != 3:
449             print >>sys.stderr,"Bad Linux target name `%s'; must be something like ubuntu-12.04-32 (i.e. distro-version-bits)" % s
450             sys.exit(1)
451         target = LinuxTarget(p[0], p[1], int(p[2]), work)
452     elif s.startswith('osx-'):
453         target = OSXSingleTarget(int(s.split('-')[1]), work)
454     elif s == 'osx':
455         if args.command == 'build':
456             target = OSXSingleTarget(64, work)
457         else:
458             target = OSXUniversalTarget(work)
459     elif s == 'source':
460         target = SourceTarget()
461
462     if target is not None:
463         target.debug = debug
464
465     return target
466
467
468 #
469 # Project
470 #
471  
472 class Project(object):
473     def __init__(self, name, directory, specifier=None):
474         self.name = name
475         self.directory = directory
476         self.version = None
477         self.specifier = specifier
478         self.git_commit = None
479         if self.specifier is None:
480             self.specifier = 'master'
481
482     def checkout(self, target):
483         flags = ''
484         redirect = ''
485         if args.quiet:
486             flags = '-q'
487             redirect = '>/dev/null'
488         command('git clone %s %s/%s.git %s/src/%s' % (flags, config.get('git_prefix'), self.name, target.directory, self.name))
489         os.chdir('%s/src/%s' % (target.directory, self.name))
490         command('git checkout %s %s %s' % (flags, self.specifier, redirect))
491         self.git_commit = command_and_read('git rev-parse --short=7 HEAD').readline().strip()
492         command('git submodule init --quiet')
493         command('git submodule update --quiet')
494         os.chdir(self.directory)
495
496         proj = '%s/src/%s/%s' % (target.directory, self.name, self.directory)
497
498         self.read_cscript('%s/cscript' % proj)
499         
500         if os.path.exists('%s/wscript' % proj):
501             v = read_wscript_variable(proj, "VERSION");
502             if v is not None:
503                 self.version = Version(v)
504
505     def read_cscript(self, s):
506         self.cscript = {}
507         execfile(s, self.cscript)
508
509 def set_version_in_wscript(version):
510     f = open('wscript', 'rw')
511     o = open('wscript.tmp', 'w')
512     while 1:
513         l = f.readline()
514         if l == '':
515             break
516
517         s = l.split()
518         if len(s) == 3 and s[0] == "VERSION":
519             print "Writing %s" % version
520             print >>o,"VERSION = '%s'" % version
521         else:
522             print >>o,l,
523     f.close()
524     o.close()
525
526     os.rename('wscript.tmp', 'wscript')
527
528 def append_version_to_changelog(version):
529     try:
530         f = open('ChangeLog', 'r')
531     except:
532         log('Could not open ChangeLog')
533         return
534
535     c = f.read()
536     f.close()
537
538     f = open('ChangeLog', 'w')
539     now = datetime.datetime.now()
540     f.write('%d-%02d-%02d  Carl Hetherington  <cth@carlh.net>\n\n\t* Version %s released.\n\n' % (now.year, now.month, now.day, version))
541     f.write(c)
542
543 def append_version_to_debian_changelog(version):
544     if not os.path.exists('debian'):
545         log('Could not find debian directory')
546         return
547
548     command('dch -b -v %s-1 "New upstream release."' % version)
549
550 def devel_to_git(project, filename):
551     if project.git_commit is not None:
552         filename = filename.replace('devel', '-%s' % project.git_commit)
553     return filename
554
555 #
556 # Command-line parser
557 #
558
559 parser = argparse.ArgumentParser()
560 parser.add_argument('command')
561 parser.add_argument('-p', '--project', help='project name')
562 parser.add_argument('-d', '--directory', help='directory within project repo', default='.')
563 parser.add_argument('--minor', help='minor version number bump', action='store_true')
564 parser.add_argument('--micro', help='micro version number bump', action='store_true')
565 parser.add_argument('--major', help='major version to return with latest', type=int)
566 parser.add_argument('-c', '--checkout', help='string to pass to git for checkout')
567 parser.add_argument('-o', '--output', help='output directory', default='.')
568 parser.add_argument('-q', '--quiet', help='be quiet', action='store_true')
569 parser.add_argument('-t', '--target', help='target')
570 parser.add_argument('-k', '--keep', help='keep working tree', action='store_true')
571 parser.add_argument('--debug', help='build with debugging symbols where possible', action='store_true')
572 parser.add_argument('-w', '--work', help='override default work directory')
573 args = parser.parse_args()
574
575 args.output = os.path.abspath(args.output)
576 if args.work is not None:
577     args.work = os.path.abspath(args.work)
578
579 if args.project is None and args.command != 'shell':
580     raise Error('you must specify -p or --project')
581
582 project = Project(args.project, args.directory, args.checkout)
583
584 if args.command == 'build':
585     if args.target is None:
586         raise Error('you must specify -t or --target')
587
588     target = target_factory(args.target, args.debug, args.work)
589     project.checkout(target)
590     target.build_dependencies(project)
591     target.build(project)
592     if not args.keep:
593         target.cleanup()
594
595 elif args.command == 'package':
596     if args.target is None:
597         raise Error('you must specify -t or --target')
598         
599     target = target_factory(args.target, args.debug, args.work)
600
601     packages = target.package(project)
602     if hasattr(packages, 'strip') or (not hasattr(packages, '__getitem__') and not hasattr(packages, '__iter__')):
603         packages = [packages]
604
605     if target.platform == 'linux':
606         out = '%s/%s-%s-%d' % (args.output, target.distro, target.version, target.bits)
607         try:
608             os.makedirs(out)
609         except:
610             pass
611         for p in packages:
612             copyfile(p, '%s/%s' % (out, os.path.basename(devel_to_git(project, p))))
613     else:
614         for p in packages:
615             copyfile(p, '%s/%s' % (args.output, os.path.basename(devel_to_git(project, p))))
616
617     if not args.keep:
618         target.cleanup()
619
620 elif args.command == 'release':
621     if args.minor is False and args.micro is False:
622         raise Error('you must specify --minor or --micro')
623
624     target = SourceTarget()
625     project.checkout(target)
626
627     version = project.version
628     version.to_release()
629     if args.minor:
630         version.bump_minor()
631     else:
632         version.bump_micro()
633
634     set_version_in_wscript(version)
635     append_version_to_changelog(version)
636     append_version_to_debian_changelog(version)
637
638     command('git commit -a -m "Bump version"')
639     command('git tag -m "v%s" v%s' % (version, version))
640
641     version.to_devel()
642     set_version_in_wscript(version)
643     command('git commit -a -m "Bump version"')
644     command('git push')
645     command('git push --tags')
646
647     target.cleanup()
648
649 elif args.command == 'pot':
650     target = SourceTarget()
651     project.checkout(target)
652
653     pots = project.cscript['make_pot'](target)
654     for p in pots:
655         copyfile(p, '%s/%s' % (args.output, os.path.basename(p)))
656
657     target.cleanup()
658
659 elif args.command == 'changelog':
660     target = SourceTarget()
661     project.checkout(target)
662
663     text = open('ChangeLog', 'r')
664     html = open('%s/changelog.html' % args.output, 'w')
665     versions = 8
666     
667     last = None
668     changes = []
669     
670     while 1:
671         l = text.readline()
672         if l == '':
673             break
674     
675         if len(l) > 0 and l[0] == "\t":
676             s = l.split()
677             if len(s) == 4 and s[1] == "Version" and s[3] == "released.":
678                 v = Version(s[2])
679                 if v.micro == 0:
680                     if last is not None and len(changes) > 0:
681                         print >>html,"<h2>Changes between version %s and %s</h2>" % (s[2], last)
682                         print >>html,"<ul>"
683                         for c in changes:
684                             print >>html,"<li>%s" % c
685                         print >>html,"</ul>"
686                     last = s[2]
687                     changes = []
688                     versions -= 1
689                     if versions < 0:
690                         break
691             else:
692                 c = l.strip()
693                 if len(c) > 0:
694                     if c[0] == '*':
695                         changes.append(c[2:])
696                     else:
697                         changes[-1] += " " + c
698
699     target.cleanup()
700
701 elif args.command == 'manual':
702     target = SourceTarget()
703     project.checkout(target)
704
705     outs = project.cscript['make_manual'](target)
706     for o in outs:
707         if os.path.isfile(o):
708             copyfile(o, '%s/%s' % (args.output, os.path.basename(o)))
709         else:
710             copytree(o, '%s/%s' % (args.output, os.path.basename(o)))
711
712     target.cleanup()
713
714 elif args.command == 'doxygen':
715     target = SourceTarget()
716     project.checkout(target)
717
718     dirs = project.cscript['make_doxygen'](target)
719     if hasattr(dirs, 'strip') or (not hasattr(dirs, '__getitem__') and not hasattr(dirs, '__iter__')):
720         dirs = [dirs]
721
722     for d in dirs:
723         copytree(d, '%s/%s' % (args.output, 'doc'))
724
725     target.cleanup()
726
727 elif args.command == 'latest':
728     target = SourceTarget()
729     project.checkout(target)
730
731     f = command_and_read('git log --tags --simplify-by-decoration --pretty="%d"')
732     latest = None
733     while latest is None:
734         t = f.readline()
735         m = re.compile(".*\((.*)\).*").match(t)
736         if m:
737             tags = m.group(1).split(', ')
738             for t in tags:
739                 s = t.split()
740                 if len(s) > 1:
741                     t = s[1]
742                 if len(t) > 0 and t[0] == 'v':
743                     v = Version(t[1:])
744                     if args.major is None or v.major == args.major:
745                         latest = v
746
747     print latest
748     target.cleanup()
749
750 elif args.command == 'test':
751     if args.target is None:
752         raise Error('you must specify -t or --target')
753
754     target = None
755     try:
756         target = target_factory(args.target, args.debug, args.work)
757         target.test(project)
758     except Error as e:
759         if target is not None:
760             target.cleanup()
761         raise
762         
763     if target is not None:
764         target.cleanup()
765
766 elif args.command == 'shell':
767     if args.target is None:
768         raise Error('you must specify -t or --target')
769
770     target = target_factory(args.target, args.debug, args.work)
771     target.command('bash')
772
773 elif args.command == 'revision':
774
775     target = SourceTarget()
776     project.checkout(target)
777     print command_and_read('git rev-parse HEAD').readline().strip()[:7]
778     target.cleanup()
779
780 else:
781     raise Error('invalid command %s' % args.command)