chiark / gitweb /
Make 'stg cp' 2nd form safe.
[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 """
b4d6a1c5
CM
223 if verbose and sys.stdout.isatty():
224 print 'Checking for changes in the working directory...',
225 sys.stdout.flush()
b6c37f44 226
f8fb5747 227 refresh_index()
41a6d859 228
9216b602
CL
229 if not files:
230 files = []
41a6d859
CM
231 cache_files = []
232
233 # unknown files
234 if unknown:
170f576b 235 exclude_file = os.path.join(basedir.get(), 'info', 'exclude')
be24d874
CM
236 base_exclude = ['--exclude=%s' % s for s in
237 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
238 base_exclude.append('--exclude-per-directory=.gitignore')
239
41a6d859 240 if os.path.exists(exclude_file):
3c6fbd2c 241 extra_exclude = ['--exclude-from=%s' % exclude_file]
be24d874
CM
242 else:
243 extra_exclude = []
4d4c0e3a
PBG
244 if noexclude:
245 extra_exclude = base_exclude = []
be24d874 246
2c02c3b7
PBG
247 lines = _output_lines(['git-ls-files', '--others', '--directory']
248 + base_exclude + extra_exclude)
26dba451 249 cache_files += [('?', line.strip()) for line in lines]
41a6d859
CM
250
251 # conflicted files
252 conflicts = get_conflicts()
253 if not conflicts:
254 conflicts = []
255 cache_files += [('C', filename) for filename in conflicts]
256
257 # the rest
a57bd720 258 for line in _output_lines(['git-diff-index', 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
b4d6a1c5
CM
263 if verbose and sys.stdout.isatty():
264 print '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 """
06848fab 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 """