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