chiark / gitweb /
Update the git cache for the resolved file
[stgit] / stgit / git.py
1 """Python GIT interface
2 """
3
4 __copyright__ = """
5 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
6
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License version 2 as
9 published by the Free Software Foundation.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 """
20
21 import sys, os, glob
22
23 from stgit.utils import *
24
25 # git exception class
26 class GitException(Exception):
27     pass
28
29
30 # Different start-up variables read from the environment
31 if 'GIT_DIR' in os.environ:
32     base_dir = os.environ['GIT_DIR']
33 else:
34     base_dir = '.git'
35
36 head_link = os.path.join(base_dir, 'HEAD')
37
38
39 #
40 # Classes
41 #
42 class Commit:
43     """Handle the commit objects
44     """
45     def __init__(self, id_hash):
46         self.__id_hash = id_hash
47         f = os.popen('git-cat-file commit %s' % id_hash, 'r')
48
49         for line in f:
50             if line == '\n':
51                 break
52             field = line.strip().split(' ', 1)
53             if field[0] == 'tree':
54                 self.__tree = field[1]
55             elif field[0] == 'parent':
56                 self.__parent = field[1]
57             if field[0] == 'author':
58                 self.__author = field[1]
59             if field[0] == 'comitter':
60                 self.__committer = field[1]
61         self.__log = f.read()
62
63         if f.close():
64             raise GitException, 'Unknown commit id'
65
66     def get_id_hash(self):
67         return self.__id_hash
68
69     def get_tree(self):
70         return self.__tree
71
72     def get_parent(self):
73         return self.__parent
74
75     def get_author(self):
76         return self.__author
77
78     def get_committer(self):
79         return self.__committer
80
81
82 #
83 # Functions
84 #
85 def get_conflicts():
86     """Return the list of file conflicts
87     """
88     conflicts_file = os.path.join(base_dir, 'conflicts')
89     if os.path.isfile(conflicts_file):
90         f = file(conflicts_file)
91         names = [line.strip() for line in f.readlines()]
92         f.close()
93         return names
94     else:
95         return None
96
97 def __output(cmd):
98     f = os.popen(cmd, 'r')
99     string = f.readline().strip()
100     if f.close():
101         raise GitException, '%s failed' % cmd
102     return string
103
104 def __check_base_dir():
105     return os.path.isdir(base_dir)
106
107 def __tree_status(files = [], tree_id = 'HEAD', unknown = False):
108     """Returns a list of pairs - [status, filename]
109     """
110     os.system('git-update-cache --refresh > /dev/null')
111
112     cache_files = []
113
114     # unknown files
115     if unknown:
116         exclude_file = os.path.join(base_dir, 'exclude')
117         extra_exclude = ''
118         if os.path.exists(exclude_file):
119             extra_exclude += ' --exclude-from=%s' % exclude_file
120         fout = os.popen('git-ls-files --others'
121                         ' --exclude="*.[ao]" --exclude=".*"'
122                         ' --exclude=TAGS --exclude=tags --exclude="*~"'
123                         ' --exclude="#*"' + extra_exclude, 'r')
124         cache_files += [('?', line.strip()) for line in fout]
125
126     # conflicted files
127     conflicts = get_conflicts()
128     if not conflicts:
129         conflicts = []
130     cache_files += [('C', filename) for filename in conflicts]
131
132     # the rest
133     files_str = reduce(lambda x, y: x + ' ' + y, files, '')
134     fout = os.popen('git-diff-cache -r %s %s' % (tree_id, files_str), 'r')
135     for line in fout:
136         fs = tuple(line.split()[4:])
137         if fs[1] not in conflicts:
138             cache_files.append(fs)
139     if fout.close():
140         raise GitException, 'git-diff-cache failed'
141
142     return cache_files
143
144 def local_changes():
145     """Return true if there are local changes in the tree
146     """
147     return len(__tree_status()) != 0
148
149 def get_head():
150     """Returns a string representing the HEAD
151     """
152     return read_string(head_link)
153
154 def get_head_file():
155     """Returns the name of the file pointed to by the HEAD link
156     """
157     # valid link
158     if os.path.islink(head_link) and os.path.isfile(head_link):
159         return os.path.basename(os.readlink(head_link))
160     else:
161         raise GitException, 'Invalid .git/HEAD link. Git tree not initialised?'
162
163 def __set_head(val):
164     """Sets the HEAD value
165     """
166     write_string(head_link, val)
167
168 def add(names):
169     """Add the files or recursively add the directory contents
170     """
171     # generate the file list
172     files = []
173     for i in names:
174         if not os.path.exists(i):
175             raise GitException, 'Unknown file or directory: %s' % i
176
177         if os.path.isdir(i):
178             # recursive search. We only add files
179             for root, dirs, local_files in os.walk(i):
180                 for name in [os.path.join(root, f) for f in local_files]:
181                     if os.path.isfile(name):
182                         files.append(os.path.normpath(name))
183         elif os.path.isfile(i):
184             files.append(os.path.normpath(i))
185         else:
186             raise GitException, '%s is not a file or directory' % i
187
188     for f in files:
189         print 'Adding file %s' % f
190         if os.system('git-update-cache --add -- %s' % f) != 0:
191             raise GitException, 'Unable to add %s' % f
192
193 def rm(files, force = False):
194     """Remove a file from the repository
195     """
196     if force:
197         git_opt = '--force-remove'
198     else:
199         git_opt = '--remove'
200
201     for f in files:
202         if force:
203             print 'Removing file %s' % f
204             if os.system('git-update-cache --force-remove -- %s' % f) != 0:
205                 raise GitException, 'Unable to remove %s' % f
206         elif os.path.exists(f):
207             raise GitException, '%s exists. Remove it first' %f
208         else:
209             print 'Removing file %s' % f
210             if os.system('git-update-cache --remove -- %s' % f) != 0:
211                 raise GitException, 'Unable to remove %s' % f
212
213 def update_cache(files):
214     """Update the cache information for the given files
215     """
216     for f in files:
217         if os.path.exists(f):
218             os.system('git-update-cache -- %s' % f)
219         else:
220             os.system('git-update-cache --remove -- %s' % f)
221
222 def commit(message, files = [], parents = [], allowempty = False,
223            author_name = None, author_email = None, author_date = None,
224            committer_name = None, committer_email = None):
225     """Commit the current tree to repository
226     """
227     first = (parents == [])
228
229     # Get the tree status
230     if not first:
231         cache_files = __tree_status(files)
232
233     if not first and len(cache_files) == 0 and not allowempty:
234         raise GitException, 'No changes to commit'
235
236     # check for unresolved conflicts
237     if not first and len(filter(lambda x: x[0] not in ['M', 'N', 'D'],
238                                 cache_files)) != 0:
239         raise GitException, 'Commit failed: unresolved conflicts'
240
241     # get the commit message
242     f = file('.commitmsg', 'w+')
243     if message[-1] == '\n':
244         f.write(message)
245     else:
246         print >> f, message
247     f.close()
248
249     # update the cache
250     if not first:
251         for f in cache_files:
252             if f[0] == 'N':
253                 git_flag = '--add'
254             elif f[0] == 'D':
255                 git_flag = '--force-remove'
256             else:
257                 git_flag = '--'
258
259             if os.system('git-update-cache %s %s' % (git_flag, f[1])) != 0:
260                 raise GitException, 'Failed git-update-cache -- %s' % f[1]
261
262     # write the index to repository
263     tree_id = __output('git-write-tree')
264
265     # the commit
266     cmd = ''
267     if author_name:
268         cmd += 'GIT_AUTHOR_NAME="%s" ' % author_name
269     if author_email:
270         cmd += 'GIT_AUTHOR_EMAIL="%s" ' % author_email
271     if author_date:
272         cmd += 'GIT_AUTHOR_DATE="%s" ' % author_date
273     if committer_name:
274         cmd += 'GIT_COMMITTER_NAME="%s" ' % committer_name
275     if committer_email:
276         cmd += 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
277     cmd += 'git-commit-tree %s' % tree_id
278
279     # get the parents
280     for p in parents:
281         cmd += ' -p %s' % p
282
283     cmd += ' < .commitmsg'
284
285     commit_id = __output(cmd)
286     __set_head(commit_id)
287     os.remove('.commitmsg')
288
289     return commit_id
290
291 def merge(base, head1, head2):
292     """Perform a 3-way merge between base, head1 and head2 into the
293     local tree
294     """
295     if os.system('git-read-tree -u -m %s %s %s' % (base, head1, head2)) != 0:
296         raise GitException, 'git-read-tree failed (local changes maybe?)'
297
298     # this can fail if there are conflicts
299     if os.system('git-merge-cache -o gitmergeonefile.py -a') != 0:
300         raise GitException, 'git-merge-cache failed (possible conflicts)'
301
302     # this should not fail
303     if os.system('git-checkout-cache -f -a') != 0:
304         raise GitException, 'Failed git-checkout-cache'
305
306 def status(files = [], modified = False, new = False, deleted = False,
307            conflict = False, unknown = False):
308     """Show the tree status
309     """
310     cache_files = __tree_status(files, unknown = True)
311     all = not (modified or new or deleted or conflict or unknown)
312
313     if not all:
314         filestat = []
315         if modified:
316             filestat.append('M')
317         if new:
318             filestat.append('N')
319         if deleted:
320             filestat.append('D')
321         if conflict:
322             filestat.append('C')
323         if unknown:
324             filestat.append('?')
325         cache_files = filter(lambda x: x[0] in filestat, cache_files)
326
327     for fs in cache_files:
328         if all:
329             print '%s %s' % (fs[0], fs[1])
330         else:
331             print '%s' % fs[1]
332
333 def diff(files = [], rev1 = 'HEAD', rev2 = None, output = None,
334          append = False):
335     """Show the diff between rev1 and rev2
336     """
337     files_str = reduce(lambda x, y: x + ' ' + y, files, '')
338
339     extra_args = ''
340     if output:
341         if append:
342             extra_args += ' >> %s' % output
343         else:
344             extra_args += ' > %s' % output
345
346     os.system('git-update-cache --refresh > /dev/null')
347
348     if rev2:
349         if os.system('git-diff-tree -p %s %s %s %s'
350                      % (rev1, rev2, files_str, extra_args)) != 0:
351             raise GitException, 'git-diff-tree failed'
352     else:
353         if os.system('git-diff-cache -p %s %s %s'
354                      % (rev1, files_str, extra_args)) != 0:
355             raise GitException, 'git-diff-cache failed'
356
357 def diffstat(files = [], rev1 = 'HEAD', rev2 = None):
358     """Return the diffstat between rev1 and rev2
359     """
360     files_str = reduce(lambda x, y: x + ' ' + y, files, '')
361
362     os.system('git-update-cache --refresh > /dev/null')
363     ds_cmd = '| git-apply --stat'
364
365     if rev2:
366         f = os.popen('git-diff-tree -p %s %s %s %s'
367                      % (rev1, rev2, files_str, ds_cmd), 'r')
368         str = f.read().rstrip()
369         if f.close():
370             raise GitException, 'git-diff-tree failed'
371     else:
372         f = os.popen('git-diff-cache -p %s %s %s'
373                      % (rev1, files_str, ds_cmd), 'r')
374         str = f.read().rstrip()
375         if f.close():
376             raise GitException, 'git-diff-cache failed'
377
378     return str
379
380 def files(rev1, rev2):
381     """Return the files modified between rev1 and rev2
382     """
383     os.system('git-update-cache --refresh > /dev/null')
384
385     str = ''
386     f = os.popen('git-diff-tree -r %s %s' % (rev1, rev2),
387                  'r')
388     for line in f:
389         str += '%s %s\n' % tuple(line.split()[4:])
390     if f.close():
391         raise GitException, 'git-diff-tree failed'
392
393     return str.rstrip()
394
395 def checkout(files = [], force = False):
396     """Check out the given or all files
397     """
398     git_flags = ''
399     if force:
400         git_flags += ' -f'
401     if len(files) == 0:
402         git_flags += ' -a'
403     else:
404         git_flags += reduce(lambda x, y: x + ' ' + y, files, ' --')
405
406     if os.system('git-checkout-cache -q -u%s' % git_flags) != 0:
407         raise GitException, 'Failed git-checkout-cache -q -u%s' % git_flags
408
409 def switch(tree_id):
410     """Switch the tree to the given id
411     """
412     to_delete = filter(lambda x: x[0] == 'N', __tree_status(tree_id = tree_id))
413
414     if os.system('git-read-tree -m %s' % tree_id) != 0:
415         raise GitException, 'Failed git-read-tree -m %s' % tree_id
416
417     checkout(force = True)
418     __set_head(tree_id)
419
420     # checkout doesn't remove files
421     for fs in to_delete:
422         os.remove(fs[1])