Modify the 'patches' command to use the locally modified files
[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
23bb4e8f 77 lines = _output_lines(['git-cat-file', 'commit', id_hash])
26dba451
BL
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):
23bb4e8f
YD
105 return _output_lines(['git-rev-list', '--parents', '--max-count=1',
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():
57c05b31 161 raise GitException, '%s failed (%s)' % (' '.join(cmd),
2c5d2242 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():
57c05b31 169 raise GitException, '%s failed (%s)' % (' '.join(cmd),
2c5d2242 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():
57c05b31 176 raise GitException, '%s failed (%s)' % (' '.join(cmd),
2c5d2242 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():
57c05b31 188 raise GitException, '%s failed (%s)' % (' '.join(cmd),
2c5d2242 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():
57c05b31 196 raise GitException, '%s failed (%s)' % (' '.join(cmd),
2c5d2242 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
d436b1da 219def tree_status(files = None, tree_id = 'HEAD', unknown = False,
2ace36ab 220 noexclude = True, verbose = False, diff_flags = []):
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
2ace36ab
YD
257 for line in _output_lines(['git-diff-index'] + diff_flags +
258 [ tree_id, '--'] + files):
26dba451 259 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
41a6d859
CM
260 if fs[1] not in conflicts:
261 cache_files.append(fs)
41a6d859 262
27ac2b7e
KH
263 if verbose:
264 out.done()
b6c37f44 265
41a6d859
CM
266 return cache_files
267
06848fab 268def local_changes(verbose = True):
41a6d859
CM
269 """Return true if there are local changes in the tree
270 """
d436b1da 271 return len(tree_status(verbose = verbose)) != 0
41a6d859 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
CM
284
285def get_head_file():
286 """Returns the name of the file pointed to by the HEAD link
287 """