6d3aa87ee5c924920bd4ee0794983a79e7cc63ed
[be.git] / libbe / storage / vcs / git.py
1 # Copyright (C) 2008-2010 Ben Finney <benf@cybersource.com.au>
2 #                         Chris Ball <cjb@laptop.org>
3 #                         Gianluca Montecchi <gian@grys.it>
4 #                         W. Trevor King <wking@drexel.edu>
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
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 along
17 # with this program; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20 """
21 Git backend.
22 """
23
24 import os
25 import os.path
26 import re
27 import shutil
28 import unittest
29
30 import libbe
31 import libbe.ui.util.user
32 import base
33
34 if libbe.TESTING == True:
35     import doctest
36     import sys
37
38
39 def new():
40     return Git()
41
42 class Git(base.VCS):
43     name='git'
44     client='git'
45
46     def __init__(self, *args, **kwargs):
47         base.VCS.__init__(self, *args, **kwargs)
48         self.versioned = True
49
50     def _vcs_version(self):
51         status,output,error = self._u_invoke_client('--version')
52         return output
53
54     def _vcs_get_user_id(self):
55         status,output,error = \
56             self._u_invoke_client('config', 'user.name', expect=(0,1))
57         if status == 0:
58             name = output.rstrip('\n')
59         else:
60             name = ''
61         status,output,error = \
62             self._u_invoke_client('config', 'user.email', expect=(0,1))
63         if status == 0:
64             email = output.rstrip('\n')
65         else:
66             email = ''
67         if name != '' or email != '': # got something!
68             # guess missing info, if necessary
69             if name == '':
70                 name = libbe.ui.util.user.get_fallback_username()
71             if email == '':
72                 email = libe.ui.util.user.get_fallback_email()
73             return libbe.ui.util.user.create_user_id(name, email)
74         return None # Git has no infomation
75
76     def _vcs_detect(self, path):
77         if self._u_search_parent_directories(path, '.git') != None :
78             return True
79         return False
80
81     def _vcs_root(self, path):
82         """Find the root of the deepest repository containing path."""
83         # Assume that nothing funny is going on; in particular, that we aren't
84         # dealing with a bare repo.
85         if os.path.isdir(path) != True:
86             path = os.path.dirname(path)
87         status,output,error = self._u_invoke_client('rev-parse', '--git-dir',
88                                                     cwd=path)
89         gitdir = os.path.join(path, output.rstrip('\n'))
90         dirname = os.path.abspath(os.path.dirname(gitdir))
91         return dirname
92
93     def _vcs_init(self, path):
94         self._u_invoke_client('init', cwd=path)
95
96     def _vcs_destroy(self):
97         vcs_dir = os.path.join(self.repo, '.git')
98         if os.path.exists(vcs_dir):
99             shutil.rmtree(vcs_dir)
100
101     def _vcs_add(self, path):
102         if os.path.isdir(path):
103             return
104         self._u_invoke_client('add', path)
105
106     def _vcs_remove(self, path):
107         if not os.path.isdir(self._u_abspath(path)):
108             self._u_invoke_client('rm', '-f', path)
109
110     def _vcs_update(self, path):
111         self._vcs_add(path)
112
113     def _vcs_get_file_contents(self, path, revision=None):
114         if revision == None:
115             return base.VCS._vcs_get_file_contents(self, path, revision)
116         else:
117             arg = '%s:%s' % (revision,path)
118             status,output,error = self._u_invoke_client('show', arg)
119             return output
120
121
122     def _vcs_path(self, id, revision):
123         return self._u_find_id(id, revision)
124
125     def _vcs_isdir(self, path, revision):
126         arg = '%s:%s' % (revision,path)
127         args = ['ls-tree', arg]
128         kwargs = {'expect':(0,128)}
129         status,output,error = self._u_invoke_client(*args, **kwargs)
130         if status != 0:
131             if 'not a tree object' in error:
132                 return False
133             raise base.CommandError(args, status, stderr=error)
134         return True
135
136     def _vcs_listdir(self, path, revision):
137         arg = '%s:%s' % (revision,path)
138         status,output,error = self._u_invoke_client(
139             'ls-tree', '--name-only', arg)
140         return output.rstrip('\n').splitlines()
141
142     def _vcs_commit(self, commitfile, allow_empty=False):
143         args = ['commit', '--all', '--file', commitfile]
144         if allow_empty == True:
145             args.append('--allow-empty')
146             status,output,error = self._u_invoke_client(*args)
147         else:
148             kwargs = {'expect':(0,1)}
149             status,output,error = self._u_invoke_client(*args, **kwargs)
150             strings = ['nothing to commit',
151                        'nothing added to commit']
152             if self._u_any_in_string(strings, output) == True:
153                 raise base.EmptyCommit()
154         full_revision = self._vcs_revision_id(-1)
155         assert full_revision[:7] in output, \
156             'Mismatched revisions:\n%s\n%s' % (full_revision, output)
157         return full_revision
158
159     def _vcs_revision_id(self, index):
160         args = ['rev-list', '--first-parent', '--reverse', 'HEAD']
161         kwargs = {'expect':(0,128)}
162         status,output,error = self._u_invoke_client(*args, **kwargs)
163         if status == 128:
164             if error.startswith("fatal: ambiguous argument 'HEAD': unknown "):
165                 return None
166             raise base.CommandError(args, status, stderr=error)
167         revisions = output.splitlines()
168         try:
169             if index > 0:
170                 return revisions[index-1]
171             elif index < 0:
172                 return revisions[index]
173             else:
174                 return None
175         except IndexError:
176             return None
177
178     def _diff(self, revision):
179         status,output,error = self._u_invoke_client('diff', revision)
180         return output
181
182     def _parse_diff(self, diff_text):
183         """
184         Example diff text:
185         
186         diff --git a/dir/changed b/dir/changed
187         index 6c3ea8c..2f2f7c7 100644
188         --- a/dir/changed
189         +++ b/dir/changed
190         @@ -1,3 +1,3 @@
191          hi
192         -there
193         +everyone and
194          joe
195         diff --git a/dir/deleted b/dir/deleted
196         deleted file mode 100644
197         index 225ec04..0000000
198         --- a/dir/deleted
199         +++ /dev/null
200         @@ -1,3 +0,0 @@
201         -in
202         -the
203         -beginning
204         diff --git a/dir/moved b/dir/moved
205         deleted file mode 100644
206         index 5ef102f..0000000
207         --- a/dir/moved
208         +++ /dev/null
209         @@ -1,4 +0,0 @@
210         -the
211         -ants
212         -go
213         -marching
214         diff --git a/dir/moved2 b/dir/moved2
215         new file mode 100644
216         index 0000000..5ef102f
217         --- /dev/null
218         +++ b/dir/moved2
219         @@ -0,0 +1,4 @@
220         +the
221         +ants
222         +go
223         +marching
224         diff --git a/dir/new b/dir/new
225         new file mode 100644
226         index 0000000..94954ab
227         --- /dev/null
228         +++ b/dir/new
229         @@ -0,0 +1,2 @@
230         +hello
231         +world
232         """
233         new = []
234         modified = []
235         removed = []
236         lines = diff_text.splitlines()
237         for i,line in enumerate(lines):
238             if not line.startswith('diff '):
239                 continue
240             file_a,file_b = line.split()[-2:]
241             assert file_a.startswith('a/'), \
242                 'missformed file_a %s' % file_a
243             assert file_b.startswith('b/'), \
244                 'missformed file_a %s' % file_b
245             file = file_a[2:]
246             assert file_b[2:] == file, \
247                 'diff file missmatch %s != %s' % (file_a, file_b)
248             if lines[i+1].startswith('new '):
249                 new.append(file)
250             elif lines[i+1].startswith('index '):
251                 modified.append(file)
252             elif lines[i+1].startswith('deleted '):
253                 removed.append(file)
254         return (new,modified,removed)
255
256     def _vcs_changed(self, revision):
257         return self._parse_diff(self._diff(revision))
258
259 \f
260 if libbe.TESTING == True:
261     base.make_vcs_testcase_subclasses(Git, sys.modules[__name__])
262
263     unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
264     suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])