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