Suggest providing a "test" command, not the old self-test attribute
[0release.git] / scm.py
blob6699478e477a5444a937b1f8c1025c1c65dd2050
1 # Copyright (C) 2007, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
4 import os, subprocess, tempfile
5 from zeroinstall import SafeException
6 from logging import info, warn
7 from support import unpack_tarball
9 class SCM:
10 def __init__(self, root_dir, options):
11 self.options = options
12 self.root_dir = root_dir
13 assert type(root_dir) == str, root_dir
15 class GIT(SCM):
16 def _run(self, args, **kwargs):
17 info("Running git %s (in %s)", ' '.join(args), self.root_dir)
18 return subprocess.Popen(["git"] + args, cwd = self.root_dir, **kwargs)
20 def _run_check(self, args, **kwargs):
21 child = self._run(args, **kwargs)
22 code = child.wait()
23 if code:
24 raise SafeException("Git %s failed with exit code %d" % (repr(args), code))
26 def _run_stdout(self, args, **kwargs):
27 child = self._run(args, stdout = subprocess.PIPE, **kwargs)
28 stdout, unused = child.communicate()
29 if child.returncode:
30 raise SafeException('Failed to get current branch! Exit code %d: %s' % (child.returncode, stdout))
31 return stdout
33 def ensure_versioned(self, path):
34 """Ensure path is a file tracked by the version control system.
35 @raise SafeException: if file is not tracked"""
36 out = self._run_stdout(['ls-tree', 'HEAD', path]).strip()
37 if not out:
38 raise SafeException("File '%s' is not under version control, according to git-ls-tree" % path)
40 def reset_hard(self, revision):
41 self._run_check(['reset', '--hard', revision])
43 def ensure_committed(self):
44 child = self._run(["status", "--porcelain", "-uno"], stdout = subprocess.PIPE)
45 stdout, unused = child.communicate()
46 if child.returncode == 0:
47 # Git >= 1.7
48 if stdout.strip():
49 raise SafeException('Uncommitted changes! Use "git-commit -a" to commit them. Changes are:\n' + stdout)
50 return
51 else:
52 # Old Git
53 child = self._run(["status", "-a"], stdout = subprocess.PIPE)
54 stdout, unused = child.communicate()
55 if not child.returncode:
56 raise SafeException('Uncommitted changes! Use "git-commit -a" to commit them. Changes are:\n' + stdout)
57 for scm in self._submodules():
58 scm.ensure_committed()
60 def _submodules(self):
61 for line in self._run_stdout(['submodule', 'status']).split('\n'):
62 if not line: continue
63 r, subdir = line.strip().split(' ')[:2]
64 scm = GIT(os.path.join(self.root_dir, subdir), self.options)
65 scm.rev = r
66 scm.rel_path = subdir
67 yield scm
69 def make_tag(self, version):
70 return 'v' + version
72 def tag(self, version, revision):
73 tag = self.make_tag(version)
74 if self.options.key:
75 key_opts = ['-u', self.options.key]
76 else:
77 key_opts = []
78 self._run_check(['tag', '-s'] + key_opts + ['-m', 'Release %s' % version, tag, revision])
79 print "Tagged as %s" % tag
81 def get_current_branch(self):
82 current_branch = self._run_stdout(['symbolic-ref', 'HEAD']).strip()
83 info("Current branch is %s", current_branch)
84 return current_branch
86 def get_tagged_versions(self):
87 child = self._run(['tag', '-l', 'v*'], stdout = subprocess.PIPE)
88 stdout, unused = child.communicate()
89 status = child.wait()
90 if status:
91 raise SafeException("git tag failed with exit code %d" % status)
92 return [v[1:] for v in stdout.split('\n') if v]
94 def delete_branch(self, branch):
95 self._run_check(['branch', '-D', branch])
97 def push_head_and_release(self, version):
98 self._run_check(['push', self.options.public_scm_repository, self.make_tag(version), self.get_current_branch()])
100 def ensure_no_tag(self, version):
101 tag = self.make_tag(version)
102 child = self._run(['tag', '-l', tag], stdout = subprocess.PIPE)
103 stdout, unused = child.communicate()
104 if tag in stdout.split('\n'):
105 raise SafeException(("Release %s is already tagged! If you want to replace it, do\n" +
106 "git tag -d %s") % (version, tag))
108 def export(self, prefix, archive_file, revision):
109 child = self._run(['archive', '--format=tar', '--prefix=' + prefix + '/', revision], stdout = subprocess.PIPE)
110 subprocess.check_call(['bzip2', '-'], stdin = child.stdout, stdout = file(archive_file, 'w'))
111 status = child.wait()
112 if status:
113 if os.path.exists(archive_file):
114 os.unlink(archive_file)
115 raise SafeException("git-archive failed with exit code %d" % status)
117 def export_submodules(self, target):
118 # Export all sub-modules under target
119 cwd = os.getcwd()
120 target = os.path.abspath(target)
121 for scm in self._submodules():
122 tmp = tempfile.NamedTemporaryFile(prefix = '0release-')
123 try:
124 scm.export(prefix = '.', archive_file = tmp.name, revision = scm.rev)
125 os.chdir(os.path.join(target, scm.rel_path))
126 unpack_tarball(tmp.name)
127 finally:
128 tmp.close()
129 os.chdir(cwd)
131 def commit(self, message, branch, parent):
132 self._run_check(['add', '-u']) # Commit all changed tracked files to index
133 tree = self._run_stdout(['write-tree']).strip()
134 child = self._run(['commit-tree', tree, '-p', parent], stdin = subprocess.PIPE, stdout = subprocess.PIPE)
135 stdout, unused = child.communicate(message)
136 commit = stdout.strip()
137 info("Committed as %s", commit)
138 self._run_check(['branch', '-f', branch, commit])
139 return commit
141 def get_head_revision(self):
142 proc = self._run(['rev-parse', 'HEAD'], stdout = subprocess.PIPE)
143 stdout, unused = proc.communicate()
144 if proc.returncode:
145 raise Exception("git rev-parse failed with exit code %d" % proc.returncode)
146 head = stdout.strip()
147 assert head
148 return head
150 def export_changelog(self, last_release_version, head, stream):
151 if last_release_version:
152 self._run_check(['log', 'refs/tags/v' + last_release_version + '..' + head], stdout = stream)
153 else:
154 self._run_check(['log', head], stdout = stream)
156 def grep(self, pattern):
157 child = self._run(['grep', pattern])
158 child.wait()
159 if child.returncode in [0, 1]:
160 return
161 warn("git grep returned exit code %d", child.returncode)
163 def has_submodules(self):
164 return os.path.isfile(os.path.join(self.root_dir, '.gitmodules'))
166 def get_scm(local_feed, options):
167 start_dir = os.path.dirname(os.path.abspath(local_feed.local_path))
168 current = start_dir
169 while True:
170 if os.path.exists(os.path.join(current, '.git')):
171 return GIT(current, options)
172 parent = os.path.dirname(current)
173 if parent == current:
174 raise SafeException("Unable to determine which version control system is being used. Couldn't find .git in %s or any parent directory." % start_dir)
175 current = parent