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