##// END OF EJS Templates
Implemented pull command for remote repos for git...
marcink -
r2209:19a6c23a beta
parent child Browse files
Show More
@@ -1,286 +1,300 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.middleware.simplegit
3 rhodecode.lib.middleware.simplegit
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 SimpleGit middleware for handling git protocol request (push/clone etc.)
6 SimpleGit middleware for handling git protocol request (push/clone etc.)
7 It's implemented with basic auth function
7 It's implemented with basic auth function
8
8
9 :created_on: Apr 28, 2010
9 :created_on: Apr 28, 2010
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
12 :license: GPLv3, see COPYING for more details.
13 """
13 """
14 # This program is free software: you can redistribute it and/or modify
14 # This program is free software: you can redistribute it and/or modify
15 # it under the terms of the GNU General Public License as published by
15 # it under the terms of the GNU General Public License as published by
16 # the Free Software Foundation, either version 3 of the License, or
16 # the Free Software Foundation, either version 3 of the License, or
17 # (at your option) any later version.
17 # (at your option) any later version.
18 #
18 #
19 # This program is distributed in the hope that it will be useful,
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
22 # GNU General Public License for more details.
23 #
23 #
24 # You should have received a copy of the GNU General Public License
24 # You should have received a copy of the GNU General Public License
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26
26
27 import os
27 import os
28 import re
28 import re
29 import logging
29 import logging
30 import traceback
30 import traceback
31
31
32 from dulwich import server as dulserver
32 from dulwich import server as dulserver
33
33
34
34
35 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
35 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
36
36
37 def handle(self):
37 def handle(self):
38 write = lambda x: self.proto.write_sideband(1, x)
38 write = lambda x: self.proto.write_sideband(1, x)
39
39
40 graph_walker = dulserver.ProtocolGraphWalker(self,
40 graph_walker = dulserver.ProtocolGraphWalker(self,
41 self.repo.object_store,
41 self.repo.object_store,
42 self.repo.get_peeled)
42 self.repo.get_peeled)
43 objects_iter = self.repo.fetch_objects(
43 objects_iter = self.repo.fetch_objects(
44 graph_walker.determine_wants, graph_walker, self.progress,
44 graph_walker.determine_wants, graph_walker, self.progress,
45 get_tagged=self.get_tagged)
45 get_tagged=self.get_tagged)
46
46
47 # Did the process short-circuit (e.g. in a stateless RPC call)? Note
47 # Did the process short-circuit (e.g. in a stateless RPC call)? Note
48 # that the client still expects a 0-object pack in most cases.
48 # that the client still expects a 0-object pack in most cases.
49 if objects_iter is None:
49 if objects_iter is None:
50 return
50 return
51
51
52 self.progress("counting objects: %d, done.\n" % len(objects_iter))
52 self.progress("counting objects: %d, done.\n" % len(objects_iter))
53 dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
53 dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
54 objects_iter)
54 objects_iter)
55 messages = []
55 messages = []
56 messages.append('thank you for using rhodecode')
56 messages.append('thank you for using rhodecode')
57
57
58 for msg in messages:
58 for msg in messages:
59 self.progress(msg + "\n")
59 self.progress(msg + "\n")
60 # we are done
60 # we are done
61 self.proto.write("0000")
61 self.proto.write("0000")
62
62
63
63
64 dulserver.DEFAULT_HANDLERS = {
64 dulserver.DEFAULT_HANDLERS = {
65 'git-upload-pack': SimpleGitUploadPackHandler,
65 'git-upload-pack': SimpleGitUploadPackHandler,
66 'git-receive-pack': dulserver.ReceivePackHandler,
66 'git-receive-pack': dulserver.ReceivePackHandler,
67 }
67 }
68
68
69 from dulwich.repo import Repo
69 from dulwich.repo import Repo
70 from dulwich.web import make_wsgi_chain
70 from dulwich.web import make_wsgi_chain
71
71
72 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
72 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
73
73
74 from rhodecode.lib.utils2 import safe_str
74 from rhodecode.lib.utils2 import safe_str
75 from rhodecode.lib.base import BaseVCSController
75 from rhodecode.lib.base import BaseVCSController
76 from rhodecode.lib.auth import get_container_username
76 from rhodecode.lib.auth import get_container_username
77 from rhodecode.lib.utils import is_valid_repo, make_ui
77 from rhodecode.lib.utils import is_valid_repo, make_ui
78 from rhodecode.model.db import User
78 from rhodecode.model.db import User
79
79
80 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
80 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
81
81
82 log = logging.getLogger(__name__)
82 log = logging.getLogger(__name__)
83
83
84
84
85 GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')
85 GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')
86
86
87
87
88 def is_git(environ):
88 def is_git(environ):
89 path_info = environ['PATH_INFO']
89 path_info = environ['PATH_INFO']
90 isgit_path = GIT_PROTO_PAT.match(path_info)
90 isgit_path = GIT_PROTO_PAT.match(path_info)
91 log.debug('pathinfo: %s detected as GIT %s' % (
91 log.debug('pathinfo: %s detected as GIT %s' % (
92 path_info, isgit_path != None)
92 path_info, isgit_path != None)
93 )
93 )
94 return isgit_path
94 return isgit_path
95
95
96
96
97 class SimpleGit(BaseVCSController):
97 class SimpleGit(BaseVCSController):
98
98
99 def _handle_request(self, environ, start_response):
99 def _handle_request(self, environ, start_response):
100
100
101 if not is_git(environ):
101 if not is_git(environ):
102 return self.application(environ, start_response)
102 return self.application(environ, start_response)
103
103
104 ipaddr = self._get_ip_addr(environ)
104 ipaddr = self._get_ip_addr(environ)
105 username = None
105 username = None
106 self._git_first_op = False
106 self._git_first_op = False
107 # skip passing error to error controller
107 # skip passing error to error controller
108 environ['pylons.status_code_redirect'] = True
108 environ['pylons.status_code_redirect'] = True
109
109
110 #======================================================================
110 #======================================================================
111 # EXTRACT REPOSITORY NAME FROM ENV
111 # EXTRACT REPOSITORY NAME FROM ENV
112 #======================================================================
112 #======================================================================
113 try:
113 try:
114 repo_name = self.__get_repository(environ)
114 repo_name = self.__get_repository(environ)
115 log.debug('Extracted repo name is %s' % repo_name)
115 log.debug('Extracted repo name is %s' % repo_name)
116 except:
116 except:
117 return HTTPInternalServerError()(environ, start_response)
117 return HTTPInternalServerError()(environ, start_response)
118
118
119 # quick check if that dir exists...
119 # quick check if that dir exists...
120 if is_valid_repo(repo_name, self.basepath) is False:
120 if is_valid_repo(repo_name, self.basepath) is False:
121 return HTTPNotFound()(environ, start_response)
121 return HTTPNotFound()(environ, start_response)
122
122
123 #======================================================================
123 #======================================================================
124 # GET ACTION PULL or PUSH
124 # GET ACTION PULL or PUSH
125 #======================================================================
125 #======================================================================
126 action = self.__get_action(environ)
126 action = self.__get_action(environ)
127
127
128 #======================================================================
128 #======================================================================
129 # CHECK ANONYMOUS PERMISSION
129 # CHECK ANONYMOUS PERMISSION
130 #======================================================================
130 #======================================================================
131 if action in ['pull', 'push']:
131 if action in ['pull', 'push']:
132 anonymous_user = self.__get_user('default')
132 anonymous_user = self.__get_user('default')
133 username = anonymous_user.username
133 username = anonymous_user.username
134 anonymous_perm = self._check_permission(action, anonymous_user,
134 anonymous_perm = self._check_permission(action, anonymous_user,
135 repo_name)
135 repo_name)
136
136
137 if anonymous_perm is not True or anonymous_user.active is False:
137 if anonymous_perm is not True or anonymous_user.active is False:
138 if anonymous_perm is not True:
138 if anonymous_perm is not True:
139 log.debug('Not enough credentials to access this '
139 log.debug('Not enough credentials to access this '
140 'repository as anonymous user')
140 'repository as anonymous user')
141 if anonymous_user.active is False:
141 if anonymous_user.active is False:
142 log.debug('Anonymous access is disabled, running '
142 log.debug('Anonymous access is disabled, running '
143 'authentication')
143 'authentication')
144 #==============================================================
144 #==============================================================
145 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
145 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
146 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
146 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
147 #==============================================================
147 #==============================================================
148
148
149 # Attempting to retrieve username from the container
149 # Attempting to retrieve username from the container
150 username = get_container_username(environ, self.config)
150 username = get_container_username(environ, self.config)
151
151
152 # If not authenticated by the container, running basic auth
152 # If not authenticated by the container, running basic auth
153 if not username:
153 if not username:
154 self.authenticate.realm = \
154 self.authenticate.realm = \
155 safe_str(self.config['rhodecode_realm'])
155 safe_str(self.config['rhodecode_realm'])
156 result = self.authenticate(environ)
156 result = self.authenticate(environ)
157 if isinstance(result, str):
157 if isinstance(result, str):
158 AUTH_TYPE.update(environ, 'basic')
158 AUTH_TYPE.update(environ, 'basic')
159 REMOTE_USER.update(environ, result)
159 REMOTE_USER.update(environ, result)
160 username = result
160 username = result
161 else:
161 else:
162 return result.wsgi_application(environ, start_response)
162 return result.wsgi_application(environ, start_response)
163
163
164 #==============================================================
164 #==============================================================
165 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
165 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
166 #==============================================================
166 #==============================================================
167 if action in ['pull', 'push']:
167 if action in ['pull', 'push']:
168 try:
168 try:
169 user = self.__get_user(username)
169 user = self.__get_user(username)
170 if user is None or not user.active:
170 if user is None or not user.active:
171 return HTTPForbidden()(environ, start_response)
171 return HTTPForbidden()(environ, start_response)
172 username = user.username
172 username = user.username
173 except:
173 except:
174 log.error(traceback.format_exc())
174 log.error(traceback.format_exc())
175 return HTTPInternalServerError()(environ,
175 return HTTPInternalServerError()(environ,
176 start_response)
176 start_response)
177
177
178 #check permissions for this repository
178 #check permissions for this repository
179 perm = self._check_permission(action, user, repo_name)
179 perm = self._check_permission(action, user, repo_name)
180 if perm is not True:
180 if perm is not True:
181 return HTTPForbidden()(environ, start_response)
181 return HTTPForbidden()(environ, start_response)
182 extras = {
182 extras = {
183 'ip': ipaddr,
183 'ip': ipaddr,
184 'username': username,
184 'username': username,
185 'action': action,
185 'action': action,
186 'repository': repo_name,
186 'repository': repo_name,
187 'scm': 'git',
187 'scm': 'git',
188 }
188 }
189
189
190 #===================================================================
190 #===================================================================
191 # GIT REQUEST HANDLING
191 # GIT REQUEST HANDLING
192 #===================================================================
192 #===================================================================
193 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
193 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
194 log.debug('Repository path is %s' % repo_path)
194 log.debug('Repository path is %s' % repo_path)
195
195
196 baseui = make_ui('db')
196 baseui = make_ui('db')
197 for k, v in extras.items():
197 self.__inject_extras(repo_path, baseui, extras)
198 baseui.setconfig('rhodecode_extras', k, v)
198
199
199
200 try:
200 try:
201 # invalidate cache on push
201 # invalidate cache on push
202 if action == 'push':
202 if action == 'push':
203 self._invalidate_cache(repo_name)
203 self._invalidate_cache(repo_name)
204 self._handle_githooks(action, baseui, environ)
204 self._handle_githooks(action, baseui, environ)
205
205
206 log.info('%s action on GIT repo "%s"' % (action, repo_name))
206 log.info('%s action on GIT repo "%s"' % (action, repo_name))
207 app = self.__make_app(repo_name, repo_path)
207 app = self.__make_app(repo_name, repo_path)
208 return app(environ, start_response)
208 return app(environ, start_response)
209 except Exception:
209 except Exception:
210 log.error(traceback.format_exc())
210 log.error(traceback.format_exc())
211 return HTTPInternalServerError()(environ, start_response)
211 return HTTPInternalServerError()(environ, start_response)
212
212
213 def __make_app(self, repo_name, repo_path):
213 def __make_app(self, repo_name, repo_path):
214 """
214 """
215 Make an wsgi application using dulserver
215 Make an wsgi application using dulserver
216
216
217 :param repo_name: name of the repository
217 :param repo_name: name of the repository
218 :param repo_path: full path to the repository
218 :param repo_path: full path to the repository
219 """
219 """
220 _d = {'/' + repo_name: Repo(repo_path)}
220 _d = {'/' + repo_name: Repo(repo_path)}
221 backend = dulserver.DictBackend(_d)
221 backend = dulserver.DictBackend(_d)
222 gitserve = make_wsgi_chain(backend)
222 gitserve = make_wsgi_chain(backend)
223
223
224 return gitserve
224 return gitserve
225
225
226 def __get_repository(self, environ):
226 def __get_repository(self, environ):
227 """
227 """
228 Get's repository name out of PATH_INFO header
228 Get's repository name out of PATH_INFO header
229
229
230 :param environ: environ where PATH_INFO is stored
230 :param environ: environ where PATH_INFO is stored
231 """
231 """
232 try:
232 try:
233 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
233 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
234 repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
234 repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
235 except:
235 except:
236 log.error(traceback.format_exc())
236 log.error(traceback.format_exc())
237 raise
237 raise
238
238
239 return repo_name
239 return repo_name
240
240
241 def __get_user(self, username):
241 def __get_user(self, username):
242 return User.get_by_username(username)
242 return User.get_by_username(username)
243
243
244 def __get_action(self, environ):
244 def __get_action(self, environ):
245 """
245 """
246 Maps git request commands into a pull or push command.
246 Maps git request commands into a pull or push command.
247
247
248 :param environ:
248 :param environ:
249 """
249 """
250 service = environ['QUERY_STRING'].split('=')
250 service = environ['QUERY_STRING'].split('=')
251
251
252 if len(service) > 1:
252 if len(service) > 1:
253 service_cmd = service[1]
253 service_cmd = service[1]
254 mapping = {
254 mapping = {
255 'git-receive-pack': 'push',
255 'git-receive-pack': 'push',
256 'git-upload-pack': 'pull',
256 'git-upload-pack': 'pull',
257 }
257 }
258 op = mapping[service_cmd]
258 op = mapping[service_cmd]
259 self._git_stored_op = op
259 self._git_stored_op = op
260 return op
260 return op
261 else:
261 else:
262 # try to fallback to stored variable as we don't know if the last
262 # try to fallback to stored variable as we don't know if the last
263 # operation is pull/push
263 # operation is pull/push
264 op = getattr(self, '_git_stored_op', 'pull')
264 op = getattr(self, '_git_stored_op', 'pull')
265 return op
265 return op
266
266
267 def _handle_githooks(self, action, baseui, environ):
267 def _handle_githooks(self, action, baseui, environ):
268
269 from rhodecode.lib.hooks import log_pull_action, log_push_action
268 from rhodecode.lib.hooks import log_pull_action, log_push_action
270 service = environ['QUERY_STRING'].split('=')
269 service = environ['QUERY_STRING'].split('=')
271 if len(service) < 2:
270 if len(service) < 2:
272 return
271 return
273
272
274 class cont(object):
273 from rhodecode.model.db import Repository
275 pass
274 _repo = Repository.get_by_repo_name(repo_name)
276
275 _repo = _repo.scm_instance
277 repo = cont()
276 _repo._repo.ui = baseui
278 setattr(repo, 'ui', baseui)
279
277
280 push_hook = 'pretxnchangegroup.push_logger'
278 push_hook = 'pretxnchangegroup.push_logger'
281 pull_hook = 'preoutgoing.pull_logger'
279 pull_hook = 'preoutgoing.pull_logger'
282 _hooks = dict(baseui.configitems('hooks')) or {}
280 _hooks = dict(baseui.configitems('hooks')) or {}
283 if action == 'push' and _hooks.get(push_hook):
281 if action == 'push' and _hooks.get(push_hook):
284 log_push_action(ui=baseui, repo=repo)
282 log_push_action(ui=baseui, repo=repo._repo)
285 elif action == 'pull' and _hooks.get(pull_hook):
283 elif action == 'pull' and _hooks.get(pull_hook):
286 log_pull_action(ui=baseui, repo=repo)
284 log_pull_action(ui=baseui, repo=repo._repo)
285
286 def __inject_extras(self, repo_path, baseui, extras={}):
287 """
288 Injects some extra params into baseui instance
289
290 :param baseui: baseui instance
291 :param extras: dict with extra params to put into baseui
292 """
293
294 # make our hgweb quiet so it doesn't print output
295 baseui.setconfig('ui', 'quiet', 'true')
296
297 #inject some additional parameters that will be available in ui
298 #for hooks
299 for k, v in extras.items():
300 baseui.setconfig('rhodecode_extras', k, v)
@@ -1,525 +1,545 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.backends.git
3 vcs.backends.git
4 ~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~
5
5
6 Git backend implementation.
6 Git backend implementation.
7
7
8 :created_on: Apr 8, 2010
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
10 """
11
11
12 import os
12 import os
13 import re
13 import re
14 import time
14 import time
15 import posixpath
15 import posixpath
16 from dulwich.repo import Repo, NotGitRepository
16 from dulwich.repo import Repo, NotGitRepository
17 #from dulwich.config import ConfigFile
17 #from dulwich.config import ConfigFile
18 from string import Template
18 from string import Template
19 from subprocess import Popen, PIPE
19 from subprocess import Popen, PIPE
20 from rhodecode.lib.vcs.backends.base import BaseRepository
20 from rhodecode.lib.vcs.backends.base import BaseRepository
21 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
21 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
22 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
22 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
23 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
23 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
24 from rhodecode.lib.vcs.exceptions import RepositoryError
24 from rhodecode.lib.vcs.exceptions import RepositoryError
25 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
25 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
26 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
26 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
27 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
27 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
28 from rhodecode.lib.vcs.utils.lazy import LazyProperty
28 from rhodecode.lib.vcs.utils.lazy import LazyProperty
29 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
29 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
30 from rhodecode.lib.vcs.utils.paths import abspath
30 from rhodecode.lib.vcs.utils.paths import abspath
31 from rhodecode.lib.vcs.utils.paths import get_user_home
31 from rhodecode.lib.vcs.utils.paths import get_user_home
32 from .workdir import GitWorkdir
32 from .workdir import GitWorkdir
33 from .changeset import GitChangeset
33 from .changeset import GitChangeset
34 from .inmemory import GitInMemoryChangeset
34 from .inmemory import GitInMemoryChangeset
35 from .config import ConfigFile
35 from .config import ConfigFile
36
36
37
37
38 class GitRepository(BaseRepository):
38 class GitRepository(BaseRepository):
39 """
39 """
40 Git repository backend.
40 Git repository backend.
41 """
41 """
42 DEFAULT_BRANCH_NAME = 'master'
42 DEFAULT_BRANCH_NAME = 'master'
43 scm = 'git'
43 scm = 'git'
44
44
45 def __init__(self, repo_path, create=False, src_url=None,
45 def __init__(self, repo_path, create=False, src_url=None,
46 update_after_clone=False, bare=False):
46 update_after_clone=False, bare=False):
47
47
48 self.path = abspath(repo_path)
48 self.path = abspath(repo_path)
49 self._repo = self._get_repo(create, src_url, update_after_clone, bare)
49 self._repo = self._get_repo(create, src_url, update_after_clone, bare)
50 #temporary set that to now at later we will move it to constructor
51 baseui = None
52 if baseui is None:
53 from mercurial.ui import ui
54 baseui = ui()
55 # patch the instance of GitRepo with an "FAKE" ui object to add
56 # compatibility layer with Mercurial
57 setattr(self._repo, 'ui', baseui)
58
50 try:
59 try:
51 self.head = self._repo.head()
60 self.head = self._repo.head()
52 except KeyError:
61 except KeyError:
53 self.head = None
62 self.head = None
54
63
55 self._config_files = [
64 self._config_files = [
56 bare and abspath(self.path, 'config') or abspath(self.path, '.git',
65 bare and abspath(self.path, 'config') or abspath(self.path, '.git',
57 'config'),
66 'config'),
58 abspath(get_user_home(), '.gitconfig'),
67 abspath(get_user_home(), '.gitconfig'),
59 ]
68 ]
60
69
61 @LazyProperty
70 @LazyProperty
62 def revisions(self):
71 def revisions(self):
63 """
72 """
64 Returns list of revisions' ids, in ascending order. Being lazy
73 Returns list of revisions' ids, in ascending order. Being lazy
65 attribute allows external tools to inject shas from cache.
74 attribute allows external tools to inject shas from cache.
66 """
75 """
67 return self._get_all_revisions()
76 return self._get_all_revisions()
68
77
69 def run_git_command(self, cmd):
78 def run_git_command(self, cmd):
70 """
79 """
71 Runs given ``cmd`` as git command and returns tuple
80 Runs given ``cmd`` as git command and returns tuple
72 (returncode, stdout, stderr).
81 (returncode, stdout, stderr).
73
82
74 .. note::
83 .. note::
75 This method exists only until log/blame functionality is implemented
84 This method exists only until log/blame functionality is implemented
76 at Dulwich (see https://bugs.launchpad.net/bugs/645142). Parsing
85 at Dulwich (see https://bugs.launchpad.net/bugs/645142). Parsing
77 os command's output is road to hell...
86 os command's output is road to hell...
78
87
79 :param cmd: git command to be executed
88 :param cmd: git command to be executed
80 """
89 """
81
90
82 _copts = ['-c', 'core.quotepath=false', ]
91 _copts = ['-c', 'core.quotepath=false', ]
83 _str_cmd = False
92 _str_cmd = False
84 if isinstance(cmd, basestring):
93 if isinstance(cmd, basestring):
85 cmd = [cmd]
94 cmd = [cmd]
86 _str_cmd = True
95 _str_cmd = True
87
96
88 cmd = ['GIT_CONFIG_NOGLOBAL=1', 'git'] + _copts + cmd
97 cmd = ['GIT_CONFIG_NOGLOBAL=1', 'git'] + _copts + cmd
89 if _str_cmd:
98 if _str_cmd:
90 cmd = ' '.join(cmd)
99 cmd = ' '.join(cmd)
91 try:
100 try:
92 opts = dict(
101 opts = dict(
93 shell=isinstance(cmd, basestring),
102 shell=isinstance(cmd, basestring),
94 stdout=PIPE,
103 stdout=PIPE,
95 stderr=PIPE)
104 stderr=PIPE)
96 if os.path.isdir(self.path):
105 if os.path.isdir(self.path):
97 opts['cwd'] = self.path
106 opts['cwd'] = self.path
98 p = Popen(cmd, **opts)
107 p = Popen(cmd, **opts)
99 except OSError, err:
108 except OSError, err:
100 raise RepositoryError("Couldn't run git command (%s).\n"
109 raise RepositoryError("Couldn't run git command (%s).\n"
101 "Original error was:%s" % (cmd, err))
110 "Original error was:%s" % (cmd, err))
102 so, se = p.communicate()
111 so, se = p.communicate()
103 if not se.startswith("fatal: bad default revision 'HEAD'") and \
112 if not se.startswith("fatal: bad default revision 'HEAD'") and \
104 p.returncode != 0:
113 p.returncode != 0:
105 raise RepositoryError("Couldn't run git command (%s).\n"
114 raise RepositoryError("Couldn't run git command (%s).\n"
106 "stderr:\n%s" % (cmd, se))
115 "stderr:\n%s" % (cmd, se))
107 return so, se
116 return so, se
108
117
109 def _check_url(self, url):
118 def _check_url(self, url):
110 """
119 """
111 Functon will check given url and try to verify if it's a valid
120 Functon will check given url and try to verify if it's a valid
112 link. Sometimes it may happened that mercurial will issue basic
121 link. Sometimes it may happened that mercurial will issue basic
113 auth request that can cause whole API to hang when used from python
122 auth request that can cause whole API to hang when used from python
114 or other external calls.
123 or other external calls.
115
124
116 On failures it'll raise urllib2.HTTPError
125 On failures it'll raise urllib2.HTTPError
117 """
126 """
118
127
119 #TODO: implement this
128 #TODO: implement this
120 pass
129 pass
121
130
122 def _get_repo(self, create, src_url=None, update_after_clone=False,
131 def _get_repo(self, create, src_url=None, update_after_clone=False,
123 bare=False):
132 bare=False):
124 if create and os.path.exists(self.path):
133 if create and os.path.exists(self.path):
125 raise RepositoryError("Location already exist")
134 raise RepositoryError("Location already exist")
126 if src_url and not create:
135 if src_url and not create:
127 raise RepositoryError("Create should be set to True if src_url is "
136 raise RepositoryError("Create should be set to True if src_url is "
128 "given (clone operation creates repository)")
137 "given (clone operation creates repository)")
129 try:
138 try:
130 if create and src_url:
139 if create and src_url:
131 self._check_url(src_url)
140 self._check_url(src_url)
132 self.clone(src_url, update_after_clone, bare)
141 self.clone(src_url, update_after_clone, bare)
133 return Repo(self.path)
142 return Repo(self.path)
134 elif create:
143 elif create:
135 os.mkdir(self.path)
144 os.mkdir(self.path)
136 if bare:
145 if bare:
137 return Repo.init_bare(self.path)
146 return Repo.init_bare(self.path)
138 else:
147 else:
139 return Repo.init(self.path)
148 return Repo.init(self.path)
140 else:
149 else:
141 return Repo(self.path)
150 return Repo(self.path)
142 except (NotGitRepository, OSError), err:
151 except (NotGitRepository, OSError), err:
143 raise RepositoryError(err)
152 raise RepositoryError(err)
144
153
145 def _get_all_revisions(self):
154 def _get_all_revisions(self):
146 cmd = 'rev-list --all --date-order'
155 cmd = 'rev-list --all --date-order'
147 try:
156 try:
148 so, se = self.run_git_command(cmd)
157 so, se = self.run_git_command(cmd)
149 except RepositoryError:
158 except RepositoryError:
150 # Can be raised for empty repositories
159 # Can be raised for empty repositories
151 return []
160 return []
152 revisions = so.splitlines()
161 revisions = so.splitlines()
153 revisions.reverse()
162 revisions.reverse()
154 return revisions
163 return revisions
155
164
156 def _get_revision(self, revision):
165 def _get_revision(self, revision):
157 """
166 """
158 For git backend we always return integer here. This way we ensure
167 For git backend we always return integer here. This way we ensure
159 that changset's revision attribute would become integer.
168 that changset's revision attribute would become integer.
160 """
169 """
161 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
170 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
162 is_bstr = lambda o: isinstance(o, (str, unicode))
171 is_bstr = lambda o: isinstance(o, (str, unicode))
163 is_null = lambda o: len(o) == revision.count('0')
172 is_null = lambda o: len(o) == revision.count('0')
164
173
165 if len(self.revisions) == 0:
174 if len(self.revisions) == 0:
166 raise EmptyRepositoryError("There are no changesets yet")
175 raise EmptyRepositoryError("There are no changesets yet")
167
176
168 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
177 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
169 revision = self.revisions[-1]
178 revision = self.revisions[-1]
170
179
171 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
180 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
172 or isinstance(revision, int) or is_null(revision)):
181 or isinstance(revision, int) or is_null(revision)):
173 try:
182 try:
174 revision = self.revisions[int(revision)]
183 revision = self.revisions[int(revision)]
175 except:
184 except:
176 raise ChangesetDoesNotExistError("Revision %r does not exist "
185 raise ChangesetDoesNotExistError("Revision %r does not exist "
177 "for this repository %s" % (revision, self))
186 "for this repository %s" % (revision, self))
178
187
179 elif is_bstr(revision):
188 elif is_bstr(revision):
180 if not pattern.match(revision) or revision not in self.revisions:
189 if not pattern.match(revision) or revision not in self.revisions:
181 raise ChangesetDoesNotExistError("Revision %r does not exist "
190 raise ChangesetDoesNotExistError("Revision %r does not exist "
182 "for this repository %s" % (revision, self))
191 "for this repository %s" % (revision, self))
183
192
184 # Ensure we return full id
193 # Ensure we return full id
185 if not pattern.match(str(revision)):
194 if not pattern.match(str(revision)):
186 raise ChangesetDoesNotExistError("Given revision %r not recognized"
195 raise ChangesetDoesNotExistError("Given revision %r not recognized"
187 % revision)
196 % revision)
188 return revision
197 return revision
189
198
190 def _get_archives(self, archive_name='tip'):
199 def _get_archives(self, archive_name='tip'):
191
200
192 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
201 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
193 yield {"type": i[0], "extension": i[1], "node": archive_name}
202 yield {"type": i[0], "extension": i[1], "node": archive_name}
194
203
195 def _get_url(self, url):
204 def _get_url(self, url):
196 """
205 """
197 Returns normalized url. If schema is not given, would fall to
206 Returns normalized url. If schema is not given, would fall to
198 filesystem (``file:///``) schema.
207 filesystem (``file:///``) schema.
199 """
208 """
200 url = str(url)
209 url = str(url)
201 if url != 'default' and not '://' in url:
210 if url != 'default' and not '://' in url:
202 url = ':///'.join(('file', url))
211 url = ':///'.join(('file', url))
203 return url
212 return url
204
213
205 @LazyProperty
214 @LazyProperty
206 def name(self):
215 def name(self):
207 return os.path.basename(self.path)
216 return os.path.basename(self.path)
208
217
209 @LazyProperty
218 @LazyProperty
210 def last_change(self):
219 def last_change(self):
211 """
220 """
212 Returns last change made on this repository as datetime object
221 Returns last change made on this repository as datetime object
213 """
222 """
214 return date_fromtimestamp(self._get_mtime(), makedate()[1])
223 return date_fromtimestamp(self._get_mtime(), makedate()[1])
215
224
216 def _get_mtime(self):
225 def _get_mtime(self):
217 try:
226 try:
218 return time.mktime(self.get_changeset().date.timetuple())
227 return time.mktime(self.get_changeset().date.timetuple())
219 except RepositoryError:
228 except RepositoryError:
220 # fallback to filesystem
229 # fallback to filesystem
221 in_path = os.path.join(self.path, '.git', "index")
230 in_path = os.path.join(self.path, '.git', "index")
222 he_path = os.path.join(self.path, '.git', "HEAD")
231 he_path = os.path.join(self.path, '.git', "HEAD")
223 if os.path.exists(in_path):
232 if os.path.exists(in_path):
224 return os.stat(in_path).st_mtime
233 return os.stat(in_path).st_mtime
225 else:
234 else:
226 return os.stat(he_path).st_mtime
235 return os.stat(he_path).st_mtime
227
236
228 @LazyProperty
237 @LazyProperty
229 def description(self):
238 def description(self):
230 undefined_description = u'unknown'
239 undefined_description = u'unknown'
231 description_path = os.path.join(self.path, '.git', 'description')
240 description_path = os.path.join(self.path, '.git', 'description')
232 if os.path.isfile(description_path):
241 if os.path.isfile(description_path):
233 return safe_unicode(open(description_path).read())
242 return safe_unicode(open(description_path).read())
234 else:
243 else:
235 return undefined_description
244 return undefined_description
236
245
237 @LazyProperty
246 @LazyProperty
238 def contact(self):
247 def contact(self):
239 undefined_contact = u'Unknown'
248 undefined_contact = u'Unknown'
240 return undefined_contact
249 return undefined_contact
241
250
242 @property
251 @property
243 def branches(self):
252 def branches(self):
244 if not self.revisions:
253 if not self.revisions:
245 return {}
254 return {}
246 refs = self._repo.refs.as_dict()
255 refs = self._repo.refs.as_dict()
247 sortkey = lambda ctx: ctx[0]
256 sortkey = lambda ctx: ctx[0]
248 _branches = [('/'.join(ref.split('/')[2:]), head)
257 _branches = [('/'.join(ref.split('/')[2:]), head)
249 for ref, head in refs.items()
258 for ref, head in refs.items()
250 if ref.startswith('refs/heads/') and not ref.endswith('/HEAD')]
259 if ref.startswith('refs/heads/') and not ref.endswith('/HEAD')]
251 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
260 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
252
261
253 def _heads(self, reverse=False):
262 def _heads(self, reverse=False):
254 refs = self._repo.get_refs()
263 refs = self._repo.get_refs()
255 heads = {}
264 heads = {}
256
265
257 for key, val in refs.items():
266 for key, val in refs.items():
258 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
267 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
259 if key.startswith(ref_key):
268 if key.startswith(ref_key):
260 n = key[len(ref_key):]
269 n = key[len(ref_key):]
261 if n not in ['HEAD']:
270 if n not in ['HEAD']:
262 heads[n] = val
271 heads[n] = val
263
272
264 return heads if reverse else dict((y,x) for x,y in heads.iteritems())
273 return heads if reverse else dict((y,x) for x,y in heads.iteritems())
265
274
266 def _get_tags(self):
275 def _get_tags(self):
267 if not self.revisions:
276 if not self.revisions:
268 return {}
277 return {}
269 sortkey = lambda ctx: ctx[0]
278 sortkey = lambda ctx: ctx[0]
270 _tags = [('/'.join(ref.split('/')[2:]), head) for ref, head in
279 _tags = [('/'.join(ref.split('/')[2:]), head) for ref, head in
271 self._repo.get_refs().items() if ref.startswith('refs/tags/')]
280 self._repo.get_refs().items() if ref.startswith('refs/tags/')]
272 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
281 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
273
282
274 @LazyProperty
283 @LazyProperty
275 def tags(self):
284 def tags(self):
276 return self._get_tags()
285 return self._get_tags()
277
286
278 def tag(self, name, user, revision=None, message=None, date=None,
287 def tag(self, name, user, revision=None, message=None, date=None,
279 **kwargs):
288 **kwargs):
280 """
289 """
281 Creates and returns a tag for the given ``revision``.
290 Creates and returns a tag for the given ``revision``.
282
291
283 :param name: name for new tag
292 :param name: name for new tag
284 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
293 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
285 :param revision: changeset id for which new tag would be created
294 :param revision: changeset id for which new tag would be created
286 :param message: message of the tag's commit
295 :param message: message of the tag's commit
287 :param date: date of tag's commit
296 :param date: date of tag's commit
288
297
289 :raises TagAlreadyExistError: if tag with same name already exists
298 :raises TagAlreadyExistError: if tag with same name already exists
290 """
299 """
291 if name in self.tags:
300 if name in self.tags:
292 raise TagAlreadyExistError("Tag %s already exists" % name)
301 raise TagAlreadyExistError("Tag %s already exists" % name)
293 changeset = self.get_changeset(revision)
302 changeset = self.get_changeset(revision)
294 message = message or "Added tag %s for commit %s" % (name,
303 message = message or "Added tag %s for commit %s" % (name,
295 changeset.raw_id)
304 changeset.raw_id)
296 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
305 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
297
306
298 self.tags = self._get_tags()
307 self.tags = self._get_tags()
299 return changeset
308 return changeset
300
309
301 def remove_tag(self, name, user, message=None, date=None):
310 def remove_tag(self, name, user, message=None, date=None):
302 """
311 """
303 Removes tag with the given ``name``.
312 Removes tag with the given ``name``.
304
313
305 :param name: name of the tag to be removed
314 :param name: name of the tag to be removed
306 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
315 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
307 :param message: message of the tag's removal commit
316 :param message: message of the tag's removal commit
308 :param date: date of tag's removal commit
317 :param date: date of tag's removal commit
309
318
310 :raises TagDoesNotExistError: if tag with given name does not exists
319 :raises TagDoesNotExistError: if tag with given name does not exists
311 """
320 """
312 if name not in self.tags:
321 if name not in self.tags:
313 raise TagDoesNotExistError("Tag %s does not exist" % name)
322 raise TagDoesNotExistError("Tag %s does not exist" % name)
314 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
323 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
315 try:
324 try:
316 os.remove(tagpath)
325 os.remove(tagpath)
317 self.tags = self._get_tags()
326 self.tags = self._get_tags()
318 except OSError, e:
327 except OSError, e:
319 raise RepositoryError(e.strerror)
328 raise RepositoryError(e.strerror)
320
329
321 def get_changeset(self, revision=None):
330 def get_changeset(self, revision=None):
322 """
331 """
323 Returns ``GitChangeset`` object representing commit from git repository
332 Returns ``GitChangeset`` object representing commit from git repository
324 at the given revision or head (most recent commit) if None given.
333 at the given revision or head (most recent commit) if None given.
325 """
334 """
326 if isinstance(revision, GitChangeset):
335 if isinstance(revision, GitChangeset):
327 return revision
336 return revision
328 revision = self._get_revision(revision)
337 revision = self._get_revision(revision)
329 changeset = GitChangeset(repository=self, revision=revision)
338 changeset = GitChangeset(repository=self, revision=revision)
330 return changeset
339 return changeset
331
340
332 def get_changesets(self, start=None, end=None, start_date=None,
341 def get_changesets(self, start=None, end=None, start_date=None,
333 end_date=None, branch_name=None, reverse=False):
342 end_date=None, branch_name=None, reverse=False):
334 """
343 """
335 Returns iterator of ``GitChangeset`` objects from start to end (both
344 Returns iterator of ``GitChangeset`` objects from start to end (both
336 are inclusive), in ascending date order (unless ``reverse`` is set).
345 are inclusive), in ascending date order (unless ``reverse`` is set).
337
346
338 :param start: changeset ID, as str; first returned changeset
347 :param start: changeset ID, as str; first returned changeset
339 :param end: changeset ID, as str; last returned changeset
348 :param end: changeset ID, as str; last returned changeset
340 :param start_date: if specified, changesets with commit date less than
349 :param start_date: if specified, changesets with commit date less than
341 ``start_date`` would be filtered out from returned set
350 ``start_date`` would be filtered out from returned set
342 :param end_date: if specified, changesets with commit date greater than
351 :param end_date: if specified, changesets with commit date greater than
343 ``end_date`` would be filtered out from returned set
352 ``end_date`` would be filtered out from returned set
344 :param branch_name: if specified, changesets not reachable from given
353 :param branch_name: if specified, changesets not reachable from given
345 branch would be filtered out from returned set
354 branch would be filtered out from returned set
346 :param reverse: if ``True``, returned generator would be reversed
355 :param reverse: if ``True``, returned generator would be reversed
347 (meaning that returned changesets would have descending date order)
356 (meaning that returned changesets would have descending date order)
348
357
349 :raise BranchDoesNotExistError: If given ``branch_name`` does not
358 :raise BranchDoesNotExistError: If given ``branch_name`` does not
350 exist.
359 exist.
351 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
360 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
352 ``end`` could not be found.
361 ``end`` could not be found.
353
362
354 """
363 """
355 if branch_name and branch_name not in self.branches:
364 if branch_name and branch_name not in self.branches:
356 raise BranchDoesNotExistError("Branch '%s' not found" \
365 raise BranchDoesNotExistError("Branch '%s' not found" \
357 % branch_name)
366 % branch_name)
358 # %H at format means (full) commit hash, initial hashes are retrieved
367 # %H at format means (full) commit hash, initial hashes are retrieved
359 # in ascending date order
368 # in ascending date order
360 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
369 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
361 cmd_params = {}
370 cmd_params = {}
362 if start_date:
371 if start_date:
363 cmd_template += ' --since "$since"'
372 cmd_template += ' --since "$since"'
364 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
373 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
365 if end_date:
374 if end_date:
366 cmd_template += ' --until "$until"'
375 cmd_template += ' --until "$until"'
367 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
376 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
368 if branch_name:
377 if branch_name:
369 cmd_template += ' $branch_name'
378 cmd_template += ' $branch_name'
370 cmd_params['branch_name'] = branch_name
379 cmd_params['branch_name'] = branch_name
371 else:
380 else:
372 cmd_template += ' --all'
381 cmd_template += ' --all'
373
382
374 cmd = Template(cmd_template).safe_substitute(**cmd_params)
383 cmd = Template(cmd_template).safe_substitute(**cmd_params)
375 revs = self.run_git_command(cmd)[0].splitlines()
384 revs = self.run_git_command(cmd)[0].splitlines()
376 start_pos = 0
385 start_pos = 0
377 end_pos = len(revs)
386 end_pos = len(revs)
378 if start:
387 if start:
379 _start = self._get_revision(start)
388 _start = self._get_revision(start)
380 try:
389 try:
381 start_pos = revs.index(_start)
390 start_pos = revs.index(_start)
382 except ValueError:
391 except ValueError:
383 pass
392 pass
384
393
385 if end is not None:
394 if end is not None:
386 _end = self._get_revision(end)
395 _end = self._get_revision(end)
387 try:
396 try:
388 end_pos = revs.index(_end)
397 end_pos = revs.index(_end)
389 except ValueError:
398 except ValueError:
390 pass
399 pass
391
400
392 if None not in [start, end] and start_pos > end_pos:
401 if None not in [start, end] and start_pos > end_pos:
393 raise RepositoryError('start cannot be after end')
402 raise RepositoryError('start cannot be after end')
394
403
395 if end_pos is not None:
404 if end_pos is not None:
396 end_pos += 1
405 end_pos += 1
397
406
398 revs = revs[start_pos:end_pos]
407 revs = revs[start_pos:end_pos]
399 if reverse:
408 if reverse:
400 revs = reversed(revs)
409 revs = reversed(revs)
401 for rev in revs:
410 for rev in revs:
402 yield self.get_changeset(rev)
411 yield self.get_changeset(rev)
403
412
404 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
413 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
405 context=3):
414 context=3):
406 """
415 """
407 Returns (git like) *diff*, as plain text. Shows changes introduced by
416 Returns (git like) *diff*, as plain text. Shows changes introduced by
408 ``rev2`` since ``rev1``.
417 ``rev2`` since ``rev1``.
409
418
410 :param rev1: Entry point from which diff is shown. Can be
419 :param rev1: Entry point from which diff is shown. Can be
411 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
420 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
412 the changes since empty state of the repository until ``rev2``
421 the changes since empty state of the repository until ``rev2``
413 :param rev2: Until which revision changes should be shown.
422 :param rev2: Until which revision changes should be shown.
414 :param ignore_whitespace: If set to ``True``, would not show whitespace
423 :param ignore_whitespace: If set to ``True``, would not show whitespace
415 changes. Defaults to ``False``.
424 changes. Defaults to ``False``.
416 :param context: How many lines before/after changed lines should be
425 :param context: How many lines before/after changed lines should be
417 shown. Defaults to ``3``.
426 shown. Defaults to ``3``.
418 """
427 """
419 flags = ['-U%s' % context]
428 flags = ['-U%s' % context]
420 if ignore_whitespace:
429 if ignore_whitespace:
421 flags.append('-w')
430 flags.append('-w')
422
431
423 if rev1 == self.EMPTY_CHANGESET:
432 if rev1 == self.EMPTY_CHANGESET:
424 rev2 = self.get_changeset(rev2).raw_id
433 rev2 = self.get_changeset(rev2).raw_id
425 cmd = ' '.join(['show'] + flags + [rev2])
434 cmd = ' '.join(['show'] + flags + [rev2])
426 else:
435 else:
427 rev1 = self.get_changeset(rev1).raw_id
436 rev1 = self.get_changeset(rev1).raw_id
428 rev2 = self.get_changeset(rev2).raw_id
437 rev2 = self.get_changeset(rev2).raw_id
429 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
438 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
430
439
431 if path:
440 if path:
432 cmd += ' -- "%s"' % path
441 cmd += ' -- "%s"' % path
433 stdout, stderr = self.run_git_command(cmd)
442 stdout, stderr = self.run_git_command(cmd)
434 # If we used 'show' command, strip first few lines (until actual diff
443 # If we used 'show' command, strip first few lines (until actual diff
435 # starts)
444 # starts)
436 if rev1 == self.EMPTY_CHANGESET:
445 if rev1 == self.EMPTY_CHANGESET:
437 lines = stdout.splitlines()
446 lines = stdout.splitlines()
438 x = 0
447 x = 0
439 for line in lines:
448 for line in lines:
440 if line.startswith('diff'):
449 if line.startswith('diff'):
441 break
450 break
442 x += 1
451 x += 1
443 # Append new line just like 'diff' command do
452 # Append new line just like 'diff' command do
444 stdout = '\n'.join(lines[x:]) + '\n'
453 stdout = '\n'.join(lines[x:]) + '\n'
445 return stdout
454 return stdout
446
455
447 @LazyProperty
456 @LazyProperty
448 def in_memory_changeset(self):
457 def in_memory_changeset(self):
449 """
458 """
450 Returns ``GitInMemoryChangeset`` object for this repository.
459 Returns ``GitInMemoryChangeset`` object for this repository.
451 """
460 """
452 return GitInMemoryChangeset(self)
461 return GitInMemoryChangeset(self)
453
462
454 def clone(self, url, update_after_clone=True, bare=False):
463 def clone(self, url, update_after_clone=True, bare=False):
455 """
464 """
456 Tries to clone changes from external location.
465 Tries to clone changes from external location.
457
466
458 :param update_after_clone: If set to ``False``, git won't checkout
467 :param update_after_clone: If set to ``False``, git won't checkout
459 working directory
468 working directory
460 :param bare: If set to ``True``, repository would be cloned into
469 :param bare: If set to ``True``, repository would be cloned into
461 *bare* git repository (no working directory at all).
470 *bare* git repository (no working directory at all).
462 """
471 """
463 url = self._get_url(url)
472 url = self._get_url(url)
464 cmd = ['clone']
473 cmd = ['clone']
465 if bare:
474 if bare:
466 cmd.append('--bare')
475 cmd.append('--bare')
467 elif not update_after_clone:
476 elif not update_after_clone:
468 cmd.append('--no-checkout')
477 cmd.append('--no-checkout')
469 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
478 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
470 cmd = ' '.join(cmd)
479 cmd = ' '.join(cmd)
471 # If error occurs run_git_command raises RepositoryError already
480 # If error occurs run_git_command raises RepositoryError already
472 self.run_git_command(cmd)
481 self.run_git_command(cmd)
473
482
483 def pull(self, url):
484 """
485 Tries to pull changes from external location.
486 """
487 url = self._get_url(url)
488 cmd = ['pull']
489 cmd.append("--ff-only")
490 cmd = ' '.join(cmd)
491 # If error occurs run_git_command raises RepositoryError already
492 self.run_git_command(cmd)
493
474 @LazyProperty
494 @LazyProperty
475 def workdir(self):
495 def workdir(self):
476 """
496 """
477 Returns ``Workdir`` instance for this repository.
497 Returns ``Workdir`` instance for this repository.
478 """
498 """
479 return GitWorkdir(self)
499 return GitWorkdir(self)
480
500
481 def get_config_value(self, section, name, config_file=None):
501 def get_config_value(self, section, name, config_file=None):
482 """
502 """
483 Returns configuration value for a given [``section``] and ``name``.
503 Returns configuration value for a given [``section``] and ``name``.
484
504
485 :param section: Section we want to retrieve value from
505 :param section: Section we want to retrieve value from
486 :param name: Name of configuration we want to retrieve
506 :param name: Name of configuration we want to retrieve
487 :param config_file: A path to file which should be used to retrieve
507 :param config_file: A path to file which should be used to retrieve
488 configuration from (might also be a list of file paths)
508 configuration from (might also be a list of file paths)
489 """
509 """
490 if config_file is None:
510 if config_file is None:
491 config_file = []
511 config_file = []
492 elif isinstance(config_file, basestring):
512 elif isinstance(config_file, basestring):
493 config_file = [config_file]
513 config_file = [config_file]
494
514
495 def gen_configs():
515 def gen_configs():
496 for path in config_file + self._config_files:
516 for path in config_file + self._config_files:
497 try:
517 try:
498 yield ConfigFile.from_path(path)
518 yield ConfigFile.from_path(path)
499 except (IOError, OSError, ValueError):
519 except (IOError, OSError, ValueError):
500 continue
520 continue
501
521
502 for config in gen_configs():
522 for config in gen_configs():
503 try:
523 try:
504 return config.get(section, name)
524 return config.get(section, name)
505 except KeyError:
525 except KeyError:
506 continue
526 continue
507 return None
527 return None
508
528
509 def get_user_name(self, config_file=None):
529 def get_user_name(self, config_file=None):
510 """
530 """
511 Returns user's name from global configuration file.
531 Returns user's name from global configuration file.
512
532
513 :param config_file: A path to file which should be used to retrieve
533 :param config_file: A path to file which should be used to retrieve
514 configuration from (might also be a list of file paths)
534 configuration from (might also be a list of file paths)
515 """
535 """
516 return self.get_config_value('user', 'name', config_file)
536 return self.get_config_value('user', 'name', config_file)
517
537
518 def get_user_email(self, config_file=None):
538 def get_user_email(self, config_file=None):
519 """
539 """
520 Returns user's email from global configuration file.
540 Returns user's email from global configuration file.
521
541
522 :param config_file: A path to file which should be used to retrieve
542 :param config_file: A path to file which should be used to retrieve
523 configuration from (might also be a list of file paths)
543 configuration from (might also be a list of file paths)
524 """
544 """
525 return self.get_config_value('user', 'email', config_file)
545 return self.get_config_value('user', 'email', config_file)
General Comments 0
You need to be logged in to leave comments. Login now