chiark / gitweb /
Handle refresh of changed files with non-ASCII names
[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
1576d681
CM
41 def __init__(self, *cmd):
42 """Initialise the Run object and insert the 'git' command name.
43 """
44 Run.__init__(self, 'git', *cmd)
41a6d859 45
41a6d859 46
41a6d859
CM
47#
48# Classes
49#
9e3f506f
KH
50
51class Person:
52 """An author, committer, etc."""
53 def __init__(self, name = None, email = None, date = '',
54 desc = None):
5cd9e87f 55 self.name = self.email = self.date = None
9e3f506f
KH
56 if name or email or date:
57 assert not desc
58 self.name = name
59 self.email = email
60 self.date = date
61 elif desc:
62 assert not (name or email or date)
63 def parse_desc(s):
64 m = re.match(r'^(.+)<(.+)>(.*)$', s)
65 assert m
66 return [x.strip() or None for x in m.groups()]
67 self.name, self.email, self.date = parse_desc(desc)
68 def set_name(self, val):
69 if val:
70 self.name = val
71 def set_email(self, val):
72 if val:
73 self.email = val
74 def set_date(self, val):
75 if val:
76 self.date = val
77 def __str__(self):
78 if self.name and self.email:
79 return '%s <%s>' % (self.name, self.email)
80 else:
81 raise GitException, 'not enough identity data'
82
41a6d859
CM
83class Commit:
84 """Handle the commit objects
85 """
86 def __init__(self, id_hash):
87 self.__id_hash = id_hash
41a6d859 88
1576d681 89 lines = GRun('cat-file', 'commit', id_hash).output_lines()
26dba451
BL
90 for i in range(len(lines)):
91 line = lines[i]
f0de3f92
KH
92 if not line:
93 break # we've seen all the header fields
94 key, val = line.split(' ', 1)
95 if key == 'tree':
96 self.__tree = val
97 elif key == 'author':
98 self.__author = val
99 elif key == 'committer':
100 self.__committer = val
101 else:
102 pass # ignore other headers
103 self.__log = '\n'.join(lines[i+1:])
41a6d859
CM
104
105 def get_id_hash(self):
106 return self.__id_hash
107
108 def get_tree(self):
109 return self.__tree
110
111 def get_parent(self):
64354a2d
CM
112 parents = self.get_parents()
113 if parents:
114 return parents[0]
115 else:
116 return None
37a4d1bf
CM
117
118 def get_parents(self):
1576d681 119 return GRun('rev-list', '--parents', '--max-count=1', self.__id_hash
f0de3f92 120 ).output_one_line().split()[1:]
41a6d859
CM
121
122 def get_author(self):
123 return self.__author
124
125 def get_committer(self):
126 return self.__committer
127
37a4d1bf
CM
128 def get_log(self):
129 return self.__log
130
4d0ba818
KH
131 def __str__(self):
132 return self.get_id_hash()
133
8e29bcd2
CM
134# dictionary of Commit objects, used to avoid multiple calls to git
135__commits = dict()
41a6d859
CM
136
137#
138# Functions
139#
bae29ddd 140
8e29bcd2
CM
141def get_commit(id_hash):
142 """Commit objects factory. Save/look-up them in the __commits
143 dictionary
144 """
3237b6e4
CM
145 global __commits
146
8e29bcd2
CM
147 if id_hash in __commits:
148 return __commits[id_hash]
149 else:
150 commit = Commit(id_hash)
151 __commits[id_hash] = commit
152 return commit
153
41a6d859
CM
154def get_conflicts():
155 """Return the list of file conflicts
156 """
170f576b 157 conflicts_file = os.path.join(basedir.get(), 'conflicts')
41a6d859
CM
158 if os.path.isfile(conflicts_file):
159 f = file(conflicts_file)
160 names = [line.strip() for line in f.readlines()]
161 f.close()
162 return names
163 else:
164 return None
165
2f830c0c
KH
166def exclude_files():
167 files = [os.path.join(basedir.get(), 'info', 'exclude')]
168 user_exclude = config.get('core.excludesfile')
169 if user_exclude:
170 files.append(user_exclude)
171 return files
172
466bfe50 173def ls_files(files, tree = 'HEAD', full_name = True):
d4356ac6
CM
174 """Return the files known to GIT or raise an error otherwise. It also
175 converts the file to the full path relative the the .git directory.
176 """
177 if not files:
178 return []
179
180 args = []
181 if tree:
182 args.append('--with-tree=%s' % tree)
183 if full_name:
184 args.append('--full-name')
185 args.append('--')
186 args.extend(files)
187 try:
1576d681 188 return GRun('ls-files', '--error-unmatch', *args).output_lines()
d4356ac6 189 except GitRunException:
1576d681 190 # just hide the details of the 'git ls-files' command we use
d4356ac6
CM
191 raise GitException, \
192 'Some of the given paths are either missing or not known to GIT'
193
8fe07fa4
KH
194def parse_git_ls(output):
195 t = None
196 for line in output.split('\0'):
197 if not line:
198 # There's a zero byte at the end of the output, which
199 # gives us an empty string as the last "line".
200 continue
201 if t == None:
202 mode_a, mode_b, sha1_a, sha1_b, t = line.split(' ')
203 else:
204 yield (t, line)
205 t = None
206
d436b1da 207def tree_status(files = None, tree_id = 'HEAD', unknown = False,
2ace36ab 208 noexclude = True, verbose = False, diff_flags = []):
70028538
DK
209 """Get the status of all changed files, or of a selected set of
210 files. Returns a list of pairs - (status, filename).
211
d4356ac6 212 If 'not files', it will check all files, and optionally all
70028538
DK
213 unknown files. If 'files' is a list, it will only check the files
214 in the list.
41a6d859 215 """
d4356ac6 216 assert not files or not unknown
70028538 217
27ac2b7e
KH
218 if verbose:
219 out.start('Checking for changes in the working directory')
b6c37f44 220
f8fb5747 221 refresh_index()
41a6d859 222
466bfe50
CM
223 if files is None:
224 files = []
41a6d859
CM
225 cache_files = []
226
227 # unknown files
228 if unknown:
1576d681 229 cmd = ['ls-files', '-z', '--others', '--directory',
6d0d7ee6 230 '--no-empty-directory']
14c88aa0
DK
231 if not noexclude:
232 cmd += ['--exclude=%s' % s for s in
233 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
234 cmd += ['--exclude-per-directory=.gitignore']
235 cmd += ['--exclude-from=%s' % fn
236 for fn in exclude_files()
237 if os.path.exists(fn)]
238
239 lines = GRun(*cmd).raw_output().split('\0')
6d0d7ee6 240 cache_files += [('?', line) for line in lines if line]
41a6d859
CM
241
242 # conflicted files
243 conflicts = get_conflicts()
244 if not conflicts:
245 conflicts = []
70028538 246 cache_files += [('C', filename) for filename in conflicts
d4356ac6 247 if not files or filename in files]
ca66756b 248 reported_files = set(conflicts)
466bfe50
CM
249 files_left = [f for f in files if f not in reported_files]
250
251 # files in the index. Only execute this code if no files were
252 # specified when calling the function (i.e. report all files) or
253 # files were specified but already found in the previous step
254 if not files or files_left:
255 args = diff_flags + [tree_id]
256 if files_left:
257 args += ['--'] + files_left
8fe07fa4
KH
258 for t, fn in parse_git_ls(GRun('diff-index', '-z', *args).raw_output()):
259 # the condition is needed in case files is emtpy and
260 # diff-index lists those already reported
261 if not fn in reported_files:
262 cache_files.append((t, fn))
263 reported_files.add(fn)
466bfe50
CM
264 files_left = [f for f in files if f not in reported_files]
265
266 # files in the index but changed on (or removed from) disk. Only
267 # execute this code if no files were specified when calling the
268 # function (i.e. report all files) or files were specified but
269 # already found in the previous step
270 if not files or files_left:
271 args = list(diff_flags)
272 if files_left:
273 args += ['--'] + files_left
8fe07fa4 274 for t, fn in parse_git_ls(GRun('diff-files', '-z', *args).raw_output()):
466bfe50
CM
275 # the condition is needed in case files is empty and
276 # diff-files lists those already reported
8fe07fa4
KH
277 if not fn in reported_files:
278 cache_files.append((t, fn))
279 reported_files.add(fn)
41a6d859 280
27ac2b7e
KH
281 if verbose:
282 out.done()
b6c37f44 283
41a6d859
CM
284 return cache_files
285
06848fab 286def local_changes(verbose = True):
41a6d859
CM
287 """Return true if there are local changes in the tree
288 """
d436b1da 289 return len(tree_status(verbose = verbose)) != 0
41a6d859 290
262d31dc
KH
291def get_heads():
292 heads = []
216a1524 293 hr = re.compile(r'^[0-9a-f]{40} refs/heads/(.+)$')
1576d681 294 for line in GRun('show-ref', '--heads').output_lines():
216a1524 295 m = hr.match(line)
262d31dc
KH
296 heads.append(m.group(1))
297 return heads
298
aa01a285
CM
299# HEAD value cached
300__head = None
301
41a6d859 302def get_head():
3097799d 303 """Verifies the HEAD and returns the SHA1 id that represents it
41a6d859 304 """
aa01a285
CM
305 global __head
306
307 if not __head:
308 __head = rev_parse('HEAD')
309 return __head
41a6d859 310
acf901c1
KH
311class DetachedHeadException(GitException):
312 def __init__(self):
313 GitException.__init__(self, 'Not on any branch')
314
41a6d859 315def get_head_file():
acf901c1
KH
316 """Return the name of the file pointed to by the HEAD symref.
317 Throw an exception if HEAD is detached."""
318 try:
319 return strip_prefix(
1576d681 320 'refs/heads/', GRun('symbolic-ref', '-q', 'HEAD'
acf901c1
KH
321 ).output_one_line())
322 except GitRunException:
323 raise DetachedHeadException()
41a6d859 324
b99a02b0
CL
325def set_head_file(ref):
326 """Resets HEAD to point to a new ref
327 """
24eede72
CM
328 # head cache flushing is needed since we might have a different value
329 # in the new head
330 __clear_head_cache()
f0de3f92 331 try:
1576d681 332 GRun('symbolic-ref', 'HEAD', 'refs/heads/%s' % ref).run()
f0de3f92 333 except GitRunException:
b99a02b0
CL
334 raise GitException, 'Could not set head to "%s"' % ref
335
262d31dc
KH
336def set_ref(ref, val):
337 """Point ref at a new commit object."""
f0de3f92 338 try:
1576d681 339 GRun('update-ref', ref, val).run()
f0de3f92 340 except GitRunException:
262d31dc
KH
341 raise GitException, 'Could not update %s to "%s".' % (ref, val)
342
e71d766b 343def set_branch(branch, val):
262d31dc 344 set_ref('refs/heads/%s' % branch, val)
e71d766b 345
41a6d859
CM
346def __set_head(val):
347 """Sets the HEAD value
348 """
aa01a285
CM
349 global __head
350
ba1a4550 351 if not __head or __head != val:
262d31dc 352 set_ref('HEAD', val)
ba1a4550
CM
353 __head = val
354
510d1442
CM
355 # only allow SHA1 hashes
356 assert(len(__head) == 40)
357
ba1a4550
CM
358def __clear_head_cache():
359 """Sets the __head to None so that a re-read is forced
360 """
361 global __head
362
363 __head = None
41a6d859 364
f8fb5747
CL
365def refresh_index():
366 """Refresh index with stat() information from the working directory.
367 """
1576d681 368 GRun('update-index', '-q', '--unmerged', '--refresh').run()
f8fb5747 369
d1eb3f85 370def rev_parse(git_id):
3097799d 371 """Parse the string and return a verified SHA1 id
d1eb3f85 372 """
84fcbc3b 373 try:
1576d681 374 return GRun('rev-parse', '--verify', git_id
d9d460a1 375 ).discard_stderr().output_one_line()
f0de3f92 376 except GitRunException:
84fcbc3b 377 raise GitException, 'Unknown revision: %s' % git_id
d1eb3f85 378
262d31dc
KH
379def ref_exists(ref):
380 try:
381 rev_parse(ref)
382 return True
383 except GitException:
384 return False
385
2b4a8aa5 386def branch_exists(branch):
262d31dc 387 return ref_exists('refs/heads/%s' % branch)
2b4a8aa5
CL
388
389def create_branch(new_branch, tree_id = None):
390 """Create a new branch in the git repository
391 """