chiark / gitweb /
Discard stderr output from git-rev-parse
[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
d436b1da 169def tree_status(files = None, tree_id = 'HEAD', unknown = False,
2ace36ab 170 noexclude = True, verbose = False, diff_flags = []):
70028538
DK
171 """Get the status of all changed files, or of a selected set of
172 files. Returns a list of pairs - (status, filename).
173
174 If 'files' is None, it will check all files, and optionally all
175 unknown files. If 'files' is a list, it will only check the files
176 in the list.
41a6d859 177 """
70028538
DK
178 assert files == None or not unknown
179
27ac2b7e
KH
180 if verbose:
181 out.start('Checking for changes in the working directory')
b6c37f44 182
f8fb5747 183 refresh_index()
41a6d859
CM
184
185 cache_files = []
186
187 # unknown files
188 if unknown:
6d0d7ee6
KH
189 cmd = ['git-ls-files', '-z', '--others', '--directory',
190 '--no-empty-directory']
14c88aa0
DK
191 if not noexclude:
192 cmd += ['--exclude=%s' % s for s in
193 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
194 cmd += ['--exclude-per-directory=.gitignore']
195 cmd += ['--exclude-from=%s' % fn
196 for fn in exclude_files()
197 if os.path.exists(fn)]
198
199 lines = GRun(*cmd).raw_output().split('\0')
6d0d7ee6 200 cache_files += [('?', line) for line in lines if line]
41a6d859
CM
201
202 # conflicted files
203 conflicts = get_conflicts()
204 if not conflicts:
205 conflicts = []
70028538
DK
206 cache_files += [('C', filename) for filename in conflicts
207 if files == None or filename in files]
41a6d859
CM
208
209 # the rest
70028538
DK
210 args = diff_flags + [tree_id]
211 if files != None:
212 args += ['--'] + files
213 for line in GRun('git-diff-index', *args).output_lines():
26dba451 214 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
41a6d859
CM
215 if fs[1] not in conflicts:
216 cache_files.append(fs)
41a6d859 217
27ac2b7e
KH
218 if verbose:
219 out.done()
b6c37f44 220
70028538 221 assert files == None or set(f for s,f in cache_files) <= set(files)
41a6d859
CM
222 return cache_files
223
06848fab 224def local_changes(verbose = True):
41a6d859
CM
225 """Return true if there are local changes in the tree
226 """
d436b1da 227 return len(tree_status(verbose = verbose)) != 0
41a6d859 228
262d31dc
KH
229def get_heads():
230 heads = []
216a1524 231 hr = re.compile(r'^[0-9a-f]{40} refs/heads/(.+)$')
f0de3f92 232 for line in GRun('git-show-ref', '--heads').output_lines():
216a1524 233 m = hr.match(line)
262d31dc
KH
234 heads.append(m.group(1))
235 return heads
236
aa01a285
CM
237# HEAD value cached
238__head = None
239
41a6d859 240def get_head():
3097799d 241 """Verifies the HEAD and returns the SHA1 id that represents it
41a6d859 242 """
aa01a285
CM
243 global __head
244
245 if not __head:
246 __head = rev_parse('HEAD')
247 return __head
41a6d859
CM
248
249def get_head_file():
250 """Returns the name of the file pointed to by the HEAD link
251 """