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