Pass the diff flags when statistics are required
[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
466bfe50 173def ls_files(files, tree = 'HEAD', full_name = True):
d4356ac6
CM
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
8fe07fa4 194def parse_git_ls(output):
a6c4be12
KH
195 """Parse the output of git diff-index, diff-files, etc. Doesn't handle
196 rename/copy output, so don't feed it output generated with the -M
197 or -C flags."""
8fe07fa4
KH
198 t = None
199 for line in output.split('\0'):
200 if not line:
201 # There's a zero byte at the end of the output, which
202 # gives us an empty string as the last "line".
203 continue
204 if t == None:
205 mode_a, mode_b, sha1_a, sha1_b, t = line.split(' ')
206 else:
207 yield (t, line)
208 t = None
209
d436b1da 210def tree_status(files = None, tree_id = 'HEAD', unknown = False,
a6c4be12 211 noexclude = True, verbose = False):
70028538
DK
212 """Get the status of all changed files, or of a selected set of
213 files. Returns a list of pairs - (status, filename).
214
d4356ac6 215 If 'not files', it will check all files, and optionally all
70028538
DK
216 unknown files. If 'files' is a list, it will only check the files
217 in the list.
41a6d859 218 """
d4356ac6 219 assert not files or not unknown
70028538 220
27ac2b7e
KH
221 if verbose:
222 out.start('Checking for changes in the working directory')
b6c37f44 223
f8fb5747 224 refresh_index()
41a6d859 225
466bfe50
CM
226 if files is None:
227 files = []
41a6d859
CM
228 cache_files = []
229
230 # unknown files
231 if unknown:
1576d681 232 cmd = ['ls-files', '-z', '--others', '--directory',
6d0d7ee6 233 '--no-empty-directory']
14c88aa0
DK
234 if not noexclude:
235 cmd += ['--exclude=%s' % s for s in
236 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
237 cmd += ['--exclude-per-directory=.gitignore']
238 cmd += ['--exclude-from=%s' % fn
239 for fn in exclude_files()
240 if os.path.exists(fn)]
241
242 lines = GRun(*cmd).raw_output().split('\0')
6d0d7ee6 243 cache_files += [('?', line) for line in lines if line]
41a6d859
CM
244
245 # conflicted files
246 conflicts = get_conflicts()
247 if not conflicts:
248 conflicts = []
70028538 249 cache_files += [('C', filename) for filename in conflicts
d4356ac6 250 if not files or filename in files]
ca66756b 251 reported_files = set(conflicts)
466bfe50
CM
252 files_left = [f for f in files if f not in reported_files]
253
254 # files in the index. Only execute this code if no files were
255 # specified when calling the function (i.e. report all files) or
256 # files were specified but already found in the previous step
257 if not files or files_left:
a6c4be12 258 args = [tree_id]
466bfe50
CM
259 if files_left:
260 args += ['--'] + files_left
8fe07fa4
KH
261 for t, fn in parse_git_ls(GRun('diff-index', '-z', *args).raw_output()):
262 # the condition is needed in case files is emtpy and
263 # diff-index lists those already reported
264 if not fn in reported_files:
265 cache_files.append((t, fn))
266 reported_files.add(fn)
466bfe50
CM
267 files_left = [f for f in files if f not in reported_files]
268
269 # files in the index but changed on (or removed from) disk. Only
270 # execute this code if no files were specified when calling the
271 # function (i.e. report all files) or files were specified but
272 # already found in the previous step
273 if not files or files_left:
a6c4be12 274 args = []
466bfe50
CM
275 if files_left:
276 args += ['--'] + files_left
8fe07fa4 277 for t, fn in parse_git_ls(GRun('diff-files', '-z', *args).raw_output()):
466bfe50
CM
278 # the condition is needed in case files is empty and
279 # diff-files lists those already reported
8fe07fa4
KH
280 if not fn in reported_files:
281 cache_files.append((t, fn))
282 reported_files.add(fn)
41a6d859 283
27ac2b7e
KH
284 if verbose:
285 out.done()
b6c37f44 286
41a6d859
CM
287 return cache_files
288
06848fab 289def local_changes(verbose = True):
41a6d859
CM
290 """Return true if there are local changes in the tree
291 """
d436b1da 292 return len(tree_status(verbose = verbose)) != 0
41a6d859 293
262d31dc
KH
294def get_heads():
295 heads = []
216a1524 296 hr = re.compile(r'^[0-9a-f]{40} refs/heads/(.+)$')
1576d681 297 for line in GRun('show-ref', '--heads').output_lines():
216a1524 298 m = hr.match(line)
262d31dc
KH
299 heads.append(m.group(1))
300 return heads
301
aa01a285
CM
302# HEAD value cached
303__head = None
304
41a6d859 305def get_head():
3097799d 306 """Verifies the HEAD and returns the SHA1 id that represents it
41a6d859 307 """
aa01a285
CM
308 global __head
309
310 if not __head:
311 __head = rev_parse('HEAD')
312 return __head
41a6d859 313
acf901c1
KH
314class DetachedHeadException(GitException):
315 def __init__(self):
316 GitException.__init__(self, 'Not on any branch')
317
41a6d859 318def get_head_file():
acf901c1
KH
319 """Return the name of the file pointed to by the HEAD symref.
320 Throw an exception if HEAD is detached."""
321 try:
322 return strip_prefix(
1576d681 323 'refs/heads/', GRun('symbolic-ref', '-q', 'HEAD'
acf901c1
KH
324 ).output_one_line())
325 except GitRunException:
326 raise DetachedHeadException()
41a6d859 327
b99a02b0
CL
328def set_head_file(ref):
329 """Resets HEAD to point to a new ref
330 """
24eede72
CM
331 # head cache flushing is needed since we might have a different value
332 # in the new head
333 __clear_head_cache()
f0de3f92 334 try:
1576d681 335 GRun('symbolic-ref', 'HEAD', 'refs/heads/%s' % ref).run()
f0de3f92 336 except GitRunException:
b99a02b0
CL
337 raise GitException, 'Could not set head to "%s"' % ref
338
262d31dc
KH
339def set_ref(ref, val):
340 """Point ref at a new commit object."""
f0de3f92 341 try:
1576d681 342 GRun('update-ref', ref, val).run()
f0de3f92 343 except GitRunException:
262d31dc
KH
344 raise GitException, 'Could not update %s to "%s".' % (ref, val)
345
e71d766b 346def set_branch(branch, val):
262d31dc 347 set_ref('refs/heads/%s' % branch, val)
e71d766b 348
41a6d859
CM
349def __set_head(val):
350 """Sets the HEAD value
351 """
aa01a285
CM
352 global __head
353
ba1a4550 354 if not __head or __head != val:
262d31dc 355 set_ref('HEAD', val)
ba1a4550
CM
356 __head = val
357
510d1442
CM
358 # only allow SHA1 hashes
359 assert(len(__head) == 40)
360
ba1a4550
CM
361def __clear_head_cache():
362 """Sets the __head to None so that a re-read is forced
363 """
364 global __head
365
366 __head = None
41a6d859 367
f8fb5747
CL
368def refresh_index():
369 """Refresh index with stat() information from the working directory.
370 """
1576d681 371 GRun('update-index', '-q', '--unmerged', '--refresh').run()
f8fb5747 372
d1eb3f85 373def rev_parse(git_id):
3097799d 374 """Parse the string and return a verified SHA1 id
d1eb3f85 375 """
84fcbc3b 376 try:
1576d681 377 return GRun('rev-parse', '--verify', git_id
d9d460a1 378 ).discard_stderr().output_one_line()
f0de3f92 379 except GitRunException:
84fcbc3b 380 raise GitException, 'Unknown revision: %s' % git_id
d1eb3f85 381
262d31dc
KH
382def ref_exists(ref):
383 try:
384 rev_parse(ref)
385 return True
386 except GitException:
387 return False
388
2b4a8aa5 389def branch_exists(branch):
262d31dc 390 return ref_exists('refs/heads/%s' % branch)
2b4a8aa5
CL
391
392def create_branch(new_branch, tree_id = None):
393 """Create a new branch in the git repository
394 """