chiark / gitweb /
Check for disappeared newborn files in git.tree_status (bug #8516)
[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]
ca66756b 233 reported_files = set(conflicts)
41a6d859 234
ca66756b 235 # files in the index
70028538 236 args = diff_flags + [tree_id]
d4356ac6 237 if files:
70028538 238 args += ['--'] + files
1576d681 239 for line in GRun('diff-index', *args).output_lines():
26dba451 240 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
ca66756b 241 if fs[1] not in reported_files:
41a6d859 242 cache_files.append(fs)
ca66756b
CM
243 reported_files.add(fs[1])
244
245 # files in the index but changed on (or removed from) disk
246 args = list(diff_flags)
247 if files:
248 args += ['--'] + files
249 for line in GRun('diff-files', *args).output_lines():
250 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
251 if fs[1] not in reported_files:
252 cache_files.append(fs)
253 reported_files.add(fs[1])
41a6d859 254
27ac2b7e
KH
255 if verbose:
256 out.done()
b6c37f44 257
41a6d859
CM
258 return cache_files
259
06848fab 260def local_changes(verbose = True):
41a6d859
CM
261 """Return true if there are local changes in the tree
262 """
d436b1da 263 return len(tree_status(verbose = verbose)) != 0
41a6d859 264
262d31dc
KH
265def get_heads():
266 heads = []
216a1524 267 hr = re.compile(r'^[0-9a-f]{40} refs/heads/(.+)$')
1576d681 268 for line in GRun('show-ref', '--heads').output_lines():
216a1524 269 m = hr.match(line)
262d31dc
KH
270 heads.append(m.group(1))
271 return heads
272
aa01a285
CM
273# HEAD value cached
274__head = None
275
41a6d859 276def get_head():
3097799d 277 """Verifies the HEAD and returns the SHA1 id that represents it
41a6d859 278 """
aa01a285
CM
279 global __head
280
281 if not __head:
282 __head = rev_parse('HEAD')
283 return __head
41a6d859 284
acf901c1
KH
285class DetachedHeadException(GitException):
286 def __init__(self):
287 GitException.__init__(self, 'Not on any branch')
288
41a6d859 289def get_head_file():
acf901c1
KH
290 """Return the name of the file pointed to by the HEAD symref.
291 Throw an exception if HEAD is detached."""
292 try:
293 return strip_prefix(
1576d681 294 'refs/heads/', GRun('symbolic-ref', '-q', 'HEAD'
acf901c1
KH
295 ).output_one_line())
296 except GitRunException:
297 raise DetachedHeadException()
41a6d859 298
b99a02b0
CL
299def set_head_file(ref):
300 """Resets HEAD to point to a new ref
301 """
24eede72
CM
302 # head cache flushing is needed since we might have a different value
303 # in the new head
304 __clear_head_cache()
f0de3f92 305 try:
1576d681 306 GRun('symbolic-ref', 'HEAD', 'refs/heads/%s' % ref).run()
f0de3f92 307 except GitRunException:
b99a02b0
CL
308 raise GitException, 'Could not set head to "%s"' % ref
309
262d31dc
KH
310def set_ref(ref, val):
311 """Point ref at a new commit object."""
f0de3f92 312 try:
1576d681 313 GRun('update-ref', ref, val).run()
f0de3f92 314 except GitRunException:
262d31dc
KH
315 raise GitException, 'Could not update %s to "%s".' % (ref, val)
316
e71d766b 317def set_branch(branch, val):
262d31dc 318 set_ref('refs/heads/%s' % branch, val)
e71d766b 319
41a6d859
CM
320def __set_head(val):
321 """Sets the HEAD value
322 """
aa01a285
CM
323 global __head
324
ba1a4550 325 if not __head or __head != val:
262d31dc 326 set_ref('HEAD', val)
ba1a4550
CM
327 __head = val
328
510d1442
CM
329 # only allow SHA1 hashes
330 assert(len(__head) == 40)
331
ba1a4550
CM
332def __clear_head_cache():
333 """Sets the __head to None so that a re-read is forced
334 """
335 global __head
336
337 __head = None
41a6d859 338
f8fb5747
CL
339def refresh_index():
340 """Refresh index with stat() information from the working directory.
341 """
1576d681 342 GRun('update-index', '-q', '--unmerged', '--refresh').run()
f8fb5747 343
d1eb3f85 344def rev_parse(git_id):
3097799d 345 """Parse the string and return a verified SHA1 id
d1eb3f85 346 """
84fcbc3b 347 try:
1576d681 348 return GRun('rev-parse', '--verify', git_id
d9d460a1 349 ).discard_stderr().output_one_line()
f0de3f92 350 except GitRunException:
84fcbc3b 351 raise GitException, 'Unknown revision: %s' % git_id
d1eb3f85 352
262d31dc
KH
353def ref_exists(ref):
354 try:
355 rev_parse(ref)
356 return True
357 except GitException:
358 return False
359
2b4a8aa5 360def branch_exists(branch):
262d31dc 361 return ref_exists('refs/heads/%s' % branch)
2b4a8aa5
CL
362
363def create_branch(new_branch, tree_id = None):
364 """Create a new branch in the git repository
365 """