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