chiark / gitweb /
Execute the 'git ...' rather than 'git-...'
[stgit] / stgit / git.py
CommitLineData
41a6d859
CM
1"""Python GIT interface
2"""
3
4__copyright__ = """
5Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
6
7This program is free software; you can redistribute it and/or modify
8it under the terms of the GNU General Public License version 2 as
9published by the Free Software Foundation.
10
11This program is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14GNU General Public License for more details.
15
16You should have received a copy of the GNU General Public License
17along with this program; if not, write to the Free Software
18Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19"""
20
f0de3f92 21import sys, os, re, gitmergeonefile
d5ae2173 22from shutil import copyfile
41a6d859 23
87c93eab 24from stgit.exception import *
170f576b 25from stgit import basedir
41a6d859 26from stgit.utils import *
5e888f30 27from stgit.out import *
f0de3f92 28from stgit.run import *
b3bfa120 29from stgit.config import config
41a6d859
CM
30
31# git exception class
87c93eab 32class GitException(StgException):
41a6d859
CM
33 pass
34
f0de3f92
KH
35# When a subprocess has a problem, we want the exception to be a
36# subclass of GitException.
37class GitRunException(GitException):
38 pass
39class GRun(Run):
40 exc = GitRunException
1576d681
CM
41 def __init__(self, *cmd):
42 """Initialise the Run object and insert the 'git' command name.
43 """
44 Run.__init__(self, 'git', *cmd)
41a6d859 45
41a6d859 46
41a6d859
CM
47#
48# Classes
49#
9e3f506f
KH
50
51class Person:
52 """An author, committer, etc."""
53 def __init__(self, name = None, email = None, date = '',
54 desc = None):
5cd9e87f 55 self.name = self.email = self.date = None
9e3f506f
KH
56 if name or email or date:
57 assert not desc
58 self.name = name
59 self.email = email
60 self.date = date
61 elif desc:
62 assert not (name or email or date)
63 def parse_desc(s):
64 m = re.match(r'^(.+)<(.+)>(.*)$', s)
65 assert m
66 return [x.strip() or None for x in m.groups()]
67 self.name, self.email, self.date = parse_desc(desc)
68 def set_name(self, val):
69 if val:
70 self.name = val
71 def set_email(self, val):
72 if val:
73 self.email = val
74 def set_date(self, val):
75 if val:
76 self.date = val
77 def __str__(self):
78 if self.name and self.email:
79 return '%s <%s>' % (self.name, self.email)
80 else:
81 raise GitException, 'not enough identity data'
82
41a6d859
CM
83class Commit:
84 """Handle the commit objects
85 """
86 def __init__(self, id_hash):
87 self.__id_hash = id_hash
41a6d859 88
1576d681 89 lines = GRun('cat-file', 'commit', id_hash).output_lines()
26dba451
BL
90 for i in range(len(lines)):
91 line = lines[i]
f0de3f92
KH
92 if not line:
93 break # we've seen all the header fields
94 key, val = line.split(' ', 1)
95 if key == 'tree':
96 self.__tree = val
97 elif key == 'author':
98 self.__author = val
99 elif key == 'committer':
100 self.__committer = val
101 else:
102 pass # ignore other headers
103 self.__log = '\n'.join(lines[i+1:])
41a6d859
CM
104
105 def get_id_hash(self):
106 return self.__id_hash
107
108 def get_tree(self):
109 return self.__tree
110
111 def get_parent(self):
64354a2d
CM
112 parents = self.get_parents()
113 if parents:
114 return parents[0]
115 else:
116 return None
37a4d1bf
CM
117
118 def get_parents(self):
1576d681 119 return GRun('rev-list', '--parents', '--max-count=1', self.__id_hash
f0de3f92 120 ).output_one_line().split()[1:]
41a6d859
CM
121
122 def get_author(self):
123 return self.__author
124
125 def get_committer(self):
126 return self.__committer
127
37a4d1bf
CM
128 def get_log(self):
129 return self.__log
130
4d0ba818
KH
131 def __str__(self):
132 return self.get_id_hash()
133
8e29bcd2
CM
134# dictionary of Commit objects, used to avoid multiple calls to git
135__commits = dict()
41a6d859
CM
136
137#
138# Functions
139#
bae29ddd 140
8e29bcd2
CM
141def get_commit(id_hash):
142 """Commit objects factory. Save/look-up them in the __commits
143 dictionary
144 """
3237b6e4
CM
145 global __commits
146
8e29bcd2
CM
147 if id_hash in __commits:
148 return __commits[id_hash]
149 else:
150 commit = Commit(id_hash)
151 __commits[id_hash] = commit
152 return commit
153
41a6d859
CM
154def get_conflicts():
155 """Return the list of file conflicts
156 """
170f576b 157 conflicts_file = os.path.join(basedir.get(), 'conflicts')
41a6d859
CM
158 if os.path.isfile(conflicts_file):
159 f = file(conflicts_file)
160 names = [line.strip() for line in f.readlines()]
161 f.close()
162 return names
163 else:
164 return None
165
2f830c0c
KH
166def exclude_files():
167 files = [os.path.join(basedir.get(), 'info', 'exclude')]
168 user_exclude = config.get('core.excludesfile')
169 if user_exclude:
170 files.append(user_exclude)
171 return files
172
d4356ac6
CM
173def ls_files(files, tree = None, full_name = True):
174 """Return the files known to GIT or raise an error otherwise. It also
175 converts the file to the full path relative the the .git directory.
176 """
177 if not files:
178 return []
179
180 args = []
181 if tree:
182 args.append('--with-tree=%s' % tree)
183 if full_name:
184 args.append('--full-name')
185 args.append('--')
186 args.extend(files)
187 try:
1576d681 188 return GRun('ls-files', '--error-unmatch', *args).output_lines()
d4356ac6 189 except GitRunException:
1576d681 190 # just hide the details of the 'git ls-files' command we use
d4356ac6
CM
191 raise GitException, \
192 'Some of the given paths are either missing or not known to GIT'
193
d436b1da 194def tree_status(files = None, tree_id = 'HEAD', unknown = False,
2ace36ab 195 noexclude = True, verbose = False, diff_flags = []):
70028538
DK
196 """Get the status of all changed files, or of a selected set of
197 files. Returns a list of pairs - (status, filename).
198
d4356ac6 199 If 'not files', it will check all files, and optionally all
70028538
DK
200 unknown files. If 'files' is a list, it will only check the files
201 in the list.
41a6d859 202 """
d4356ac6 203 assert not files or not unknown
70028538 204
27ac2b7e
KH
205 if verbose:
206 out.start('Checking for changes in the working directory')
b6c37f44 207
f8fb5747 208 refresh_index()
41a6d859
CM
209
210 cache_files = []
211
212 # unknown files
213 if unknown:
1576d681 214 cmd = ['ls-files', '-z', '--others', '--directory',
6d0d7ee6 215 '--no-empty-directory']
14c88aa0
DK
216 if not noexclude:
217 cmd += ['--exclude=%s' % s for s in
218 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
219 cmd += ['--exclude-per-directory=.gitignore']
220 cmd += ['--exclude-from=%s' % fn
221 for fn in exclude_files()
222 if os.path.exists(fn)]
223
224 lines = GRun(*cmd).raw_output().split('\0')
6d0d7ee6 225 cache_files += [('?', line) for line in lines if line]
41a6d859
CM
226
227 # conflicted files
228 conflicts = get_conflicts()
229 if not conflicts:
230 conflicts = []
70028538 231 cache_files += [('C', filename) for filename in conflicts
d4356ac6 232 if not files or filename in files]
41a6d859
CM
233
234 # the rest
70028538 235 args = diff_flags + [tree_id]
d4356ac6 236 if files:
70028538 237 args += ['--'] + files
1576d681 238 for line in GRun('diff-index', *args).output_lines():
26dba451 239 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
41a6d859
CM
240 if fs[1] not in conflicts:
241 cache_files.append(fs)
41a6d859 242
27ac2b7e
KH
243 if verbose:
244 out.done()
b6c37f44 245
41a6d859
CM
246 return cache_files
247
06848fab 248def local_changes(verbose = True):
41a6d859
CM
249 """Return true if there are local changes in the tree
250 """
d436b1da 251 return len(tree_status(verbose = verbose)) != 0
41a6d859 252
262d31dc
KH
253def get_heads():
254 heads = []
216a1524 255 hr = re.compile(r'^[0-9a-f]{40} refs/heads/(.+)$')
1576d681 256 for line in GRun('show-ref', '--heads').output_lines():
216a1524 257 m = hr.match(line)
262d31dc
KH
258 heads.append(m.group(1))
259 return heads
260
aa01a285
CM
261# HEAD value cached
262__head = None
263
41a6d859 264def get_head():
3097799d 265 """Verifies the HEAD and returns the SHA1 id that represents it
41a6d859 266 """
aa01a285
CM
267 global __head
268
269 if not __head:
270 __head = rev_parse('HEAD')
271 return __head
41a6d859 272
acf901c1
KH
273class DetachedHeadException(GitException):
274 def __init__(self):
275 GitException.__init__(self, 'Not on any branch')
276
41a6d859 277def get_head_file():
acf901c1
KH
278 """Return the name of the file pointed to by the HEAD symref.
279 Throw an exception if HEAD is detached."""
280 try:
281 return strip_prefix(
1576d681 282 'refs/heads/', GRun('symbolic-ref', '-q', 'HEAD'
acf901c1
KH
283 ).output_one_line())
284 except GitRunException:
285 raise DetachedHeadException()
41a6d859 286
b99a02b0
CL
287def set_head_file(ref):
288 """Resets HEAD to point to a new ref
289 """
24eede72
CM
290 # head cache flushing is needed since we might have a different value
291 # in the new head
292 __clear_head_cache()
f0de3f92 293 try:
1576d681 294 GRun('symbolic-ref', 'HEAD', 'refs/heads/%s' % ref).run()
f0de3f92 295 except GitRunException:
b99a02b0
CL
296 raise GitException, 'Could not set head to "%s"' % ref
297
262d31dc
KH
298def set_ref(ref, val):
299 """Point ref at a new commit object."""
f0de3f92 300 try:
1576d681 301 GRun('update-ref', ref, val).run()
f0de3f92 302 except GitRunException:
262d31dc
KH
303 raise GitException, 'Could not update %s to "%s".' % (ref, val)
304
e71d766b 305def set_branch(branch, val):
262d31dc 306 set_ref('refs/heads/%s' % branch, val)
e71d766b 307
41a6d859
CM
308def __set_head(val):
309 """Sets the HEAD value
310 """
aa01a285
CM
311 global __head
312
ba1a4550 313 if not __head or __head != val:
262d31dc 314 set_ref('HEAD', val)
ba1a4550
CM
315 __head = val
316
510d1442
CM
317 # only allow SHA1 hashes
318 assert(len(__head) == 40)
319
ba1a4550
CM
320def __clear_head_cache():
321 """Sets the __head to None so that a re-read is forced
322 """
323 global __head
324
325 __head = None
41a6d859 326
f8fb5747
CL
327def refresh_index():
328 """Refresh index with stat() information from the working directory.
329 """
1576d681 330 GRun('update-index', '-q', '--unmerged', '--refresh').run()
f8fb5747 331
d1eb3f85 332def rev_parse(git_id):
3097799d 333 """Parse the string and return a verified SHA1 id
d1eb3f85 334 """
84fcbc3b 335 try:
1576d681 336 return GRun('rev-parse', '--verify', git_id
d9d460a1 337 ).discard_stderr().output_one_line()
f0de3f92 338 except GitRunException:
84fcbc3b 339 raise GitException, 'Unknown revision: %s' % git_id
d1eb3f85 340
262d31dc
KH
341def ref_exists(ref):
342 try:
343 rev_parse(ref)
344 return True
345 except GitException:
346 return False
347
2b4a8aa5 348def branch_exists(branch):
262d31dc 349 return ref_exists('refs/heads/%s' % branch)
2b4a8aa5
CL
350
351def create_branch(new_branch, tree_id = None):
352 """Create a new branch in the git repository
353 """