##// END OF EJS Templates
--version command should be safe, and bare no modifications...
marcink -
r3397:64c19449 beta
parent child Browse files
Show More
@@ -1,409 +1,415 b''
1 1 '''
2 2 Module provides a class allowing to wrap communication over subprocess.Popen
3 3 input, output, error streams into a meaningfull, non-blocking, concurrent
4 4 stream processor exposing the output data as an iterator fitting to be a
5 5 return value passed by a WSGI applicaiton to a WSGI server per PEP 3333.
6 6
7 7 Copyright (c) 2011 Daniel Dotsenko <dotsa@hotmail.com>
8 8
9 9 This file is part of git_http_backend.py Project.
10 10
11 11 git_http_backend.py Project is free software: you can redistribute it and/or
12 12 modify it under the terms of the GNU Lesser General Public License as
13 13 published by the Free Software Foundation, either version 2.1 of the License,
14 14 or (at your option) any later version.
15 15
16 16 git_http_backend.py Project is distributed in the hope that it will be useful,
17 17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 19 GNU Lesser General Public License for more details.
20 20
21 21 You should have received a copy of the GNU Lesser General Public License
22 22 along with git_http_backend.py Project.
23 23 If not, see <http://www.gnu.org/licenses/>.
24 24 '''
25 25 import os
26 26 import subprocess
27 27 from rhodecode.lib.compat import deque, Event, Thread, _bytes, _bytearray
28 28
29 29
30 30 class StreamFeeder(Thread):
31 31 """
32 32 Normal writing into pipe-like is blocking once the buffer is filled.
33 33 This thread allows a thread to seep data from a file-like into a pipe
34 34 without blocking the main thread.
35 35 We close inpipe once the end of the source stream is reached.
36 36 """
37 37 def __init__(self, source):
38 38 super(StreamFeeder, self).__init__()
39 39 self.daemon = True
40 40 filelike = False
41 41 self.bytes = _bytes()
42 42 if type(source) in (type(''), _bytes, _bytearray): # string-like
43 43 self.bytes = _bytes(source)
44 44 else: # can be either file pointer or file-like
45 45 if type(source) in (int, long): # file pointer it is
46 46 ## converting file descriptor (int) stdin into file-like
47 47 try:
48 48 source = os.fdopen(source, 'rb', 16384)
49 49 except Exception:
50 50 pass
51 51 # let's see if source is file-like by now
52 52 try:
53 53 filelike = source.read
54 54 except Exception:
55 55 pass
56 56 if not filelike and not self.bytes:
57 57 raise TypeError("StreamFeeder's source object must be a readable "
58 58 "file-like, a file descriptor, or a string-like.")
59 59 self.source = source
60 60 self.readiface, self.writeiface = os.pipe()
61 61
62 62 def run(self):
63 63 t = self.writeiface
64 64 if self.bytes:
65 65 os.write(t, self.bytes)
66 66 else:
67 67 s = self.source
68 68 b = s.read(4096)
69 69 while b:
70 70 os.write(t, b)
71 71 b = s.read(4096)
72 72 os.close(t)
73 73
74 74 @property
75 75 def output(self):
76 76 return self.readiface
77 77
78 78
79 79 class InputStreamChunker(Thread):
80 80 def __init__(self, source, target, buffer_size, chunk_size):
81 81
82 82 super(InputStreamChunker, self).__init__()
83 83
84 84 self.daemon = True # die die die.
85 85
86 86 self.source = source
87 87 self.target = target
88 88 self.chunk_count_max = int(buffer_size / chunk_size) + 1
89 89 self.chunk_size = chunk_size
90 90
91 91 self.data_added = Event()
92 92 self.data_added.clear()
93 93
94 94 self.keep_reading = Event()
95 95 self.keep_reading.set()
96 96
97 97 self.EOF = Event()
98 98 self.EOF.clear()
99 99
100 100 self.go = Event()
101 101 self.go.set()
102 102
103 103 def stop(self):
104 104 self.go.clear()
105 105 self.EOF.set()
106 106 try:
107 107 # this is not proper, but is done to force the reader thread let
108 108 # go of the input because, if successful, .close() will send EOF
109 109 # down the pipe.
110 110 self.source.close()
111 111 except:
112 112 pass
113 113
114 114 def run(self):
115 115 s = self.source
116 116 t = self.target
117 117 cs = self.chunk_size
118 118 ccm = self.chunk_count_max
119 119 kr = self.keep_reading
120 120 da = self.data_added
121 121 go = self.go
122
123 try:
122 124 b = s.read(cs)
125 except ValueError:
126 b = ''
123 127
124 128 while b and go.is_set():
125 129 if len(t) > ccm:
126 130 kr.clear()
127 131 kr.wait(2)
128 132 # # this only works on 2.7.x and up
129 133 # if not kr.wait(10):
130 134 # raise Exception("Timed out while waiting for input to be read.")
131 135 # instead we'll use this
132 136 if len(t) > ccm + 3:
133 137 raise IOError("Timed out while waiting for input from subprocess.")
134 138 t.append(b)
135 139 da.set()
136 140 b = s.read(cs)
137 141 self.EOF.set()
138 142 da.set() # for cases when done but there was no input.
139 143
140 144
141 145 class BufferedGenerator():
142 146 '''
143 147 Class behaves as a non-blocking, buffered pipe reader.
144 148 Reads chunks of data (through a thread)
145 149 from a blocking pipe, and attaches these to an array (Deque) of chunks.
146 150 Reading is halted in the thread when max chunks is internally buffered.
147 151 The .next() may operate in blocking or non-blocking fashion by yielding
148 152 '' if no data is ready
149 153 to be sent or by not returning until there is some data to send
150 154 When we get EOF from underlying source pipe we raise the marker to raise
151 155 StopIteration after the last chunk of data is yielded.
152 156 '''
153 157
154 158 def __init__(self, source, buffer_size=65536, chunk_size=4096,
155 159 starting_values=[], bottomless=False):
156 160
157 161 if bottomless:
158 162 maxlen = int(buffer_size / chunk_size)
159 163 else:
160 164 maxlen = None
161 165
162 166 self.data = deque(starting_values, maxlen)
163 167
164 168 self.worker = InputStreamChunker(source, self.data, buffer_size,
165 169 chunk_size)
166 170 if starting_values:
167 171 self.worker.data_added.set()
168 172 self.worker.start()
169 173
170 174 ####################
171 175 # Generator's methods
172 176 ####################
173 177
174 178 def __iter__(self):
175 179 return self
176 180
177 181 def next(self):
178 182 while not len(self.data) and not self.worker.EOF.is_set():
179 183 self.worker.data_added.clear()
180 184 self.worker.data_added.wait(0.2)
181 185 if len(self.data):
182 186 self.worker.keep_reading.set()
183 187 return _bytes(self.data.popleft())
184 188 elif self.worker.EOF.is_set():
185 189 raise StopIteration
186 190
187 191 def throw(self, type, value=None, traceback=None):
188 192 if not self.worker.EOF.is_set():
189 193 raise type(value)
190 194
191 195 def start(self):
192 196 self.worker.start()
193 197
194 198 def stop(self):
195 199 self.worker.stop()
196 200
197 201 def close(self):
198 202 try:
199 203 self.worker.stop()
200 204 self.throw(GeneratorExit)
201 205 except (GeneratorExit, StopIteration):
202 206 pass
203 207
204 208 def __del__(self):
205 209 self.close()
206 210
207 211 ####################
208 212 # Threaded reader's infrastructure.
209 213 ####################
210 214 @property
211 215 def input(self):
212 216 return self.worker.w
213 217
214 218 @property
215 219 def data_added_event(self):
216 220 return self.worker.data_added
217 221
218 222 @property
219 223 def data_added(self):
220 224 return self.worker.data_added.is_set()
221 225
222 226 @property
223 227 def reading_paused(self):
224 228 return not self.worker.keep_reading.is_set()
225 229
226 230 @property
227 231 def done_reading_event(self):
228 232 '''
229 233 Done_reding does not mean that the iterator's buffer is empty.
230 234 Iterator might have done reading from underlying source, but the read
231 235 chunks might still be available for serving through .next() method.
232 236
233 237 @return An Event class instance.
234 238 '''
235 239 return self.worker.EOF
236 240
237 241 @property
238 242 def done_reading(self):
239 243 '''
240 244 Done_reding does not mean that the iterator's buffer is empty.
241 245 Iterator might have done reading from underlying source, but the read
242 246 chunks might still be available for serving through .next() method.
243 247
244 248 @return An Bool value.
245 249 '''
246 250 return self.worker.EOF.is_set()
247 251
248 252 @property
249 253 def length(self):
250 254 '''
251 255 returns int.
252 256
253 257 This is the lenght of the que of chunks, not the length of
254 258 the combined contents in those chunks.
255 259
256 260 __len__() cannot be meaningfully implemented because this
257 261 reader is just flying throuh a bottomless pit content and
258 262 can only know the lenght of what it already saw.
259 263
260 264 If __len__() on WSGI server per PEP 3333 returns a value,
261 265 the responce's length will be set to that. In order not to
262 266 confuse WSGI PEP3333 servers, we will not implement __len__
263 267 at all.
264 268 '''
265 269 return len(self.data)
266 270
267 271 def prepend(self, x):
268 272 self.data.appendleft(x)
269 273
270 274 def append(self, x):
271 275 self.data.append(x)
272 276
273 277 def extend(self, o):
274 278 self.data.extend(o)
275 279
276 280 def __getitem__(self, i):
277 281 return self.data[i]
278 282
279 283
280 284 class SubprocessIOChunker(object):
281 285 '''
282 286 Processor class wrapping handling of subprocess IO.
283 287
284 288 In a way, this is a "communicate()" replacement with a twist.
285 289
286 290 - We are multithreaded. Writing in and reading out, err are all sep threads.
287 291 - We support concurrent (in and out) stream processing.
288 292 - The output is not a stream. It's a queue of read string (bytes, not unicode)
289 293 chunks. The object behaves as an iterable. You can "for chunk in obj:" us.
290 294 - We are non-blocking in more respects than communicate()
291 295 (reading from subprocess out pauses when internal buffer is full, but
292 296 does not block the parent calling code. On the flip side, reading from
293 297 slow-yielding subprocess may block the iteration until data shows up. This
294 298 does not block the parallel inpipe reading occurring parallel thread.)
295 299
296 300 The purpose of the object is to allow us to wrap subprocess interactions into
297 301 and interable that can be passed to a WSGI server as the application's return
298 302 value. Because of stream-processing-ability, WSGI does not have to read ALL
299 303 of the subprocess's output and buffer it, before handing it to WSGI server for
300 304 HTTP response. Instead, the class initializer reads just a bit of the stream
301 305 to figure out if error ocurred or likely to occur and if not, just hands the
302 306 further iteration over subprocess output to the server for completion of HTTP
303 307 response.
304 308
305 309 The real or perceived subprocess error is trapped and raised as one of
306 310 EnvironmentError family of exceptions
307 311
308 312 Example usage:
309 313 # try:
310 314 # answer = SubprocessIOChunker(
311 315 # cmd,
312 316 # input,
313 317 # buffer_size = 65536,
314 318 # chunk_size = 4096
315 319 # )
316 320 # except (EnvironmentError) as e:
317 321 # print str(e)
318 322 # raise e
319 323 #
320 324 # return answer
321 325
322 326
323 327 '''
324 328 def __init__(self, cmd, inputstream=None, buffer_size=65536,
325 329 chunk_size=4096, starting_values=[], **kwargs):
326 330 '''
327 331 Initializes SubprocessIOChunker
328 332
329 333 :param cmd: A Subprocess.Popen style "cmd". Can be string or array of strings
330 334 :param inputstream: (Default: None) A file-like, string, or file pointer.
331 335 :param buffer_size: (Default: 65536) A size of total buffer per stream in bytes.
332 336 :param chunk_size: (Default: 4096) A max size of a chunk. Actual chunk may be smaller.
333 337 :param starting_values: (Default: []) An array of strings to put in front of output que.
334 338 '''
335 339
336 340 if inputstream:
337 341 input_streamer = StreamFeeder(inputstream)
338 342 input_streamer.start()
339 343 inputstream = input_streamer.output
340 344
341 345 if isinstance(cmd, (list, tuple)):
342 346 cmd = ' '.join(cmd)
343 347
344 348 _shell = kwargs.get('shell') or True
345 349 kwargs['shell'] = _shell
346 350 _p = subprocess.Popen(cmd,
347 351 bufsize=-1,
348 352 stdin=inputstream,
349 353 stdout=subprocess.PIPE,
350 354 stderr=subprocess.PIPE,
351 355 **kwargs
352 356 )
353 357
354 358 bg_out = BufferedGenerator(_p.stdout, buffer_size, chunk_size, starting_values)
355 359 bg_err = BufferedGenerator(_p.stderr, 16000, 1, bottomless=True)
356 360
357 361 while not bg_out.done_reading and not bg_out.reading_paused and not bg_err.length:
358 362 # doing this until we reach either end of file, or end of buffer.
359 363 bg_out.data_added_event.wait(1)
360 364 bg_out.data_added_event.clear()
361 365
362 366 # at this point it's still ambiguous if we are done reading or just full buffer.
363 367 # Either way, if error (returned by ended process, or implied based on
364 368 # presence of stuff in stderr output) we error out.
365 369 # Else, we are happy.
366 370 _returncode = _p.poll()
367 371 if _returncode or (_returncode == None and bg_err.length):
368 372 try:
369 373 _p.terminate()
370 374 except:
371 375 pass
372 376 bg_out.stop()
373 377 bg_err.stop()
374 378 err = '%s' % ''.join(bg_err)
379 if err:
375 380 raise EnvironmentError("Subprocess exited due to an error:\n" + err)
381 raise EnvironmentError("Subprocess exited with non 0 ret code:%s" % _returncode)
376 382
377 383 self.process = _p
378 384 self.output = bg_out
379 385 self.error = bg_err
380 386
381 387 def __iter__(self):
382 388 return self
383 389
384 390 def next(self):
385 391 if self.process.poll():
386 392 err = '%s' % ''.join(self.error)
387 393 raise EnvironmentError("Subprocess exited due to an error:\n" + err)
388 394 return self.output.next()
389 395
390 396 def throw(self, type, value=None, traceback=None):
391 397 if self.output.length or not self.output.done_reading:
392 398 raise type(value)
393 399
394 400 def close(self):
395 401 try:
396 402 self.process.terminate()
397 403 except:
398 404 pass
399 405 try:
400 406 self.output.close()
401 407 except:
402 408 pass
403 409 try:
404 410 self.error.close()
405 411 except:
406 412 pass
407 413
408 414 def __del__(self):
409 415 self.close()
@@ -1,800 +1,801 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.utils
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Utilities library for RhodeCode
7 7
8 8 :created_on: Apr 18, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import re
28 28 import logging
29 29 import datetime
30 30 import traceback
31 31 import paste
32 32 import beaker
33 33 import tarfile
34 34 import shutil
35 35 import decorator
36 36 import warnings
37 37 from os.path import abspath
38 38 from os.path import dirname as dn, join as jn
39 39
40 40 from paste.script.command import Command, BadCommand
41 41
42 42 from mercurial import ui, config
43 43
44 44 from webhelpers.text import collapse, remove_formatting, strip_tags
45 45
46 46 from rhodecode.lib.vcs import get_backend
47 47 from rhodecode.lib.vcs.backends.base import BaseChangeset
48 48 from rhodecode.lib.vcs.utils.lazy import LazyProperty
49 49 from rhodecode.lib.vcs.utils.helpers import get_scm
50 50 from rhodecode.lib.vcs.exceptions import VCSError
51 51
52 52 from rhodecode.lib.caching_query import FromCache
53 53
54 54 from rhodecode.model import meta
55 55 from rhodecode.model.db import Repository, User, RhodeCodeUi, \
56 56 UserLog, RepoGroup, RhodeCodeSetting, CacheInvalidation
57 57 from rhodecode.model.meta import Session
58 58 from rhodecode.model.repos_group import ReposGroupModel
59 59 from rhodecode.lib.utils2 import safe_str, safe_unicode
60 60 from rhodecode.lib.vcs.utils.fakemod import create_module
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
65 65
66 66
67 67 def recursive_replace(str_, replace=' '):
68 68 """
69 69 Recursive replace of given sign to just one instance
70 70
71 71 :param str_: given string
72 72 :param replace: char to find and replace multiple instances
73 73
74 74 Examples::
75 75 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
76 76 'Mighty-Mighty-Bo-sstones'
77 77 """
78 78
79 79 if str_.find(replace * 2) == -1:
80 80 return str_
81 81 else:
82 82 str_ = str_.replace(replace * 2, replace)
83 83 return recursive_replace(str_, replace)
84 84
85 85
86 86 def repo_name_slug(value):
87 87 """
88 88 Return slug of name of repository
89 89 This function is called on each creation/modification
90 90 of repository to prevent bad names in repo
91 91 """
92 92
93 93 slug = remove_formatting(value)
94 94 slug = strip_tags(slug)
95 95
96 96 for c in """`?=[]\;'"<>,/~!@#$%^&*()+{}|: """:
97 97 slug = slug.replace(c, '-')
98 98 slug = recursive_replace(slug, '-')
99 99 slug = collapse(slug, '-')
100 100 return slug
101 101
102 102
103 103 def get_repo_slug(request):
104 104 _repo = request.environ['pylons.routes_dict'].get('repo_name')
105 105 if _repo:
106 106 _repo = _repo.rstrip('/')
107 107 return _repo
108 108
109 109
110 110 def get_repos_group_slug(request):
111 111 _group = request.environ['pylons.routes_dict'].get('group_name')
112 112 if _group:
113 113 _group = _group.rstrip('/')
114 114 return _group
115 115
116 116
117 117 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
118 118 """
119 119 Action logger for various actions made by users
120 120
121 121 :param user: user that made this action, can be a unique username string or
122 122 object containing user_id attribute
123 123 :param action: action to log, should be on of predefined unique actions for
124 124 easy translations
125 125 :param repo: string name of repository or object containing repo_id,
126 126 that action was made on
127 127 :param ipaddr: optional ip address from what the action was made
128 128 :param sa: optional sqlalchemy session
129 129
130 130 """
131 131
132 132 if not sa:
133 133 sa = meta.Session()
134 134
135 135 try:
136 136 if hasattr(user, 'user_id'):
137 137 user_obj = User.get(user.user_id)
138 138 elif isinstance(user, basestring):
139 139 user_obj = User.get_by_username(user)
140 140 else:
141 141 raise Exception('You have to provide a user object or a username')
142 142
143 143 if hasattr(repo, 'repo_id'):
144 144 repo_obj = Repository.get(repo.repo_id)
145 145 repo_name = repo_obj.repo_name
146 146 elif isinstance(repo, basestring):
147 147 repo_name = repo.lstrip('/')
148 148 repo_obj = Repository.get_by_repo_name(repo_name)
149 149 else:
150 150 repo_obj = None
151 151 repo_name = ''
152 152
153 153 user_log = UserLog()
154 154 user_log.user_id = user_obj.user_id
155 155 user_log.username = user_obj.username
156 156 user_log.action = safe_unicode(action)
157 157
158 158 user_log.repository = repo_obj
159 159 user_log.repository_name = repo_name
160 160
161 161 user_log.action_date = datetime.datetime.now()
162 162 user_log.user_ip = ipaddr
163 163 sa.add(user_log)
164 164
165 165 log.info('Logging action %s on %s by %s' %
166 166 (action, safe_unicode(repo), user_obj))
167 167 if commit:
168 168 sa.commit()
169 169 except:
170 170 log.error(traceback.format_exc())
171 171 raise
172 172
173 173
174 174 def get_repos(path, recursive=False, skip_removed_repos=True):
175 175 """
176 176 Scans given path for repos and return (name,(type,path)) tuple
177 177
178 178 :param path: path to scan for repositories
179 179 :param recursive: recursive search and return names with subdirs in front
180 180 """
181 181
182 182 # remove ending slash for better results
183 183 path = path.rstrip(os.sep)
184 184 log.debug('now scanning in %s location recursive:%s...' % (path, recursive))
185 185
186 186 def _get_repos(p):
187 187 if not os.access(p, os.W_OK):
188 188 return
189 189 for dirpath in os.listdir(p):
190 190 if os.path.isfile(os.path.join(p, dirpath)):
191 191 continue
192 192 cur_path = os.path.join(p, dirpath)
193 193
194 194 # skip removed repos
195 195 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
196 196 continue
197 197
198 198 #skip .<somethin> dirs
199 199 if dirpath.startswith('.'):
200 200 continue
201 201
202 202 try:
203 203 scm_info = get_scm(cur_path)
204 204 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
205 205 except VCSError:
206 206 if not recursive:
207 207 continue
208 208 #check if this dir containts other repos for recursive scan
209 209 rec_path = os.path.join(p, dirpath)
210 210 if os.path.isdir(rec_path):
211 211 for inner_scm in _get_repos(rec_path):
212 212 yield inner_scm
213 213
214 214 return _get_repos(path)
215 215
216 216 #alias for backward compat
217 217 get_filesystem_repos = get_repos
218 218
219 219
220 220 def is_valid_repo(repo_name, base_path, scm=None):
221 221 """
222 222 Returns True if given path is a valid repository False otherwise.
223 223 If scm param is given also compare if given scm is the same as expected
224 224 from scm parameter
225 225
226 226 :param repo_name:
227 227 :param base_path:
228 228 :param scm:
229 229
230 230 :return True: if given path is a valid repository
231 231 """
232 232 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
233 233
234 234 try:
235 235 scm_ = get_scm(full_path)
236 236 if scm:
237 237 return scm_[0] == scm
238 238 return True
239 239 except VCSError:
240 240 return False
241 241
242 242
243 243 def is_valid_repos_group(repos_group_name, base_path):
244 244 """
245 245 Returns True if given path is a repos group False otherwise
246 246
247 247 :param repo_name:
248 248 :param base_path:
249 249 """
250 250 full_path = os.path.join(safe_str(base_path), safe_str(repos_group_name))
251 251
252 252 # check if it's not a repo
253 253 if is_valid_repo(repos_group_name, base_path):
254 254 return False
255 255
256 256 try:
257 257 # we need to check bare git repos at higher level
258 258 # since we might match branches/hooks/info/objects or possible
259 259 # other things inside bare git repo
260 260 get_scm(os.path.dirname(full_path))
261 261 return False
262 262 except VCSError:
263 263 pass
264 264
265 265 # check if it's a valid path
266 266 if os.path.isdir(full_path):
267 267 return True
268 268
269 269 return False
270 270
271 271
272 272 def ask_ok(prompt, retries=4, complaint='Yes or no please!'):
273 273 while True:
274 274 ok = raw_input(prompt)
275 275 if ok in ('y', 'ye', 'yes'):
276 276 return True
277 277 if ok in ('n', 'no', 'nop', 'nope'):
278 278 return False
279 279 retries = retries - 1
280 280 if retries < 0:
281 281 raise IOError
282 282 print complaint
283 283
284 284 #propagated from mercurial documentation
285 285 ui_sections = ['alias', 'auth',
286 286 'decode/encode', 'defaults',
287 287 'diff', 'email',
288 288 'extensions', 'format',
289 289 'merge-patterns', 'merge-tools',
290 290 'hooks', 'http_proxy',
291 291 'smtp', 'patch',
292 292 'paths', 'profiling',
293 293 'server', 'trusted',
294 294 'ui', 'web', ]
295 295
296 296
297 297 def make_ui(read_from='file', path=None, checkpaths=True, clear_session=True):
298 298 """
299 299 A function that will read python rc files or database
300 300 and make an mercurial ui object from read options
301 301
302 302 :param path: path to mercurial config file
303 303 :param checkpaths: check the path
304 304 :param read_from: read from 'file' or 'db'
305 305 """
306 306
307 307 baseui = ui.ui()
308 308
309 309 # clean the baseui object
310 310 baseui._ocfg = config.config()
311 311 baseui._ucfg = config.config()
312 312 baseui._tcfg = config.config()
313 313
314 314 if read_from == 'file':
315 315 if not os.path.isfile(path):
316 316 log.debug('hgrc file is not present at %s, skipping...' % path)
317 317 return False
318 318 log.debug('reading hgrc from %s' % path)
319 319 cfg = config.config()
320 320 cfg.read(path)
321 321 for section in ui_sections:
322 322 for k, v in cfg.items(section):
323 323 log.debug('settings ui from file: [%s] %s=%s' % (section, k, v))
324 324 baseui.setconfig(safe_str(section), safe_str(k), safe_str(v))
325 325
326 326 elif read_from == 'db':
327 327 sa = meta.Session()
328 328 ret = sa.query(RhodeCodeUi)\
329 329 .options(FromCache("sql_cache_short", "get_hg_ui_settings"))\
330 330 .all()
331 331
332 332 hg_ui = ret
333 333 for ui_ in hg_ui:
334 334 if ui_.ui_active:
335 335 log.debug('settings ui from db: [%s] %s=%s', ui_.ui_section,
336 336 ui_.ui_key, ui_.ui_value)
337 337 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
338 338 safe_str(ui_.ui_value))
339 339 if ui_.ui_key == 'push_ssl':
340 340 # force set push_ssl requirement to False, rhodecode
341 341 # handles that
342 342 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
343 343 False)
344 344 if clear_session:
345 345 meta.Session.remove()
346 346 return baseui
347 347
348 348
349 349 def set_rhodecode_config(config):
350 350 """
351 351 Updates pylons config with new settings from database
352 352
353 353 :param config:
354 354 """
355 355 hgsettings = RhodeCodeSetting.get_app_settings()
356 356
357 357 for k, v in hgsettings.items():
358 358 config[k] = v
359 359
360 360
361 361 def invalidate_cache(cache_key, *args):
362 362 """
363 363 Puts cache invalidation task into db for
364 364 further global cache invalidation
365 365 """
366 366
367 367 from rhodecode.model.scm import ScmModel
368 368
369 369 if cache_key.startswith('get_repo_cached_'):
370 370 name = cache_key.split('get_repo_cached_')[-1]
371 371 ScmModel().mark_for_invalidation(name)
372 372
373 373
374 374 def map_groups(path):
375 375 """
376 376 Given a full path to a repository, create all nested groups that this
377 377 repo is inside. This function creates parent-child relationships between
378 378 groups and creates default perms for all new groups.
379 379
380 380 :param paths: full path to repository
381 381 """
382 382 sa = meta.Session()
383 383 groups = path.split(Repository.url_sep())
384 384 parent = None
385 385 group = None
386 386
387 387 # last element is repo in nested groups structure
388 388 groups = groups[:-1]
389 389 rgm = ReposGroupModel(sa)
390 390 for lvl, group_name in enumerate(groups):
391 391 group_name = '/'.join(groups[:lvl] + [group_name])
392 392 group = RepoGroup.get_by_group_name(group_name)
393 393 desc = '%s group' % group_name
394 394
395 395 # skip folders that are now removed repos
396 396 if REMOVED_REPO_PAT.match(group_name):
397 397 break
398 398
399 399 if group is None:
400 400 log.debug('creating group level: %s group_name: %s' % (lvl,
401 401 group_name))
402 402 group = RepoGroup(group_name, parent)
403 403 group.group_description = desc
404 404 sa.add(group)
405 405 rgm._create_default_perms(group)
406 406 sa.flush()
407 407 parent = group
408 408 return group
409 409
410 410
411 411 def repo2db_mapper(initial_repo_list, remove_obsolete=False,
412 412 install_git_hook=False):
413 413 """
414 414 maps all repos given in initial_repo_list, non existing repositories
415 415 are created, if remove_obsolete is True it also check for db entries
416 416 that are not in initial_repo_list and removes them.
417 417
418 418 :param initial_repo_list: list of repositories found by scanning methods
419 419 :param remove_obsolete: check for obsolete entries in database
420 420 :param install_git_hook: if this is True, also check and install githook
421 421 for a repo if missing
422 422 """
423 423 from rhodecode.model.repo import RepoModel
424 424 from rhodecode.model.scm import ScmModel
425 425 sa = meta.Session()
426 426 rm = RepoModel()
427 427 user = sa.query(User).filter(User.admin == True).first()
428 428 if user is None:
429 429 raise Exception('Missing administrative account!')
430 430 added = []
431 431
432 432 # # clear cache keys
433 433 # log.debug("Clearing cache keys now...")
434 434 # CacheInvalidation.clear_cache()
435 435 # sa.commit()
436 436
437 437 ##creation defaults
438 438 defs = RhodeCodeSetting.get_default_repo_settings(strip_prefix=True)
439 439 enable_statistics = defs.get('repo_enable_statistics')
440 440 enable_locking = defs.get('repo_enable_locking')
441 441 enable_downloads = defs.get('repo_enable_downloads')
442 442 private = defs.get('repo_private')
443 443
444 444 for name, repo in initial_repo_list.items():
445 445 group = map_groups(name)
446 446 db_repo = rm.get_by_repo_name(name)
447 447 # found repo that is on filesystem not in RhodeCode database
448 448 if not db_repo:
449 449 log.info('repository %s not found, creating now' % name)
450 450 added.append(name)
451 451 desc = (repo.description
452 452 if repo.description != 'unknown'
453 453 else '%s repository' % name)
454 454
455 455 new_repo = rm.create_repo(
456 456 repo_name=name,
457 457 repo_type=repo.alias,
458 458 description=desc,
459 459 repos_group=getattr(group, 'group_id', None),
460 460 owner=user,
461 461 just_db=True,
462 462 enable_locking=enable_locking,
463 463 enable_downloads=enable_downloads,
464 464 enable_statistics=enable_statistics,
465 465 private=private
466 466 )
467 467 # we added that repo just now, and make sure it has githook
468 468 # installed
469 469 if new_repo.repo_type == 'git':
470 470 ScmModel().install_git_hook(new_repo.scm_instance)
471 471 new_repo.update_changeset_cache()
472 472 elif install_git_hook:
473 473 if db_repo.repo_type == 'git':
474 474 ScmModel().install_git_hook(db_repo.scm_instance)
475 475 # during starting install all cache keys for all repositories in the
476 476 # system, this will register all repos and multiple instances
477 477 key, _prefix, _org_key = CacheInvalidation._get_key(name)
478 478 CacheInvalidation.invalidate(name)
479 479 log.debug("Creating a cache key for %s, instance_id %s"
480 480 % (name, _prefix or 'unknown'))
481 481
482 482 sa.commit()
483 483 removed = []
484 484 if remove_obsolete:
485 485 # remove from database those repositories that are not in the filesystem
486 486 for repo in sa.query(Repository).all():
487 487 if repo.repo_name not in initial_repo_list.keys():
488 488 log.debug("Removing non-existing repository found in db `%s`" %
489 489 repo.repo_name)
490 490 try:
491 491 sa.delete(repo)
492 492 sa.commit()
493 493 removed.append(repo.repo_name)
494 494 except:
495 495 #don't hold further removals on error
496 496 log.error(traceback.format_exc())
497 497 sa.rollback()
498 498
499 499 return added, removed
500 500
501 501
502 502 # set cache regions for beaker so celery can utilise it
503 503 def add_cache(settings):
504 504 cache_settings = {'regions': None}
505 505 for key in settings.keys():
506 506 for prefix in ['beaker.cache.', 'cache.']:
507 507 if key.startswith(prefix):
508 508 name = key.split(prefix)[1].strip()
509 509 cache_settings[name] = settings[key].strip()
510 510 if cache_settings['regions']:
511 511 for region in cache_settings['regions'].split(','):
512 512 region = region.strip()
513 513 region_settings = {}
514 514 for key, value in cache_settings.items():
515 515 if key.startswith(region):
516 516 region_settings[key.split('.')[1]] = value
517 517 region_settings['expire'] = int(region_settings.get('expire',
518 518 60))
519 519 region_settings.setdefault('lock_dir',
520 520 cache_settings.get('lock_dir'))
521 521 region_settings.setdefault('data_dir',
522 522 cache_settings.get('data_dir'))
523 523
524 524 if 'type' not in region_settings:
525 525 region_settings['type'] = cache_settings.get('type',
526 526 'memory')
527 527 beaker.cache.cache_regions[region] = region_settings
528 528
529 529
530 530 def load_rcextensions(root_path):
531 531 import rhodecode
532 532 from rhodecode.config import conf
533 533
534 534 path = os.path.join(root_path, 'rcextensions', '__init__.py')
535 535 if os.path.isfile(path):
536 536 rcext = create_module('rc', path)
537 537 EXT = rhodecode.EXTENSIONS = rcext
538 538 log.debug('Found rcextensions now loading %s...' % rcext)
539 539
540 540 # Additional mappings that are not present in the pygments lexers
541 541 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
542 542
543 543 #OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
544 544
545 545 if getattr(EXT, 'INDEX_EXTENSIONS', []) != []:
546 546 log.debug('settings custom INDEX_EXTENSIONS')
547 547 conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', [])
548 548
549 549 #ADDITIONAL MAPPINGS
550 550 log.debug('adding extra into INDEX_EXTENSIONS')
551 551 conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', []))
552 552
553 553 # auto check if the module is not missing any data, set to default if is
554 554 # this will help autoupdate new feature of rcext module
555 555 from rhodecode.config import rcextensions
556 556 for k in dir(rcextensions):
557 557 if not k.startswith('_') and not hasattr(EXT, k):
558 558 setattr(EXT, k, getattr(rcextensions, k))
559 559
560 560
561 561 def get_custom_lexer(extension):
562 562 """
563 563 returns a custom lexer if it's defined in rcextensions module, or None
564 564 if there's no custom lexer defined
565 565 """
566 566 import rhodecode
567 567 from pygments import lexers
568 568 #check if we didn't define this extension as other lexer
569 569 if rhodecode.EXTENSIONS and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
570 570 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
571 571 return lexers.get_lexer_by_name(_lexer_name)
572 572
573 573
574 574 #==============================================================================
575 575 # TEST FUNCTIONS AND CREATORS
576 576 #==============================================================================
577 577 def create_test_index(repo_location, config, full_index):
578 578 """
579 579 Makes default test index
580 580
581 581 :param config: test config
582 582 :param full_index:
583 583 """
584 584
585 585 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
586 586 from rhodecode.lib.pidlock import DaemonLock, LockHeld
587 587
588 588 repo_location = repo_location
589 589
590 590 index_location = os.path.join(config['app_conf']['index_dir'])
591 591 if not os.path.exists(index_location):
592 592 os.makedirs(index_location)
593 593
594 594 try:
595 595 l = DaemonLock(file_=jn(dn(index_location), 'make_index.lock'))
596 596 WhooshIndexingDaemon(index_location=index_location,
597 597 repo_location=repo_location)\
598 598 .run(full_index=full_index)
599 599 l.release()
600 600 except LockHeld:
601 601 pass
602 602
603 603
604 604 def create_test_env(repos_test_path, config):
605 605 """
606 606 Makes a fresh database and
607 607 install test repository into tmp dir
608 608 """
609 609 from rhodecode.lib.db_manage import DbManage
610 610 from rhodecode.tests import HG_REPO, GIT_REPO, TESTS_TMP_PATH
611 611
612 612 # PART ONE create db
613 613 dbconf = config['sqlalchemy.db1.url']
614 614 log.debug('making test db %s' % dbconf)
615 615
616 616 # create test dir if it doesn't exist
617 617 if not os.path.isdir(repos_test_path):
618 618 log.debug('Creating testdir %s' % repos_test_path)
619 619 os.makedirs(repos_test_path)
620 620
621 621 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
622 622 tests=True)
623 623 dbmanage.create_tables(override=True)
624 624 dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
625 625 dbmanage.create_default_user()
626 626 dbmanage.admin_prompt()
627 627 dbmanage.create_permissions()
628 628 dbmanage.populate_default_permissions()
629 629 Session().commit()
630 630 # PART TWO make test repo
631 631 log.debug('making test vcs repositories')
632 632
633 633 idx_path = config['app_conf']['index_dir']
634 634 data_path = config['app_conf']['cache_dir']
635 635
636 636 #clean index and data
637 637 if idx_path and os.path.exists(idx_path):
638 638 log.debug('remove %s' % idx_path)
639 639 shutil.rmtree(idx_path)
640 640
641 641 if data_path and os.path.exists(data_path):
642 642 log.debug('remove %s' % data_path)
643 643 shutil.rmtree(data_path)
644 644
645 645 #CREATE DEFAULT TEST REPOS
646 646 cur_dir = dn(dn(abspath(__file__)))
647 647 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_hg.tar.gz"))
648 648 tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
649 649 tar.close()
650 650
651 651 cur_dir = dn(dn(abspath(__file__)))
652 652 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_git.tar.gz"))
653 653 tar.extractall(jn(TESTS_TMP_PATH, GIT_REPO))
654 654 tar.close()
655 655
656 656 #LOAD VCS test stuff
657 657 from rhodecode.tests.vcs import setup_package
658 658 setup_package()
659 659
660 660
661 661 #==============================================================================
662 662 # PASTER COMMANDS
663 663 #==============================================================================
664 664 class BasePasterCommand(Command):
665 665 """
666 666 Abstract Base Class for paster commands.
667 667
668 668 The celery commands are somewhat aggressive about loading
669 669 celery.conf, and since our module sets the `CELERY_LOADER`
670 670 environment variable to our loader, we have to bootstrap a bit and
671 671 make sure we've had a chance to load the pylons config off of the
672 672 command line, otherwise everything fails.
673 673 """
674 674 min_args = 1
675 675 min_args_error = "Please provide a paster config file as an argument."
676 676 takes_config_file = 1
677 677 requires_config_file = True
678 678
679 679 def notify_msg(self, msg, log=False):
680 680 """Make a notification to user, additionally if logger is passed
681 681 it logs this action using given logger
682 682
683 683 :param msg: message that will be printed to user
684 684 :param log: logging instance, to use to additionally log this message
685 685
686 686 """
687 687 if log and isinstance(log, logging):
688 688 log(msg)
689 689
690 690 def run(self, args):
691 691 """
692 692 Overrides Command.run
693 693
694 694 Checks for a config file argument and loads it.
695 695 """
696 696 if len(args) < self.min_args:
697 697 raise BadCommand(
698 698 self.min_args_error % {'min_args': self.min_args,
699 699 'actual_args': len(args)})
700 700
701 701 # Decrement because we're going to lob off the first argument.
702 702 # @@ This is hacky
703 703 self.min_args -= 1
704 704 self.bootstrap_config(args[0])
705 705 self.update_parser()
706 706 return super(BasePasterCommand, self).run(args[1:])
707 707
708 708 def update_parser(self):
709 709 """
710 710 Abstract method. Allows for the class's parser to be updated
711 711 before the superclass's `run` method is called. Necessary to
712 712 allow options/arguments to be passed through to the underlying
713 713 celery command.
714 714 """
715 715 raise NotImplementedError("Abstract Method.")
716 716
717 717 def bootstrap_config(self, conf):
718 718 """
719 719 Loads the pylons configuration.
720 720 """
721 721 from pylons import config as pylonsconfig
722 722
723 723 self.path_to_ini_file = os.path.realpath(conf)
724 724 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
725 725 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
726 726
727 727 def _init_session(self):
728 728 """
729 729 Inits SqlAlchemy Session
730 730 """
731 731 logging.config.fileConfig(self.path_to_ini_file)
732 732 from pylons import config
733 733 from rhodecode.model import init_model
734 734 from rhodecode.lib.utils2 import engine_from_config
735 735
736 736 #get to remove repos !!
737 737 add_cache(config)
738 738 engine = engine_from_config(config, 'sqlalchemy.db1.')
739 739 init_model(engine)
740 740
741 741
742 742 def check_git_version():
743 743 """
744 744 Checks what version of git is installed in system, and issues a warning
745 745 if it's too old for RhodeCode to properly work.
746 746 """
747 747 from rhodecode import BACKENDS
748 748 from rhodecode.lib.vcs.backends.git.repository import GitRepository
749 749 from distutils.version import StrictVersion
750 750
751 stdout, stderr = GitRepository._run_git_command('--version')
751 stdout, stderr = GitRepository._run_git_command('--version', _bare=True,
752 _safe=True)
752 753
753 754 ver = (stdout.split(' ')[-1] or '').strip() or '0.0.0'
754 755 if len(ver.split('.')) > 3:
755 756 #StrictVersion needs to be only 3 element type
756 757 ver = '.'.join(ver.split('.')[:3])
757 758 try:
758 759 _ver = StrictVersion(ver)
759 760 except:
760 761 _ver = StrictVersion('0.0.0')
761 762 stderr = traceback.format_exc()
762 763
763 764 req_ver = '1.7.4'
764 765 to_old_git = False
765 766 if _ver < StrictVersion(req_ver):
766 767 to_old_git = True
767 768
768 769 if 'git' in BACKENDS:
769 770 log.debug('GIT version detected: %s' % stdout)
770 771 if stderr:
771 772 log.warning('Unable to detect git version org error was:%r' % stderr)
772 773 elif to_old_git:
773 774 log.warning('RhodeCode detected git version %s, which is too old '
774 775 'for the system to function properly. Make sure '
775 776 'its version is at least %s' % (ver, req_ver))
776 777 return _ver
777 778
778 779
779 780 @decorator.decorator
780 781 def jsonify(func, *args, **kwargs):
781 782 """Action decorator that formats output for JSON
782 783
783 784 Given a function that will return content, this decorator will turn
784 785 the result into JSON, with a content-type of 'application/json' and
785 786 output it.
786 787
787 788 """
788 789 from pylons.decorators.util import get_pylons
789 790 from rhodecode.lib.ext_json import json
790 791 pylons = get_pylons(args)
791 792 pylons.response.headers['Content-Type'] = 'application/json; charset=utf-8'
792 793 data = func(*args, **kwargs)
793 794 if isinstance(data, (list, tuple)):
794 795 msg = "JSON responses with Array envelopes are susceptible to " \
795 796 "cross-site data leak attacks, see " \
796 797 "http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
797 798 warnings.warn(msg, Warning, 2)
798 799 log.warning(msg)
799 800 log.debug("Returning JSON wrapped action output")
800 801 return json.dumps(data, encoding='utf-8')
@@ -1,673 +1,687 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 import logging
17 17 import traceback
18 18 import urllib
19 19 import urllib2
20 20 from dulwich.repo import Repo, NotGitRepository
21 21 from dulwich.objects import Tag
22 22 from string import Template
23 23
24 24 import rhodecode
25 25 from rhodecode.lib.vcs.backends.base import BaseRepository
26 26 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
27 27 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
28 28 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
29 29 from rhodecode.lib.vcs.exceptions import RepositoryError
30 30 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
31 31 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
32 32 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
33 33 from rhodecode.lib.vcs.utils.lazy import LazyProperty, ThreadLocalLazyProperty
34 34 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
35 35 from rhodecode.lib.vcs.utils.paths import abspath
36 36 from rhodecode.lib.vcs.utils.paths import get_user_home
37 37 from .workdir import GitWorkdir
38 38 from .changeset import GitChangeset
39 39 from .inmemory import GitInMemoryChangeset
40 40 from .config import ConfigFile
41 41 from rhodecode.lib import subprocessio
42 42
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46
47 47 class GitRepository(BaseRepository):
48 48 """
49 49 Git repository backend.
50 50 """
51 51 DEFAULT_BRANCH_NAME = 'master'
52 52 scm = 'git'
53 53
54 54 def __init__(self, repo_path, create=False, src_url=None,
55 55 update_after_clone=False, bare=False):
56 56
57 57 self.path = abspath(repo_path)
58 58 repo = self._get_repo(create, src_url, update_after_clone, bare)
59 59 self.bare = repo.bare
60 60
61 61 self._config_files = [
62 62 bare and abspath(self.path, 'config')
63 63 or abspath(self.path, '.git', 'config'),
64 64 abspath(get_user_home(), '.gitconfig'),
65 65 ]
66 66
67 67 @ThreadLocalLazyProperty
68 68 def _repo(self):
69 69 repo = Repo(self.path)
70 70 #temporary set that to now at later we will move it to constructor
71 71 baseui = None
72 72 if baseui is None:
73 73 from mercurial.ui import ui
74 74 baseui = ui()
75 75 # patch the instance of GitRepo with an "FAKE" ui object to add
76 76 # compatibility layer with Mercurial
77 77 setattr(repo, 'ui', baseui)
78 78 return repo
79 79
80 80 @property
81 81 def head(self):
82 82 try:
83 83 return self._repo.head()
84 84 except KeyError:
85 85 return None
86 86
87 87 @LazyProperty
88 88 def revisions(self):
89 89 """
90 90 Returns list of revisions' ids, in ascending order. Being lazy
91 91 attribute allows external tools to inject shas from cache.
92 92 """
93 93 return self._get_all_revisions()
94 94
95 95 @classmethod
96 96 def _run_git_command(cls, cmd, **opts):
97 97 """
98 98 Runs given ``cmd`` as git command and returns tuple
99 99 (stdout, stderr).
100 100
101 101 :param cmd: git command to be executed
102 102 :param opts: env options to pass into Subprocess command
103 103 """
104 104
105 if '_bare' in opts:
106 _copts = []
107 del opts['_bare']
108 else:
105 109 _copts = ['-c', 'core.quotepath=false', ]
110 safe_call = False
111 if '_safe' in opts:
112 #no exc on failure
113 del opts['_safe']
114 safe_call = True
115
106 116 _str_cmd = False
107 117 if isinstance(cmd, basestring):
108 118 cmd = [cmd]
109 119 _str_cmd = True
110 120
111 121 gitenv = os.environ
112 122 # need to clean fix GIT_DIR !
113 123 if 'GIT_DIR' in gitenv:
114 124 del gitenv['GIT_DIR']
115 125 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
116 126
117 127 _git_path = rhodecode.CONFIG.get('git_path', 'git')
118 128 cmd = [_git_path] + _copts + cmd
119 129 if _str_cmd:
120 130 cmd = ' '.join(cmd)
121 131 try:
122 132 _opts = dict(
123 133 env=gitenv,
124 134 shell=False,
125 135 )
126 136 _opts.update(opts)
127 137 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
128 138 except (EnvironmentError, OSError), err:
129 log.error(traceback.format_exc())
130 raise RepositoryError("Couldn't run git command (%s).\n"
131 "Original error was:%s" % (cmd, err))
139 tb_err = ("Couldn't run git command (%s).\n"
140 "Original error was:%s\n" % (cmd, err))
141 log.error(tb_err)
142 if safe_call:
143 return '', err
144 else:
145 raise RepositoryError(tb_err)
132 146
133 147 return ''.join(p.output), ''.join(p.error)
134 148
135 149 def run_git_command(self, cmd):
136 150 opts = {}
137 151 if os.path.isdir(self.path):
138 152 opts['cwd'] = self.path
139 153 return self._run_git_command(cmd, **opts)
140 154
141 155 @classmethod
142 156 def _check_url(cls, url):
143 157 """
144 158 Functon will check given url and try to verify if it's a valid
145 159 link. Sometimes it may happened that mercurial will issue basic
146 160 auth request that can cause whole API to hang when used from python
147 161 or other external calls.
148 162
149 163 On failures it'll raise urllib2.HTTPError
150 164 """
151 165 from mercurial.util import url as Url
152 166
153 167 # those authnadlers are patched for python 2.6.5 bug an
154 168 # infinit looping when given invalid resources
155 169 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
156 170
157 171 # check first if it's not an local url
158 172 if os.path.isdir(url) or url.startswith('file:'):
159 173 return True
160 174
161 175 if('+' in url[:url.find('://')]):
162 176 url = url[url.find('+') + 1:]
163 177
164 178 handlers = []
165 179 test_uri, authinfo = Url(url).authinfo()
166 180 if not test_uri.endswith('info/refs'):
167 181 test_uri = test_uri.rstrip('/') + '/info/refs'
168 182 if authinfo:
169 183 #create a password manager
170 184 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
171 185 passmgr.add_password(*authinfo)
172 186
173 187 handlers.extend((httpbasicauthhandler(passmgr),
174 188 httpdigestauthhandler(passmgr)))
175 189
176 190 o = urllib2.build_opener(*handlers)
177 191 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
178 192
179 193 q = {"service": 'git-upload-pack'}
180 194 qs = '?%s' % urllib.urlencode(q)
181 195 cu = "%s%s" % (test_uri, qs)
182 196 req = urllib2.Request(cu, None, {})
183 197
184 198 try:
185 199 resp = o.open(req)
186 200 return resp.code == 200
187 201 except Exception, e:
188 202 # means it cannot be cloned
189 203 raise urllib2.URLError("[%s] %s" % (url, e))
190 204
191 205 def _get_repo(self, create, src_url=None, update_after_clone=False,
192 206 bare=False):
193 207 if create and os.path.exists(self.path):
194 208 raise RepositoryError("Location already exist")
195 209 if src_url and not create:
196 210 raise RepositoryError("Create should be set to True if src_url is "
197 211 "given (clone operation creates repository)")
198 212 try:
199 213 if create and src_url:
200 214 GitRepository._check_url(src_url)
201 215 self.clone(src_url, update_after_clone, bare)
202 216 return Repo(self.path)
203 217 elif create:
204 218 os.mkdir(self.path)
205 219 if bare:
206 220 return Repo.init_bare(self.path)
207 221 else:
208 222 return Repo.init(self.path)
209 223 else:
210 224 return self._repo
211 225 except (NotGitRepository, OSError), err:
212 226 raise RepositoryError(err)
213 227
214 228 def _get_all_revisions(self):
215 229 # we must check if this repo is not empty, since later command
216 230 # fails if it is. And it's cheaper to ask than throw the subprocess
217 231 # errors
218 232 try:
219 233 self._repo.head()
220 234 except KeyError:
221 235 return []
222 236 cmd = 'rev-list --all --reverse --date-order'
223 237 try:
224 238 so, se = self.run_git_command(cmd)
225 239 except RepositoryError:
226 240 # Can be raised for empty repositories
227 241 return []
228 242 return so.splitlines()
229 243
230 244 def _get_all_revisions2(self):
231 245 #alternate implementation using dulwich
232 246 includes = [x[1][0] for x in self._parsed_refs.iteritems()
233 247 if x[1][1] != 'T']
234 248 return [c.commit.id for c in self._repo.get_walker(include=includes)]
235 249
236 250 def _get_revision(self, revision):
237 251 """
238 252 For git backend we always return integer here. This way we ensure
239 253 that changset's revision attribute would become integer.
240 254 """
241 255 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
242 256 is_bstr = lambda o: isinstance(o, (str, unicode))
243 257 is_null = lambda o: len(o) == revision.count('0')
244 258
245 259 if len(self.revisions) == 0:
246 260 raise EmptyRepositoryError("There are no changesets yet")
247 261
248 262 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
249 263 revision = self.revisions[-1]
250 264
251 265 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
252 266 or isinstance(revision, int) or is_null(revision)):
253 267 try:
254 268 revision = self.revisions[int(revision)]
255 269 except:
256 270 raise ChangesetDoesNotExistError("Revision %r does not exist "
257 271 "for this repository %s" % (revision, self))
258 272
259 273 elif is_bstr(revision):
260 274 # get by branch/tag name
261 275 _ref_revision = self._parsed_refs.get(revision)
262 276 _tags_shas = self.tags.values()
263 277 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
264 278 return _ref_revision[0]
265 279
266 280 # maybe it's a tag ? we don't have them in self.revisions
267 281 elif revision in _tags_shas:
268 282 return _tags_shas[_tags_shas.index(revision)]
269 283
270 284 elif not pattern.match(revision) or revision not in self.revisions:
271 285 raise ChangesetDoesNotExistError("Revision %r does not exist "
272 286 "for this repository %s" % (revision, self))
273 287
274 288 # Ensure we return full id
275 289 if not pattern.match(str(revision)):
276 290 raise ChangesetDoesNotExistError("Given revision %r not recognized"
277 291 % revision)
278 292 return revision
279 293
280 294 def _get_archives(self, archive_name='tip'):
281 295
282 296 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
283 297 yield {"type": i[0], "extension": i[1], "node": archive_name}
284 298
285 299 def _get_url(self, url):
286 300 """
287 301 Returns normalized url. If schema is not given, would fall to
288 302 filesystem (``file:///``) schema.
289 303 """
290 304 url = str(url)
291 305 if url != 'default' and not '://' in url:
292 306 url = ':///'.join(('file', url))
293 307 return url
294 308
295 309 @LazyProperty
296 310 def name(self):
297 311 return os.path.basename(self.path)
298 312
299 313 @LazyProperty
300 314 def last_change(self):
301 315 """
302 316 Returns last change made on this repository as datetime object
303 317 """
304 318 return date_fromtimestamp(self._get_mtime(), makedate()[1])
305 319
306 320 def _get_mtime(self):
307 321 try:
308 322 return time.mktime(self.get_changeset().date.timetuple())
309 323 except RepositoryError:
310 324 idx_loc = '' if self.bare else '.git'
311 325 # fallback to filesystem
312 326 in_path = os.path.join(self.path, idx_loc, "index")
313 327 he_path = os.path.join(self.path, idx_loc, "HEAD")
314 328 if os.path.exists(in_path):
315 329 return os.stat(in_path).st_mtime
316 330 else:
317 331 return os.stat(he_path).st_mtime
318 332
319 333 @LazyProperty
320 334 def description(self):
321 335 idx_loc = '' if self.bare else '.git'
322 336 undefined_description = u'unknown'
323 337 description_path = os.path.join(self.path, idx_loc, 'description')
324 338 if os.path.isfile(description_path):
325 339 return safe_unicode(open(description_path).read())
326 340 else:
327 341 return undefined_description
328 342
329 343 @LazyProperty
330 344 def contact(self):
331 345 undefined_contact = u'Unknown'
332 346 return undefined_contact
333 347
334 348 @property
335 349 def branches(self):
336 350 if not self.revisions:
337 351 return {}
338 352 sortkey = lambda ctx: ctx[0]
339 353 _branches = [(x[0], x[1][0])
340 354 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
341 355 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
342 356
343 357 @LazyProperty
344 358 def tags(self):
345 359 return self._get_tags()
346 360
347 361 def _get_tags(self):
348 362 if not self.revisions:
349 363 return {}
350 364
351 365 sortkey = lambda ctx: ctx[0]
352 366 _tags = [(x[0], x[1][0])
353 367 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
354 368 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
355 369
356 370 def tag(self, name, user, revision=None, message=None, date=None,
357 371 **kwargs):
358 372 """
359 373 Creates and returns a tag for the given ``revision``.
360 374
361 375 :param name: name for new tag
362 376 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
363 377 :param revision: changeset id for which new tag would be created
364 378 :param message: message of the tag's commit
365 379 :param date: date of tag's commit
366 380
367 381 :raises TagAlreadyExistError: if tag with same name already exists
368 382 """
369 383 if name in self.tags:
370 384 raise TagAlreadyExistError("Tag %s already exists" % name)
371 385 changeset = self.get_changeset(revision)
372 386 message = message or "Added tag %s for commit %s" % (name,
373 387 changeset.raw_id)
374 388 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
375 389
376 390 self._parsed_refs = self._get_parsed_refs()
377 391 self.tags = self._get_tags()
378 392 return changeset
379 393
380 394 def remove_tag(self, name, user, message=None, date=None):
381 395 """
382 396 Removes tag with the given ``name``.
383 397
384 398 :param name: name of the tag to be removed
385 399 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
386 400 :param message: message of the tag's removal commit
387 401 :param date: date of tag's removal commit
388 402
389 403 :raises TagDoesNotExistError: if tag with given name does not exists
390 404 """
391 405 if name not in self.tags:
392 406 raise TagDoesNotExistError("Tag %s does not exist" % name)
393 407 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
394 408 try:
395 409 os.remove(tagpath)
396 410 self._parsed_refs = self._get_parsed_refs()
397 411 self.tags = self._get_tags()
398 412 except OSError, e:
399 413 raise RepositoryError(e.strerror)
400 414
401 415 @LazyProperty
402 416 def _parsed_refs(self):
403 417 return self._get_parsed_refs()
404 418
405 419 def _get_parsed_refs(self):
406 420 refs = self._repo.get_refs()
407 421 keys = [('refs/heads/', 'H'),
408 422 ('refs/remotes/origin/', 'RH'),
409 423 ('refs/tags/', 'T')]
410 424 _refs = {}
411 425 for ref, sha in refs.iteritems():
412 426 for k, type_ in keys:
413 427 if ref.startswith(k):
414 428 _key = ref[len(k):]
415 429 if type_ == 'T':
416 430 obj = self._repo.get_object(sha)
417 431 if isinstance(obj, Tag):
418 432 sha = self._repo.get_object(sha).object[1]
419 433 _refs[_key] = [sha, type_]
420 434 break
421 435 return _refs
422 436
423 437 def _heads(self, reverse=False):
424 438 refs = self._repo.get_refs()
425 439 heads = {}
426 440
427 441 for key, val in refs.items():
428 442 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
429 443 if key.startswith(ref_key):
430 444 n = key[len(ref_key):]
431 445 if n not in ['HEAD']:
432 446 heads[n] = val
433 447
434 448 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
435 449
436 450 def get_changeset(self, revision=None):
437 451 """
438 452 Returns ``GitChangeset`` object representing commit from git repository
439 453 at the given revision or head (most recent commit) if None given.
440 454 """
441 455 if isinstance(revision, GitChangeset):
442 456 return revision
443 457 revision = self._get_revision(revision)
444 458 changeset = GitChangeset(repository=self, revision=revision)
445 459 return changeset
446 460
447 461 def get_changesets(self, start=None, end=None, start_date=None,
448 462 end_date=None, branch_name=None, reverse=False):
449 463 """
450 464 Returns iterator of ``GitChangeset`` objects from start to end (both
451 465 are inclusive), in ascending date order (unless ``reverse`` is set).
452 466
453 467 :param start: changeset ID, as str; first returned changeset
454 468 :param end: changeset ID, as str; last returned changeset
455 469 :param start_date: if specified, changesets with commit date less than
456 470 ``start_date`` would be filtered out from returned set
457 471 :param end_date: if specified, changesets with commit date greater than
458 472 ``end_date`` would be filtered out from returned set
459 473 :param branch_name: if specified, changesets not reachable from given
460 474 branch would be filtered out from returned set
461 475 :param reverse: if ``True``, returned generator would be reversed
462 476 (meaning that returned changesets would have descending date order)
463 477
464 478 :raise BranchDoesNotExistError: If given ``branch_name`` does not
465 479 exist.
466 480 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
467 481 ``end`` could not be found.
468 482
469 483 """
470 484 if branch_name and branch_name not in self.branches:
471 485 raise BranchDoesNotExistError("Branch '%s' not found" \
472 486 % branch_name)
473 487 # %H at format means (full) commit hash, initial hashes are retrieved
474 488 # in ascending date order
475 489 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
476 490 cmd_params = {}
477 491 if start_date:
478 492 cmd_template += ' --since "$since"'
479 493 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
480 494 if end_date:
481 495 cmd_template += ' --until "$until"'
482 496 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
483 497 if branch_name:
484 498 cmd_template += ' $branch_name'
485 499 cmd_params['branch_name'] = branch_name
486 500 else:
487 501 cmd_template += ' --all'
488 502
489 503 cmd = Template(cmd_template).safe_substitute(**cmd_params)
490 504 revs = self.run_git_command(cmd)[0].splitlines()
491 505 start_pos = 0
492 506 end_pos = len(revs)
493 507 if start:
494 508 _start = self._get_revision(start)
495 509 try:
496 510 start_pos = revs.index(_start)
497 511 except ValueError:
498 512 pass
499 513
500 514 if end is not None:
501 515 _end = self._get_revision(end)
502 516 try:
503 517 end_pos = revs.index(_end)
504 518 except ValueError:
505 519 pass
506 520
507 521 if None not in [start, end] and start_pos > end_pos:
508 522 raise RepositoryError('start cannot be after end')
509 523
510 524 if end_pos is not None:
511 525 end_pos += 1
512 526
513 527 revs = revs[start_pos:end_pos]
514 528 if reverse:
515 529 revs = reversed(revs)
516 530 for rev in revs:
517 531 yield self.get_changeset(rev)
518 532
519 533 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
520 534 context=3):
521 535 """
522 536 Returns (git like) *diff*, as plain text. Shows changes introduced by
523 537 ``rev2`` since ``rev1``.
524 538
525 539 :param rev1: Entry point from which diff is shown. Can be
526 540 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
527 541 the changes since empty state of the repository until ``rev2``
528 542 :param rev2: Until which revision changes should be shown.
529 543 :param ignore_whitespace: If set to ``True``, would not show whitespace
530 544 changes. Defaults to ``False``.
531 545 :param context: How many lines before/after changed lines should be
532 546 shown. Defaults to ``3``.
533 547 """
534 548 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
535 549 if ignore_whitespace:
536 550 flags.append('-w')
537 551
538 552 if hasattr(rev1, 'raw_id'):
539 553 rev1 = getattr(rev1, 'raw_id')
540 554
541 555 if hasattr(rev2, 'raw_id'):
542 556 rev2 = getattr(rev2, 'raw_id')
543 557
544 558 if rev1 == self.EMPTY_CHANGESET:
545 559 rev2 = self.get_changeset(rev2).raw_id
546 560 cmd = ' '.join(['show'] + flags + [rev2])
547 561 else:
548 562 rev1 = self.get_changeset(rev1).raw_id
549 563 rev2 = self.get_changeset(rev2).raw_id
550 564 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
551 565
552 566 if path:
553 567 cmd += ' -- "%s"' % path
554 568
555 569 stdout, stderr = self.run_git_command(cmd)
556 570 # If we used 'show' command, strip first few lines (until actual diff
557 571 # starts)
558 572 if rev1 == self.EMPTY_CHANGESET:
559 573 lines = stdout.splitlines()
560 574 x = 0
561 575 for line in lines:
562 576 if line.startswith('diff'):
563 577 break
564 578 x += 1
565 579 # Append new line just like 'diff' command do
566 580 stdout = '\n'.join(lines[x:]) + '\n'
567 581 return stdout
568 582
569 583 @LazyProperty
570 584 def in_memory_changeset(self):
571 585 """
572 586 Returns ``GitInMemoryChangeset`` object for this repository.
573 587 """
574 588 return GitInMemoryChangeset(self)
575 589
576 590 def clone(self, url, update_after_clone=True, bare=False):
577 591 """
578 592 Tries to clone changes from external location.
579 593
580 594 :param update_after_clone: If set to ``False``, git won't checkout
581 595 working directory
582 596 :param bare: If set to ``True``, repository would be cloned into
583 597 *bare* git repository (no working directory at all).
584 598 """
585 599 url = self._get_url(url)
586 600 cmd = ['clone']
587 601 if bare:
588 602 cmd.append('--bare')
589 603 elif not update_after_clone:
590 604 cmd.append('--no-checkout')
591 605 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
592 606 cmd = ' '.join(cmd)
593 607 # If error occurs run_git_command raises RepositoryError already
594 608 self.run_git_command(cmd)
595 609
596 610 def pull(self, url):
597 611 """
598 612 Tries to pull changes from external location.
599 613 """
600 614 url = self._get_url(url)
601 615 cmd = ['pull']
602 616 cmd.append("--ff-only")
603 617 cmd.append(url)
604 618 cmd = ' '.join(cmd)
605 619 # If error occurs run_git_command raises RepositoryError already
606 620 self.run_git_command(cmd)
607 621
608 622 def fetch(self, url):
609 623 """
610 624 Tries to pull changes from external location.
611 625 """
612 626 url = self._get_url(url)
613 627 so, se = self.run_git_command('ls-remote -h %s' % url)
614 628 refs = []
615 629 for line in (x for x in so.splitlines()):
616 630 sha, ref = line.split('\t')
617 631 refs.append(ref)
618 632 refs = ' '.join(('+%s:%s' % (r, r) for r in refs))
619 633 cmd = '''fetch %s -- %s''' % (url, refs)
620 634 self.run_git_command(cmd)
621 635
622 636 @LazyProperty
623 637 def workdir(self):
624 638 """
625 639 Returns ``Workdir`` instance for this repository.
626 640 """
627 641 return GitWorkdir(self)
628 642
629 643 def get_config_value(self, section, name, config_file=None):
630 644 """
631 645 Returns configuration value for a given [``section``] and ``name``.
632 646
633 647 :param section: Section we want to retrieve value from
634 648 :param name: Name of configuration we want to retrieve
635 649 :param config_file: A path to file which should be used to retrieve
636 650 configuration from (might also be a list of file paths)
637 651 """
638 652 if config_file is None:
639 653 config_file = []
640 654 elif isinstance(config_file, basestring):
641 655 config_file = [config_file]
642 656
643 657 def gen_configs():
644 658 for path in config_file + self._config_files:
645 659 try:
646 660 yield ConfigFile.from_path(path)
647 661 except (IOError, OSError, ValueError):
648 662 continue
649 663
650 664 for config in gen_configs():
651 665 try:
652 666 return config.get(section, name)
653 667 except KeyError:
654 668 continue
655 669 return None
656 670
657 671 def get_user_name(self, config_file=None):
658 672 """
659 673 Returns user's name from global configuration file.
660 674
661 675 :param config_file: A path to file which should be used to retrieve
662 676 configuration from (might also be a list of file paths)
663 677 """
664 678 return self.get_config_value('user', 'name', config_file)
665 679
666 680 def get_user_email(self, config_file=None):
667 681 """
668 682 Returns user's email from global configuration file.
669 683
670 684 :param config_file: A path to file which should be used to retrieve
671 685 configuration from (might also be a list of file paths)
672 686 """
673 687 return self.get_config_value('user', 'email', config_file)
General Comments 0
You need to be logged in to leave comments. Login now