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