chiark / gitweb /
Refactor message printing
[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
3659ef88 21import sys, os, popen2, re, gitmergeonefile
d5ae2173 22from shutil import copyfile
41a6d859 23
170f576b 24from stgit import basedir
41a6d859 25from stgit.utils import *
b3bfa120 26from stgit.config import config
fba895f5 27from sets import Set
41a6d859
CM
28
29# git exception class
30class GitException(Exception):
31 pass
32
33
41a6d859 34
41a6d859
CM
35#
36# Classes
37#
9e3f506f
KH
38
39class Person:
40 """An author, committer, etc."""
41 def __init__(self, name = None, email = None, date = '',
42 desc = None):
5cd9e87f 43 self.name = self.email = self.date = None
9e3f506f
KH
44 if name or email or date:
45 assert not desc
46 self.name = name
47 self.email = email
48 self.date = date
49 elif desc:
50 assert not (name or email or date)
51 def parse_desc(s):
52 m = re.match(r'^(.+)<(.+)>(.*)$', s)
53 assert m
54 return [x.strip() or None for x in m.groups()]
55 self.name, self.email, self.date = parse_desc(desc)
56 def set_name(self, val):
57 if val:
58 self.name = val
59 def set_email(self, val):
60 if val:
61 self.email = val
62 def set_date(self, val):
63 if val:
64 self.date = val
65 def __str__(self):
66 if self.name and self.email:
67 return '%s <%s>' % (self.name, self.email)
68 else:
69 raise GitException, 'not enough identity data'
70
41a6d859
CM
71class Commit:
72 """Handle the commit objects
73 """
74 def __init__(self, id_hash):
75 self.__id_hash = id_hash
41a6d859 76
26dba451
BL
77 lines = _output_lines('git-cat-file commit %s' % id_hash)
78 for i in range(len(lines)):
79 line = lines[i]
41a6d859
CM
80 if line == '\n':
81 break
82 field = line.strip().split(' ', 1)
83 if field[0] == 'tree':
84 self.__tree = field[1]
41a6d859
CM
85 if field[0] == 'author':
86 self.__author = field[1]
dad310d0 87 if field[0] == 'committer':
41a6d859 88 self.__committer = field[1]
0618ea9c 89 self.__log = ''.join(lines[i+1:])
41a6d859
CM
90
91 def get_id_hash(self):
92 return self.__id_hash
93
94 def get_tree(self):
95 return self.__tree
96
97 def get_parent(self):
64354a2d
CM
98 parents = self.get_parents()
99 if parents:
100 return parents[0]
101 else:
102 return None
37a4d1bf
CM
103
104 def get_parents(self):
2406f7d1
CM
105 return _output_lines('git-rev-list --parents --max-count=1 %s'
106 % self.__id_hash)[0].split()[1:]
41a6d859
CM
107
108 def get_author(self):
109 return self.__author
110
111 def get_committer(self):
112 return self.__committer
113
37a4d1bf
CM
114 def get_log(self):
115 return self.__log
116
4d0ba818
KH
117 def __str__(self):
118 return self.get_id_hash()
119
8e29bcd2
CM
120# dictionary of Commit objects, used to avoid multiple calls to git
121__commits = dict()
41a6d859
CM
122
123#
124# Functions
125#
bae29ddd 126
8e29bcd2
CM
127def get_commit(id_hash):
128 """Commit objects factory. Save/look-up them in the __commits
129 dictionary
130 """
3237b6e4
CM
131 global __commits
132
8e29bcd2
CM
133 if id_hash in __commits:
134 return __commits[id_hash]
135 else:
136 commit = Commit(id_hash)
137 __commits[id_hash] = commit
138 return commit
139
41a6d859
CM
140def get_conflicts():
141 """Return the list of file conflicts
142 """
170f576b 143 conflicts_file = os.path.join(basedir.get(), 'conflicts')
41a6d859
CM
144 if os.path.isfile(conflicts_file):
145 f = file(conflicts_file)
146 names = [line.strip() for line in f.readlines()]
147 f.close()
148 return names
149 else:
150 return None
151
0d2cd1e4 152def _input(cmd, file_desc):
741f2784 153 p = popen2.Popen3(cmd, True)
6fe6b1bd
CM
154 while True:
155 line = file_desc.readline()
156 if not line:
157 break
0d2cd1e4
CM
158 p.tochild.write(line)
159 p.tochild.close()
160 if p.wait():
2c5d2242
CM
161 raise GitException, '%s failed (%s)' % (str(cmd),
162 p.childerr.read().strip())
0d2cd1e4 163
d0bfda1a
CM
164def _input_str(cmd, string):
165 p = popen2.Popen3(cmd, True)
166 p.tochild.write(string)
167 p.tochild.close()
168 if p.wait():
2c5d2242
CM
169 raise GitException, '%s failed (%s)' % (str(cmd),
170 p.childerr.read().strip())
d0bfda1a 171
26dba451 172def _output(cmd):
741f2784 173 p=popen2.Popen3(cmd, True)
7cc615f3 174 output = p.fromchild.read()
26dba451 175 if p.wait():
2c5d2242
CM
176 raise GitException, '%s failed (%s)' % (str(cmd),
177 p.childerr.read().strip())
7cc615f3 178 return output
26dba451 179
d3cf7d86 180def _output_one_line(cmd, file_desc = None):
741f2784 181 p=popen2.Popen3(cmd, True)
d3cf7d86
PBG
182 if file_desc != None:
183 for line in file_desc:
184 p.tochild.write(line)
185 p.tochild.close()
7cc615f3 186 output = p.fromchild.readline().strip()
26dba451 187 if p.wait():
2c5d2242
CM
188 raise GitException, '%s failed (%s)' % (str(cmd),
189 p.childerr.read().strip())
7cc615f3 190 return output
41a6d859 191
26dba451 192def _output_lines(cmd):
741f2784 193 p=popen2.Popen3(cmd, True)
26dba451
BL
194 lines = p.fromchild.readlines()
195 if p.wait():
2c5d2242
CM
196 raise GitException, '%s failed (%s)' % (str(cmd),
197 p.childerr.read().strip())
26dba451
BL
198 return lines
199
200def __run(cmd, args=None):
201 """__run: runs cmd using spawnvp.
202
203 Runs cmd using spawnvp. The shell is avoided so it won't mess up
204 our arguments. If args is very large, the command is run multiple
205 times; args is split xargs style: cmd is passed on each
206 invocation. Unlike xargs, returns immediately if any non-zero
207 return code is received.
208 """
209
210 args_l=cmd.split()
211 if args is None:
212 args = []
213 for i in range(0, len(args)+1, 100):
214 r=os.spawnvp(os.P_WAIT, args_l[0], args_l + args[i:min(i+100, len(args))])
215 if r:
216 return r
217 return 0
218
9216b602 219def __tree_status(files = None, tree_id = 'HEAD', unknown = False,
b6c37f44 220 noexclude = True, verbose = False):
41a6d859
CM
221 """Returns a list of pairs - [status, filename]
222 """
27ac2b7e
KH
223 if verbose:
224 out.start('Checking for changes in the working directory')
b6c37f44 225
f8fb5747 226 refresh_index()
41a6d859 227
9216b602
CL
228 if not files:
229 files = []
41a6d859
CM
230 cache_files = []
231
232 # unknown files
233 if unknown:
170f576b 234 exclude_file = os.path.join(basedir.get(), 'info', 'exclude')
be24d874
CM
235 base_exclude = ['--exclude=%s' % s for s in
236 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
237 base_exclude.append('--exclude-per-directory=.gitignore')
238
41a6d859 239 if os.path.exists(exclude_file):
3c6fbd2c 240 extra_exclude = ['--exclude-from=%s' % exclude_file]
be24d874
CM
241 else:
242 extra_exclude = []
4d4c0e3a
PBG
243 if noexclude:
244 extra_exclude = base_exclude = []
be24d874 245
2c02c3b7
PBG
246 lines = _output_lines(['git-ls-files', '--others', '--directory']
247 + base_exclude + extra_exclude)
26dba451 248 cache_files += [('?', line.strip()) for line in lines]
41a6d859
CM
249
250 # conflicted files
251 conflicts = get_conflicts()
252 if not conflicts:
253 conflicts = []
254 cache_files += [('C', filename) for filename in conflicts]
255
256 # the rest
a57bd720 257 for line in _output_lines(['git-diff-index', tree_id, '--'] + files):
26dba451 258 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
41a6d859
CM
259 if fs[1] not in conflicts:
260 cache_files.append(fs)
41a6d859 261
27ac2b7e
KH
262 if verbose:
263 out.done()
b6c37f44 264
41a6d859
CM
265 return cache_files
266
06848fab 267def local_changes(verbose = True):
41a6d859
CM
268 """Return true if there are local changes in the tree
269 """
06848fab 270 return len(__tree_status(verbose = verbose)) != 0
41a6d859 271
aa01a285
CM
272# HEAD value cached
273__head = None
274
41a6d859 275def get_head():
3097799d 276 """Verifies the HEAD and returns the SHA1 id that represents it
41a6d859 277 """
aa01a285
CM
278 global __head
279
280 if not __head:
281 __head = rev_parse('HEAD')
282 return __head
41a6d859
CM
283
284def get_head_file():
285 """Returns the name of the file pointed to by the HEAD link
286 """