##// END OF EJS Templates
Git fixes...
marcink -
r2047:092080cd beta
parent child Browse files
Show More
@@ -1,516 +1,530 b''
1 1 .. _changelog:
2 2
3 3 Changelog
4 4 =========
5 5
6 6
7 1.3.1 (**2012-XX-XX**)
7 1.3.2 (**2012-XX-XX**)
8 8 ----------------------
9 9
10 10 :status: in-progress
11 11 :branch: beta
12 12
13 13 news
14 14 ++++
15 15
16 16
17 17 fixes
18 18 +++++
19 19
20 20
21 1.3.1 (**2012-02-27**)
22 ----------------------
23
24 news
25 ++++
26
27
28 fixes
29 +++++
30
31 - redirection loop occurs when remember-me wasn't checked during login
32 - fixes issues with git blob history generation
33 - don't fetch branch for git in file history dropdown. Causes unneeded slowness
34
21 35 1.3.0 (**2012-02-26**)
22 36 ----------------------
23 37
24 38 news
25 39 ++++
26 40
27 41 - code review, inspired by github code-comments
28 42 - #215 rst and markdown README files support
29 43 - #252 Container-based and proxy pass-through authentication support
30 44 - #44 branch browser. Filtering of changelog by branches
31 45 - mercurial bookmarks support
32 46 - new hover top menu, optimized to add maximum size for important views
33 47 - configurable clone url template with possibility to specify protocol like
34 48 ssh:// or http:// and also manually alter other parts of clone_url.
35 49 - enabled largefiles extension by default
36 50 - optimized summary file pages and saved a lot of unused space in them
37 51 - #239 option to manually mark repository as fork
38 52 - #320 mapping of commit authors to RhodeCode users
39 53 - #304 hashes are displayed using monospace font
40 54 - diff configuration, toggle white lines and context lines
41 55 - #307 configurable diffs, whitespace toggle, increasing context lines
42 56 - sorting on branches, tags and bookmarks using YUI datatable
43 57 - improved file filter on files page
44 58 - implements #330 api method for listing nodes ar particular revision
45 59 - #73 added linking issues in commit messages to chosen issue tracker url
46 60 based on user defined regular expression
47 61 - added linking of changesets in commit messages
48 62 - new compact changelog with expandable commit messages
49 63 - firstname and lastname are optional in user creation
50 64 - #348 added post-create repository hook
51 65 - #212 global encoding settings is now configurable from .ini files
52 66 - #227 added repository groups permissions
53 67 - markdown gets codehilite extensions
54 68 - new API methods, delete_repositories, grante/revoke permissions for groups
55 69 and repos
56 70
57 71
58 72 fixes
59 73 +++++
60 74
61 75 - rewrote dbsession management for atomic operations, and better error handling
62 76 - fixed sorting of repo tables
63 77 - #326 escape of special html entities in diffs
64 78 - normalized user_name => username in api attributes
65 79 - fixes #298 ldap created users with mixed case emails created conflicts
66 80 on saving a form
67 81 - fixes issue when owner of a repo couldn't revoke permissions for users
68 82 and groups
69 83 - fixes #271 rare JSON serialization problem with statistics
70 84 - fixes #337 missing validation check for conflicting names of a group with a
71 85 repositories group
72 86 - #340 fixed session problem for mysql and celery tasks
73 87 - fixed #331 RhodeCode mangles repository names if the a repository group
74 88 contains the "full path" to the repositories
75 89 - #355 RhodeCode doesn't store encrypted LDAP passwords
76 90
77 91 1.2.5 (**2012-01-28**)
78 92 ----------------------
79 93
80 94 news
81 95 ++++
82 96
83 97 fixes
84 98 +++++
85 99
86 100 - #340 Celery complains about MySQL server gone away, added session cleanup
87 101 for celery tasks
88 102 - #341 "scanning for repositories in None" log message during Rescan was missing
89 103 a parameter
90 104 - fixed creating archives with subrepos. Some hooks were triggered during that
91 105 operation leading to crash.
92 106 - fixed missing email in account page.
93 107 - Reverted Mercurial to 2.0.1 for windows due to bug in Mercurial that makes
94 108 forking on windows impossible
95 109
96 110 1.2.4 (**2012-01-19**)
97 111 ----------------------
98 112
99 113 news
100 114 ++++
101 115
102 116 - RhodeCode is bundled with mercurial series 2.0.X by default, with
103 117 full support to largefiles extension. Enabled by default in new installations
104 118 - #329 Ability to Add/Remove Groups to/from a Repository via AP
105 119 - added requires.txt file with requirements
106 120
107 121 fixes
108 122 +++++
109 123
110 124 - fixes db session issues with celery when emailing admins
111 125 - #331 RhodeCode mangles repository names if the a repository group
112 126 contains the "full path" to the repositories
113 127 - #298 Conflicting e-mail addresses for LDAP and RhodeCode users
114 128 - DB session cleanup after hg protocol operations, fixes issues with
115 129 `mysql has gone away` errors
116 130 - #333 doc fixes for get_repo api function
117 131 - #271 rare JSON serialization problem with statistics enabled
118 132 - #337 Fixes issues with validation of repository name conflicting with
119 133 a group name. A proper message is now displayed.
120 134 - #292 made ldap_dn in user edit readonly, to get rid of confusion that field
121 135 doesn't work
122 136 - #316 fixes issues with web description in hgrc files
123 137
124 138 1.2.3 (**2011-11-02**)
125 139 ----------------------
126 140
127 141 news
128 142 ++++
129 143
130 144 - added option to manage repos group for non admin users
131 145 - added following API methods for get_users, create_user, get_users_groups,
132 146 get_users_group, create_users_group, add_user_to_users_groups, get_repos,
133 147 get_repo, create_repo, add_user_to_repo
134 148 - implements #237 added password confirmation for my account
135 149 and admin edit user.
136 150 - implements #291 email notification for global events are now sent to all
137 151 administrator users, and global config email.
138 152
139 153 fixes
140 154 +++++
141 155
142 156 - added option for passing auth method for smtp mailer
143 157 - #276 issue with adding a single user with id>10 to usergroups
144 158 - #277 fixes windows LDAP settings in which missing values breaks the ldap auth
145 159 - #288 fixes managing of repos in a group for non admin user
146 160
147 161 1.2.2 (**2011-10-17**)
148 162 ----------------------
149 163
150 164 news
151 165 ++++
152 166
153 167 - #226 repo groups are available by path instead of numerical id
154 168
155 169 fixes
156 170 +++++
157 171
158 172 - #259 Groups with the same name but with different parent group
159 173 - #260 Put repo in group, then move group to another group -> repo becomes unavailable
160 174 - #258 RhodeCode 1.2 assumes egg folder is writable (lockfiles problems)
161 175 - #265 ldap save fails sometimes on converting attributes to booleans,
162 176 added getter and setter into model that will prevent from this on db model level
163 177 - fixed problems with timestamps issues #251 and #213
164 178 - fixes #266 RhodeCode allows to create repo with the same name and in
165 179 the same parent as group
166 180 - fixes #245 Rescan of the repositories on Windows
167 181 - fixes #248 cannot edit repos inside a group on windows
168 182 - fixes #219 forking problems on windows
169 183
170 184 1.2.1 (**2011-10-08**)
171 185 ----------------------
172 186
173 187 news
174 188 ++++
175 189
176 190
177 191 fixes
178 192 +++++
179 193
180 194 - fixed problems with basic auth and push problems
181 195 - gui fixes
182 196 - fixed logger
183 197
184 198 1.2.0 (**2011-10-07**)
185 199 ----------------------
186 200
187 201 news
188 202 ++++
189 203
190 204 - implemented #47 repository groups
191 205 - implemented #89 Can setup google analytics code from settings menu
192 206 - implemented #91 added nicer looking archive urls with more download options
193 207 like tags, branches
194 208 - implemented #44 into file browsing, and added follow branch option
195 209 - implemented #84 downloads can be enabled/disabled for each repository
196 210 - anonymous repository can be cloned without having to pass default:default
197 211 into clone url
198 212 - fixed #90 whoosh indexer can index chooses repositories passed in command
199 213 line
200 214 - extended journal with day aggregates and paging
201 215 - implemented #107 source code lines highlight ranges
202 216 - implemented #93 customizable changelog on combined revision ranges -
203 217 equivalent of githubs compare view
204 218 - implemented #108 extended and more powerful LDAP configuration
205 219 - implemented #56 users groups
206 220 - major code rewrites optimized codes for speed and memory usage
207 221 - raw and diff downloads are now in git format
208 222 - setup command checks for write access to given path
209 223 - fixed many issues with international characters and unicode. It uses utf8
210 224 decode with replace to provide less errors even with non utf8 encoded strings
211 225 - #125 added API KEY access to feeds
212 226 - #109 Repository can be created from external Mercurial link (aka. remote
213 227 repository, and manually updated (via pull) from admin panel
214 228 - beta git support - push/pull server + basic view for git repos
215 229 - added followers page and forks page
216 230 - server side file creation (with binary file upload interface)
217 231 and edition with commits powered by codemirror
218 232 - #111 file browser file finder, quick lookup files on whole file tree
219 233 - added quick login sliding menu into main page
220 234 - changelog uses lazy loading of affected files details, in some scenarios
221 235 this can improve speed of changelog page dramatically especially for
222 236 larger repositories.
223 237 - implements #214 added support for downloading subrepos in download menu.
224 238 - Added basic API for direct operations on rhodecode via JSON
225 239 - Implemented advanced hook management
226 240
227 241 fixes
228 242 +++++
229 243
230 244 - fixed file browser bug, when switching into given form revision the url was
231 245 not changing
232 246 - fixed propagation to error controller on simplehg and simplegit middlewares
233 247 - fixed error when trying to make a download on empty repository
234 248 - fixed problem with '[' chars in commit messages in journal
235 249 - fixed #99 Unicode errors, on file node paths with non utf-8 characters
236 250 - journal fork fixes
237 251 - removed issue with space inside renamed repository after deletion
238 252 - fixed strange issue on formencode imports
239 253 - fixed #126 Deleting repository on Windows, rename used incompatible chars.
240 254 - #150 fixes for errors on repositories mapped in db but corrupted in
241 255 filesystem
242 256 - fixed problem with ascendant characters in realm #181
243 257 - fixed problem with sqlite file based database connection pool
244 258 - whoosh indexer and code stats share the same dynamic extensions map
245 259 - fixes #188 - relationship delete of repo_to_perm entry on user removal
246 260 - fixes issue #189 Trending source files shows "show more" when no more exist
247 261 - fixes issue #197 Relative paths for pidlocks
248 262 - fixes issue #198 password will require only 3 chars now for login form
249 263 - fixes issue #199 wrong redirection for non admin users after creating a repository
250 264 - fixes issues #202, bad db constraint made impossible to attach same group
251 265 more than one time. Affects only mysql/postgres
252 266 - fixes #218 os.kill patch for windows was missing sig param
253 267 - improved rendering of dag (they are not trimmed anymore when number of
254 268 heads exceeds 5)
255 269
256 270 1.1.8 (**2011-04-12**)
257 271 ----------------------
258 272
259 273 news
260 274 ++++
261 275
262 276 - improved windows support
263 277
264 278 fixes
265 279 +++++
266 280
267 281 - fixed #140 freeze of python dateutil library, since new version is python2.x
268 282 incompatible
269 283 - setup-app will check for write permission in given path
270 284 - cleaned up license info issue #149
271 285 - fixes for issues #137,#116 and problems with unicode and accented characters.
272 286 - fixes crashes on gravatar, when passed in email as unicode
273 287 - fixed tooltip flickering problems
274 288 - fixed came_from redirection on windows
275 289 - fixed logging modules, and sql formatters
276 290 - windows fixes for os.kill issue #133
277 291 - fixes path splitting for windows issues #148
278 292 - fixed issue #143 wrong import on migration to 1.1.X
279 293 - fixed problems with displaying binary files, thanks to Thomas Waldmann
280 294 - removed name from archive files since it's breaking ui for long repo names
281 295 - fixed issue with archive headers sent to browser, thanks to Thomas Waldmann
282 296 - fixed compatibility for 1024px displays, and larger dpi settings, thanks to
283 297 Thomas Waldmann
284 298 - fixed issue #166 summary pager was skipping 10 revisions on second page
285 299
286 300
287 301 1.1.7 (**2011-03-23**)
288 302 ----------------------
289 303
290 304 news
291 305 ++++
292 306
293 307 fixes
294 308 +++++
295 309
296 310 - fixed (again) #136 installation support for FreeBSD
297 311
298 312
299 313 1.1.6 (**2011-03-21**)
300 314 ----------------------
301 315
302 316 news
303 317 ++++
304 318
305 319 fixes
306 320 +++++
307 321
308 322 - fixed #136 installation support for FreeBSD
309 323 - RhodeCode will check for python version during installation
310 324
311 325 1.1.5 (**2011-03-17**)
312 326 ----------------------
313 327
314 328 news
315 329 ++++
316 330
317 331 - basic windows support, by exchanging pybcrypt into sha256 for windows only
318 332 highly inspired by idea of mantis406
319 333
320 334 fixes
321 335 +++++
322 336
323 337 - fixed sorting by author in main page
324 338 - fixed crashes with diffs on binary files
325 339 - fixed #131 problem with boolean values for LDAP
326 340 - fixed #122 mysql problems thanks to striker69
327 341 - fixed problem with errors on calling raw/raw_files/annotate functions
328 342 with unknown revisions
329 343 - fixed returned rawfiles attachment names with international character
330 344 - cleaned out docs, big thanks to Jason Harris
331 345
332 346 1.1.4 (**2011-02-19**)
333 347 ----------------------
334 348
335 349 news
336 350 ++++
337 351
338 352 fixes
339 353 +++++
340 354
341 355 - fixed formencode import problem on settings page, that caused server crash
342 356 when that page was accessed as first after server start
343 357 - journal fixes
344 358 - fixed option to access repository just by entering http://server/<repo_name>
345 359
346 360 1.1.3 (**2011-02-16**)
347 361 ----------------------
348 362
349 363 news
350 364 ++++
351 365
352 366 - implemented #102 allowing the '.' character in username
353 367 - added option to access repository just by entering http://server/<repo_name>
354 368 - celery task ignores result for better performance
355 369
356 370 fixes
357 371 +++++
358 372
359 373 - fixed ehlo command and non auth mail servers on smtp_lib. Thanks to
360 374 apollo13 and Johan Walles
361 375 - small fixes in journal
362 376 - fixed problems with getting setting for celery from .ini files
363 377 - registration, password reset and login boxes share the same title as main
364 378 application now
365 379 - fixed #113: to high permissions to fork repository
366 380 - fixed problem with '[' chars in commit messages in journal
367 381 - removed issue with space inside renamed repository after deletion
368 382 - db transaction fixes when filesystem repository creation failed
369 383 - fixed #106 relation issues on databases different than sqlite
370 384 - fixed static files paths links to use of url() method
371 385
372 386 1.1.2 (**2011-01-12**)
373 387 ----------------------
374 388
375 389 news
376 390 ++++
377 391
378 392
379 393 fixes
380 394 +++++
381 395
382 396 - fixes #98 protection against float division of percentage stats
383 397 - fixed graph bug
384 398 - forced webhelpers version since it was making troubles during installation
385 399
386 400 1.1.1 (**2011-01-06**)
387 401 ----------------------
388 402
389 403 news
390 404 ++++
391 405
392 406 - added force https option into ini files for easier https usage (no need to
393 407 set server headers with this options)
394 408 - small css updates
395 409
396 410 fixes
397 411 +++++
398 412
399 413 - fixed #96 redirect loop on files view on repositories without changesets
400 414 - fixed #97 unicode string passed into server header in special cases (mod_wsgi)
401 415 and server crashed with errors
402 416 - fixed large tooltips problems on main page
403 417 - fixed #92 whoosh indexer is more error proof
404 418
405 419 1.1.0 (**2010-12-18**)
406 420 ----------------------
407 421
408 422 news
409 423 ++++
410 424
411 425 - rewrite of internals for vcs >=0.1.10
412 426 - uses mercurial 1.7 with dotencode disabled for maintaining compatibility
413 427 with older clients
414 428 - anonymous access, authentication via ldap
415 429 - performance upgrade for cached repos list - each repository has its own
416 430 cache that's invalidated when needed.
417 431 - performance upgrades on repositories with large amount of commits (20K+)
418 432 - main page quick filter for filtering repositories
419 433 - user dashboards with ability to follow chosen repositories actions
420 434 - sends email to admin on new user registration
421 435 - added cache/statistics reset options into repository settings
422 436 - more detailed action logger (based on hooks) with pushed changesets lists
423 437 and options to disable those hooks from admin panel
424 438 - introduced new enhanced changelog for merges that shows more accurate results
425 439 - new improved and faster code stats (based on pygments lexers mapping tables,
426 440 showing up to 10 trending sources for each repository. Additionally stats
427 441 can be disabled in repository settings.
428 442 - gui optimizations, fixed application width to 1024px
429 443 - added cut off (for large files/changesets) limit into config files
430 444 - whoosh, celeryd, upgrade moved to paster command
431 445 - other than sqlite database backends can be used
432 446
433 447 fixes
434 448 +++++
435 449
436 450 - fixes #61 forked repo was showing only after cache expired
437 451 - fixes #76 no confirmation on user deletes
438 452 - fixes #66 Name field misspelled
439 453 - fixes #72 block user removal when he owns repositories
440 454 - fixes #69 added password confirmation fields
441 455 - fixes #87 RhodeCode crashes occasionally on updating repository owner
442 456 - fixes #82 broken annotations on files with more than 1 blank line at the end
443 457 - a lot of fixes and tweaks for file browser
444 458 - fixed detached session issues
445 459 - fixed when user had no repos he would see all repos listed in my account
446 460 - fixed ui() instance bug when global hgrc settings was loaded for server
447 461 instance and all hgrc options were merged with our db ui() object
448 462 - numerous small bugfixes
449 463
450 464 (special thanks for TkSoh for detailed feedback)
451 465
452 466
453 467 1.0.2 (**2010-11-12**)
454 468 ----------------------
455 469
456 470 news
457 471 ++++
458 472
459 473 - tested under python2.7
460 474 - bumped sqlalchemy and celery versions
461 475
462 476 fixes
463 477 +++++
464 478
465 479 - fixed #59 missing graph.js
466 480 - fixed repo_size crash when repository had broken symlinks
467 481 - fixed python2.5 crashes.
468 482
469 483
470 484 1.0.1 (**2010-11-10**)
471 485 ----------------------
472 486
473 487 news
474 488 ++++
475 489
476 490 - small css updated
477 491
478 492 fixes
479 493 +++++
480 494
481 495 - fixed #53 python2.5 incompatible enumerate calls
482 496 - fixed #52 disable mercurial extension for web
483 497 - fixed #51 deleting repositories don't delete it's dependent objects
484 498
485 499
486 500 1.0.0 (**2010-11-02**)
487 501 ----------------------
488 502
489 503 - security bugfix simplehg wasn't checking for permissions on commands
490 504 other than pull or push.
491 505 - fixed doubled messages after push or pull in admin journal
492 506 - templating and css corrections, fixed repo switcher on chrome, updated titles
493 507 - admin menu accessible from options menu on repository view
494 508 - permissions cached queries
495 509
496 510 1.0.0rc4 (**2010-10-12**)
497 511 --------------------------
498 512
499 513 - fixed python2.5 missing simplejson imports (thanks to Jens BΓ€ckman)
500 514 - removed cache_manager settings from sqlalchemy meta
501 515 - added sqlalchemy cache settings to ini files
502 516 - validated password length and added second try of failure on paster setup-app
503 517 - fixed setup database destroy prompt even when there was no db
504 518
505 519
506 520 1.0.0rc3 (**2010-10-11**)
507 521 -------------------------
508 522
509 523 - fixed i18n during installation.
510 524
511 525 1.0.0rc2 (**2010-10-11**)
512 526 -------------------------
513 527
514 528 - Disabled dirsize in file browser, it's causing nasty bug when dir renames
515 529 occure. After vcs is fixed it'll be put back again.
516 530 - templating/css rewrites, optimized css. No newline at end of file
@@ -1,450 +1,451 b''
1 1 import re
2 2 from itertools import chain
3 3 from dulwich import objects
4 4 from subprocess import Popen, PIPE
5 5 from rhodecode.lib.vcs.conf import settings
6 6 from rhodecode.lib.vcs.exceptions import RepositoryError
7 7 from rhodecode.lib.vcs.exceptions import ChangesetError
8 8 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
9 9 from rhodecode.lib.vcs.exceptions import VCSError
10 10 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
11 11 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError
12 12 from rhodecode.lib.vcs.backends.base import BaseChangeset
13 13 from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, RemovedFileNode
14 14 from rhodecode.lib.vcs.utils import safe_unicode
15 15 from rhodecode.lib.vcs.utils import date_fromtimestamp
16 16 from rhodecode.lib.vcs.utils.lazy import LazyProperty
17 17
18 18
19 19 class GitChangeset(BaseChangeset):
20 20 """
21 21 Represents state of the repository at single revision.
22 22 """
23 23
24 24 def __init__(self, repository, revision):
25 25 self._stat_modes = {}
26 26 self.repository = repository
27 27 self.raw_id = revision
28 28 self.revision = repository.revisions.index(revision)
29 29
30 30 self.short_id = self.raw_id[:12]
31 31 self.id = self.raw_id
32 32 try:
33 33 commit = self.repository._repo.get_object(self.raw_id)
34 34 except KeyError:
35 35 raise RepositoryError("Cannot get object with id %s" % self.raw_id)
36 36 self._commit = commit
37 37 self._tree_id = commit.tree
38 38
39 39 try:
40 40 self.message = safe_unicode(commit.message[:-1])
41 41 # Always strip last eol
42 42 except UnicodeDecodeError:
43 43 self.message = commit.message[:-1].decode(commit.encoding
44 44 or 'utf-8')
45 45 #self.branch = None
46 46 self.tags = []
47 47 #tree = self.repository.get_object(self._tree_id)
48 48 self.nodes = {}
49 49 self._paths = {}
50 50
51 51 @LazyProperty
52 52 def author(self):
53 53 return safe_unicode(self._commit.committer)
54 54
55 55 @LazyProperty
56 56 def date(self):
57 57 return date_fromtimestamp(self._commit.commit_time,
58 58 self._commit.commit_timezone)
59 59
60 60 @LazyProperty
61 61 def status(self):
62 62 """
63 63 Returns modified, added, removed, deleted files for current changeset
64 64 """
65 65 return self.changed, self.added, self.removed
66 66
67 67 @LazyProperty
68 68 def branch(self):
69 69 # TODO: Cache as we walk (id <-> branch name mapping)
70 70 refs = self.repository._repo.get_refs()
71 71 heads = [(key[len('refs/heads/'):], val) for key, val in refs.items()
72 72 if key.startswith('refs/heads/')]
73 73
74 74 for name, id in heads:
75 75 walker = self.repository._repo.object_store.get_graph_walker([id])
76 76 while True:
77 77 id = walker.next()
78 78 if not id:
79 79 break
80 80 if id == self.id:
81 81 return safe_unicode(name)
82 82 raise ChangesetError("This should not happen... Have you manually "
83 83 "change id of the changeset?")
84 84
85 85 def _fix_path(self, path):
86 86 """
87 87 Paths are stored without trailing slash so we need to get rid off it if
88 88 needed.
89 89 """
90 90 if path.endswith('/'):
91 91 path = path.rstrip('/')
92 92 return path
93 93
94 94 def _get_id_for_path(self, path):
95 95 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
96 96 if not path in self._paths:
97 97 path = path.strip('/')
98 98 # set root tree
99 99 tree = self.repository._repo[self._commit.tree]
100 100 if path == '':
101 101 self._paths[''] = tree.id
102 102 return tree.id
103 103 splitted = path.split('/')
104 104 dirs, name = splitted[:-1], splitted[-1]
105 105 curdir = ''
106 106 for dir in dirs:
107 107 if curdir:
108 108 curdir = '/'.join((curdir, dir))
109 109 else:
110 110 curdir = dir
111 111 #if curdir in self._paths:
112 112 ## This path have been already traversed
113 113 ## Update tree and continue
114 114 #tree = self.repository._repo[self._paths[curdir]]
115 115 #continue
116 116 dir_id = None
117 117 for item, stat, id in tree.iteritems():
118 118 if curdir:
119 119 item_path = '/'.join((curdir, item))
120 120 else:
121 121 item_path = item
122 122 self._paths[item_path] = id
123 123 self._stat_modes[item_path] = stat
124 124 if dir == item:
125 125 dir_id = id
126 126 if dir_id:
127 127 # Update tree
128 128 tree = self.repository._repo[dir_id]
129 129 if not isinstance(tree, objects.Tree):
130 130 raise ChangesetError('%s is not a directory' % curdir)
131 131 else:
132 132 raise ChangesetError('%s have not been found' % curdir)
133 133 for item, stat, id in tree.iteritems():
134 134 if curdir:
135 135 name = '/'.join((curdir, item))
136 136 else:
137 137 name = item
138 138 self._paths[name] = id
139 139 self._stat_modes[name] = stat
140 140 if not path in self._paths:
141 141 raise NodeDoesNotExistError("There is no file nor directory "
142 142 "at the given path %r at revision %r"
143 143 % (path, self.short_id))
144 144 return self._paths[path]
145 145
146 146 def _get_kind(self, path):
147 147 id = self._get_id_for_path(path)
148 148 obj = self.repository._repo[id]
149 149 if isinstance(obj, objects.Blob):
150 150 return NodeKind.FILE
151 151 elif isinstance(obj, objects.Tree):
152 152 return NodeKind.DIR
153 153
154 154 def _get_file_nodes(self):
155 155 return chain(*(t[2] for t in self.walk()))
156 156
157 157 @LazyProperty
158 158 def parents(self):
159 159 """
160 160 Returns list of parents changesets.
161 161 """
162 162 return [self.repository.get_changeset(parent)
163 163 for parent in self._commit.parents]
164 164
165 165 def next(self, branch=None):
166 166
167 167 if branch and self.branch != branch:
168 168 raise VCSError('Branch option used on changeset not belonging '
169 169 'to that branch')
170 170
171 171 def _next(changeset, branch):
172 172 try:
173 173 next_ = changeset.revision + 1
174 174 next_rev = changeset.repository.revisions[next_]
175 175 except IndexError:
176 176 raise ChangesetDoesNotExistError
177 177 cs = changeset.repository.get_changeset(next_rev)
178 178
179 179 if branch and branch != cs.branch:
180 180 return _next(cs, branch)
181 181
182 182 return cs
183 183
184 184 return _next(self, branch)
185 185
186 186 def prev(self, branch=None):
187 187 if branch and self.branch != branch:
188 188 raise VCSError('Branch option used on changeset not belonging '
189 189 'to that branch')
190 190
191 191 def _prev(changeset, branch):
192 192 try:
193 193 prev_ = changeset.revision - 1
194 194 if prev_ < 0:
195 195 raise IndexError
196 196 prev_rev = changeset.repository.revisions[prev_]
197 197 except IndexError:
198 198 raise ChangesetDoesNotExistError
199 199
200 200 cs = changeset.repository.get_changeset(prev_rev)
201 201
202 202 if branch and branch != cs.branch:
203 203 return _prev(cs, branch)
204 204
205 205 return cs
206 206
207 207 return _prev(self, branch)
208 208
209 209 def get_file_mode(self, path):
210 210 """
211 211 Returns stat mode of the file at the given ``path``.
212 212 """
213 213 # ensure path is traversed
214 214 self._get_id_for_path(path)
215 215 return self._stat_modes[path]
216 216
217 217 def get_file_content(self, path):
218 218 """
219 219 Returns content of the file at given ``path``.
220 220 """
221 221 id = self._get_id_for_path(path)
222 222 blob = self.repository._repo[id]
223 223 return blob.as_pretty_string()
224 224
225 225 def get_file_size(self, path):
226 226 """
227 227 Returns size of the file at given ``path``.
228 228 """
229 229 id = self._get_id_for_path(path)
230 230 blob = self.repository._repo[id]
231 231 return blob.raw_length()
232 232
233 233 def get_file_changeset(self, path):
234 234 """
235 235 Returns last commit of the file at the given ``path``.
236 236 """
237 237 node = self.get_node(path)
238 238 return node.history[0]
239 239
240 240 def get_file_history(self, path):
241 241 """
242 242 Returns history of file as reversed list of ``Changeset`` objects for
243 243 which file at given ``path`` has been modified.
244 244
245 245 TODO: This function now uses os underlying 'git' and 'grep' commands
246 246 which is generally not good. Should be replaced with algorithm
247 247 iterating commits.
248 248 """
249 cmd = 'log --name-status -p %s -- "%s" | grep "^commit"' \
250 % (self.id, path)
249 cmd = 'log --pretty="format: %%H" --name-status -p %s -- "%s"' % (
250 '', path
251 )
251 252 so, se = self.repository.run_git_command(cmd)
252 253 ids = re.findall(r'\w{40}', so)
253 254 return [self.repository.get_changeset(id) for id in ids]
254 255
255 256 def get_file_annotate(self, path):
256 257 """
257 258 Returns a list of three element tuples with lineno,changeset and line
258 259
259 260 TODO: This function now uses os underlying 'git' command which is
260 261 generally not good. Should be replaced with algorithm iterating
261 262 commits.
262 263 """
263 264 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
264 265 # -l ==> outputs long shas (and we need all 40 characters)
265 266 # --root ==> doesn't put '^' character for bounderies
266 267 # -r sha ==> blames for the given revision
267 268 so, se = self.repository.run_git_command(cmd)
268 269 annotate = []
269 270 for i, blame_line in enumerate(so.split('\n')[:-1]):
270 271 ln_no = i + 1
271 272 id, line = re.split(r' \(.+?\) ', blame_line, 1)
272 273 annotate.append((ln_no, self.repository.get_changeset(id), line))
273 274 return annotate
274 275
275 276 def fill_archive(self, stream=None, kind='tgz', prefix=None,
276 277 subrepos=False):
277 278 """
278 279 Fills up given stream.
279 280
280 281 :param stream: file like object.
281 282 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
282 283 Default: ``tgz``.
283 284 :param prefix: name of root directory in archive.
284 285 Default is repository name and changeset's raw_id joined with dash
285 286 (``repo-tip.<KIND>``).
286 287 :param subrepos: include subrepos in this archive.
287 288
288 289 :raise ImproperArchiveTypeError: If given kind is wrong.
289 290 :raise VcsError: If given stream is None
290 291
291 292 """
292 293 allowed_kinds = settings.ARCHIVE_SPECS.keys()
293 294 if kind not in allowed_kinds:
294 295 raise ImproperArchiveTypeError('Archive kind not supported use one'
295 296 'of %s', allowed_kinds)
296 297
297 298 if prefix is None:
298 299 prefix = '%s-%s' % (self.repository.name, self.short_id)
299 300 elif prefix.startswith('/'):
300 301 raise VCSError("Prefix cannot start with leading slash")
301 302 elif prefix.strip() == '':
302 303 raise VCSError("Prefix cannot be empty")
303 304
304 305 if kind == 'zip':
305 306 frmt = 'zip'
306 307 else:
307 308 frmt = 'tar'
308 309 cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
309 310 self.raw_id)
310 311 if kind == 'tgz':
311 312 cmd += ' | gzip -9'
312 313 elif kind == 'tbz2':
313 314 cmd += ' | bzip2 -9'
314 315
315 316 if stream is None:
316 317 raise VCSError('You need to pass in a valid stream for filling'
317 318 ' with archival data')
318 319 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
319 320 cwd=self.repository.path)
320 321
321 322 buffer_size = 1024 * 8
322 323 chunk = popen.stdout.read(buffer_size)
323 324 while chunk:
324 325 stream.write(chunk)
325 326 chunk = popen.stdout.read(buffer_size)
326 327 # Make sure all descriptors would be read
327 328 popen.communicate()
328 329
329 330 def get_nodes(self, path):
330 331 if self._get_kind(path) != NodeKind.DIR:
331 332 raise ChangesetError("Directory does not exist for revision %r at "
332 333 " %r" % (self.revision, path))
333 334 path = self._fix_path(path)
334 335 id = self._get_id_for_path(path)
335 336 tree = self.repository._repo[id]
336 337 dirnodes = []
337 338 filenodes = []
338 339 for name, stat, id in tree.iteritems():
339 340 obj = self.repository._repo.get_object(id)
340 341 if path != '':
341 342 obj_path = '/'.join((path, name))
342 343 else:
343 344 obj_path = name
344 345 if obj_path not in self._stat_modes:
345 346 self._stat_modes[obj_path] = stat
346 347 if isinstance(obj, objects.Tree):
347 348 dirnodes.append(DirNode(obj_path, changeset=self))
348 349 elif isinstance(obj, objects.Blob):
349 350 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
350 351 else:
351 352 raise ChangesetError("Requested object should be Tree "
352 353 "or Blob, is %r" % type(obj))
353 354 nodes = dirnodes + filenodes
354 355 for node in nodes:
355 356 if not node.path in self.nodes:
356 357 self.nodes[node.path] = node
357 358 nodes.sort()
358 359 return nodes
359 360
360 361 def get_node(self, path):
361 362 if isinstance(path, unicode):
362 363 path = path.encode('utf-8')
363 364 path = self._fix_path(path)
364 365 if not path in self.nodes:
365 366 try:
366 367 id = self._get_id_for_path(path)
367 368 except ChangesetError:
368 369 raise NodeDoesNotExistError("Cannot find one of parents' "
369 370 "directories for a given path: %s" % path)
370 371 obj = self.repository._repo.get_object(id)
371 372 if isinstance(obj, objects.Tree):
372 373 if path == '':
373 374 node = RootNode(changeset=self)
374 375 else:
375 376 node = DirNode(path, changeset=self)
376 377 node._tree = obj
377 378 elif isinstance(obj, objects.Blob):
378 379 node = FileNode(path, changeset=self)
379 380 node._blob = obj
380 381 else:
381 382 raise NodeDoesNotExistError("There is no file nor directory "
382 383 "at the given path %r at revision %r"
383 384 % (path, self.short_id))
384 385 # cache node
385 386 self.nodes[path] = node
386 387 return self.nodes[path]
387 388
388 389 @LazyProperty
389 390 def affected_files(self):
390 391 """
391 392 Get's a fast accessible file changes for given changeset
392 393 """
393 394
394 395 return self.added + self.changed
395 396
396 397 @LazyProperty
397 398 def _diff_name_status(self):
398 399 output = []
399 400 for parent in self.parents:
400 401 cmd = 'diff --name-status %s %s' % (parent.raw_id, self.raw_id)
401 402 so, se = self.repository.run_git_command(cmd)
402 403 output.append(so.strip())
403 404 return '\n'.join(output)
404 405
405 406 def _get_paths_for_status(self, status):
406 407 """
407 408 Returns sorted list of paths for given ``status``.
408 409
409 410 :param status: one of: *added*, *modified* or *deleted*
410 411 """
411 412 paths = set()
412 413 char = status[0].upper()
413 414 for line in self._diff_name_status.splitlines():
414 415 if not line:
415 416 continue
416 417 if line.startswith(char):
417 418 splitted = line.split(char,1)
418 419 if not len(splitted) == 2:
419 420 raise VCSError("Couldn't parse diff result:\n%s\n\n and "
420 421 "particularly that line: %s" % (self._diff_name_status,
421 422 line))
422 423 paths.add(splitted[1].strip())
423 424 return sorted(paths)
424 425
425 426 @LazyProperty
426 427 def added(self):
427 428 """
428 429 Returns list of added ``FileNode`` objects.
429 430 """
430 431 if not self.parents:
431 432 return list(self._get_file_nodes())
432 433 return [self.get_node(path) for path in self._get_paths_for_status('added')]
433 434
434 435 @LazyProperty
435 436 def changed(self):
436 437 """
437 438 Returns list of modified ``FileNode`` objects.
438 439 """
439 440 if not self.parents:
440 441 return []
441 442 return [self.get_node(path) for path in self._get_paths_for_status('modified')]
442 443
443 444 @LazyProperty
444 445 def removed(self):
445 446 """
446 447 Returns list of removed ``FileNode`` objects.
447 448 """
448 449 if not self.parents:
449 450 return []
450 451 return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')]
@@ -1,508 +1,507 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 50 try:
51 51 self.head = self._repo.head()
52 52 except KeyError:
53 53 self.head = None
54 54
55 55 self._config_files = [
56 56 bare and abspath(self.path, 'config') or abspath(self.path, '.git',
57 57 'config'),
58 58 abspath(get_user_home(), '.gitconfig'),
59 59 ]
60 60
61 61 @LazyProperty
62 62 def revisions(self):
63 63 """
64 64 Returns list of revisions' ids, in ascending order. Being lazy
65 65 attribute allows external tools to inject shas from cache.
66 66 """
67 67 return self._get_all_revisions()
68 68
69 69 def run_git_command(self, cmd):
70 70 """
71 71 Runs given ``cmd`` as git command and returns tuple
72 72 (returncode, stdout, stderr).
73 73
74 74 .. note::
75 75 This method exists only until log/blame functionality is implemented
76 76 at Dulwich (see https://bugs.launchpad.net/bugs/645142). Parsing
77 77 os command's output is road to hell...
78 78
79 79 :param cmd: git command to be executed
80 80 """
81 81 #cmd = '(cd %s && git %s)' % (self.path, cmd)
82 82 if isinstance(cmd, basestring):
83 83 cmd = 'git %s' % cmd
84 84 else:
85 85 cmd = ['git'] + cmd
86 86 try:
87 87 opts = dict(
88 88 shell=isinstance(cmd, basestring),
89 89 stdout=PIPE,
90 90 stderr=PIPE)
91 91 if os.path.isdir(self.path):
92 92 opts['cwd'] = self.path
93 93 p = Popen(cmd, **opts)
94 94 except OSError, err:
95 95 raise RepositoryError("Couldn't run git command (%s).\n"
96 96 "Original error was:%s" % (cmd, err))
97 97 so, se = p.communicate()
98 98 if not se.startswith("fatal: bad default revision 'HEAD'") and \
99 99 p.returncode != 0:
100 100 raise RepositoryError("Couldn't run git command (%s).\n"
101 101 "stderr:\n%s" % (cmd, se))
102 102 return so, se
103 103
104 104 def _check_url(self, url):
105 105 """
106 106 Functon will check given url and try to verify if it's a valid
107 107 link. Sometimes it may happened that mercurial will issue basic
108 108 auth request that can cause whole API to hang when used from python
109 109 or other external calls.
110 110
111 111 On failures it'll raise urllib2.HTTPError
112 112 """
113 113
114 114 #TODO: implement this
115 115 pass
116 116
117 117 def _get_repo(self, create, src_url=None, update_after_clone=False,
118 118 bare=False):
119 119 if create and os.path.exists(self.path):
120 120 raise RepositoryError("Location already exist")
121 121 if src_url and not create:
122 122 raise RepositoryError("Create should be set to True if src_url is "
123 123 "given (clone operation creates repository)")
124 124 try:
125 125 if create and src_url:
126 126 self._check_url(src_url)
127 127 self.clone(src_url, update_after_clone, bare)
128 128 return Repo(self.path)
129 129 elif create:
130 130 os.mkdir(self.path)
131 131 if bare:
132 132 return Repo.init_bare(self.path)
133 133 else:
134 134 return Repo.init(self.path)
135 135 else:
136 136 return Repo(self.path)
137 137 except (NotGitRepository, OSError), err:
138 138 raise RepositoryError(err)
139 139
140 140 def _get_all_revisions(self):
141 141 cmd = 'rev-list --all --date-order'
142 142 try:
143 143 so, se = self.run_git_command(cmd)
144 144 except RepositoryError:
145 145 # Can be raised for empty repositories
146 146 return []
147 147 revisions = so.splitlines()
148 148 revisions.reverse()
149 149 return revisions
150 150
151 151 def _get_revision(self, revision):
152 152 """
153 153 For git backend we always return integer here. This way we ensure
154 154 that changset's revision attribute would become integer.
155 155 """
156 156 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
157 157 is_bstr = lambda o: isinstance(o, (str, unicode))
158 158 is_null = lambda o: len(o) == revision.count('0')
159 159
160 160 if len(self.revisions) == 0:
161 161 raise EmptyRepositoryError("There are no changesets yet")
162 162
163 163 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
164 164 revision = self.revisions[-1]
165 165
166 166 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
167 167 or isinstance(revision, int) or is_null(revision)):
168 168 try:
169 169 revision = self.revisions[int(revision)]
170 170 except:
171 171 raise ChangesetDoesNotExistError("Revision %r does not exist "
172 172 "for this repository %s" % (revision, self))
173 173
174 174 elif is_bstr(revision):
175 175 if not pattern.match(revision) or revision not in self.revisions:
176 176 raise ChangesetDoesNotExistError("Revision %r does not exist "
177 177 "for this repository %s" % (revision, self))
178 178
179 179 # Ensure we return full id
180 180 if not pattern.match(str(revision)):
181 181 raise ChangesetDoesNotExistError("Given revision %r not recognized"
182 182 % revision)
183 183 return revision
184 184
185 185 def _get_archives(self, archive_name='tip'):
186 186
187 187 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
188 188 yield {"type": i[0], "extension": i[1], "node": archive_name}
189 189
190 190 def _get_url(self, url):
191 191 """
192 192 Returns normalized url. If schema is not given, would fall to
193 193 filesystem (``file:///``) schema.
194 194 """
195 195 url = str(url)
196 196 if url != 'default' and not '://' in url:
197 197 url = ':///'.join(('file', url))
198 198 return url
199 199
200 200 @LazyProperty
201 201 def name(self):
202 202 return os.path.basename(self.path)
203 203
204 204 @LazyProperty
205 205 def last_change(self):
206 206 """
207 207 Returns last change made on this repository as datetime object
208 208 """
209 209 return date_fromtimestamp(self._get_mtime(), makedate()[1])
210 210
211 211 def _get_mtime(self):
212 212 try:
213 213 return time.mktime(self.get_changeset().date.timetuple())
214 214 except RepositoryError:
215 215 # fallback to filesystem
216 216 in_path = os.path.join(self.path, '.git', "index")
217 217 he_path = os.path.join(self.path, '.git', "HEAD")
218 218 if os.path.exists(in_path):
219 219 return os.stat(in_path).st_mtime
220 220 else:
221 221 return os.stat(he_path).st_mtime
222 222
223 223 @LazyProperty
224 224 def description(self):
225 225 undefined_description = u'unknown'
226 226 description_path = os.path.join(self.path, '.git', 'description')
227 227 if os.path.isfile(description_path):
228 228 return safe_unicode(open(description_path).read())
229 229 else:
230 230 return undefined_description
231 231
232 232 @LazyProperty
233 233 def contact(self):
234 234 undefined_contact = u'Unknown'
235 235 return undefined_contact
236 236
237 237 @property
238 238 def branches(self):
239 239 if not self.revisions:
240 240 return {}
241 241 refs = self._repo.refs.as_dict()
242 242 sortkey = lambda ctx: ctx[0]
243 243 _branches = [('/'.join(ref.split('/')[2:]), head)
244 244 for ref, head in refs.items()
245 if ref.startswith('refs/heads/') or
246 ref.startswith('refs/remotes/') and not ref.endswith('/HEAD')]
245 if ref.startswith('refs/heads/') and not ref.endswith('/HEAD')]
247 246 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
248 247
249 248 def _get_tags(self):
250 249 if not self.revisions:
251 250 return {}
252 251 sortkey = lambda ctx: ctx[0]
253 252 _tags = [('/'.join(ref.split('/')[2:]), head) for ref, head in
254 253 self._repo.get_refs().items() if ref.startswith('refs/tags/')]
255 254 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
256 255
257 256 @LazyProperty
258 257 def tags(self):
259 258 return self._get_tags()
260 259
261 260 def tag(self, name, user, revision=None, message=None, date=None,
262 261 **kwargs):
263 262 """
264 263 Creates and returns a tag for the given ``revision``.
265 264
266 265 :param name: name for new tag
267 266 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
268 267 :param revision: changeset id for which new tag would be created
269 268 :param message: message of the tag's commit
270 269 :param date: date of tag's commit
271 270
272 271 :raises TagAlreadyExistError: if tag with same name already exists
273 272 """
274 273 if name in self.tags:
275 274 raise TagAlreadyExistError("Tag %s already exists" % name)
276 275 changeset = self.get_changeset(revision)
277 276 message = message or "Added tag %s for commit %s" % (name,
278 277 changeset.raw_id)
279 278 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
280 279
281 280 self.tags = self._get_tags()
282 281 return changeset
283 282
284 283 def remove_tag(self, name, user, message=None, date=None):
285 284 """
286 285 Removes tag with the given ``name``.
287 286
288 287 :param name: name of the tag to be removed
289 288 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
290 289 :param message: message of the tag's removal commit
291 290 :param date: date of tag's removal commit
292 291
293 292 :raises TagDoesNotExistError: if tag with given name does not exists
294 293 """
295 294 if name not in self.tags:
296 295 raise TagDoesNotExistError("Tag %s does not exist" % name)
297 296 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
298 297 try:
299 298 os.remove(tagpath)
300 299 self.tags = self._get_tags()
301 300 except OSError, e:
302 301 raise RepositoryError(e.strerror)
303 302
304 303 def get_changeset(self, revision=None):
305 304 """
306 305 Returns ``GitChangeset`` object representing commit from git repository
307 306 at the given revision or head (most recent commit) if None given.
308 307 """
309 308 if isinstance(revision, GitChangeset):
310 309 return revision
311 310 revision = self._get_revision(revision)
312 311 changeset = GitChangeset(repository=self, revision=revision)
313 312 return changeset
314 313
315 314 def get_changesets(self, start=None, end=None, start_date=None,
316 315 end_date=None, branch_name=None, reverse=False):
317 316 """
318 317 Returns iterator of ``GitChangeset`` objects from start to end (both
319 318 are inclusive), in ascending date order (unless ``reverse`` is set).
320 319
321 320 :param start: changeset ID, as str; first returned changeset
322 321 :param end: changeset ID, as str; last returned changeset
323 322 :param start_date: if specified, changesets with commit date less than
324 323 ``start_date`` would be filtered out from returned set
325 324 :param end_date: if specified, changesets with commit date greater than
326 325 ``end_date`` would be filtered out from returned set
327 326 :param branch_name: if specified, changesets not reachable from given
328 327 branch would be filtered out from returned set
329 328 :param reverse: if ``True``, returned generator would be reversed
330 329 (meaning that returned changesets would have descending date order)
331 330
332 331 :raise BranchDoesNotExistError: If given ``branch_name`` does not
333 332 exist.
334 333 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
335 334 ``end`` could not be found.
336 335
337 336 """
338 337 if branch_name and branch_name not in self.branches:
339 338 raise BranchDoesNotExistError("Branch '%s' not found" \
340 339 % branch_name)
341 340 # %H at format means (full) commit hash, initial hashes are retrieved
342 341 # in ascending date order
343 342 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
344 343 cmd_params = {}
345 344 if start_date:
346 345 cmd_template += ' --since "$since"'
347 346 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
348 347 if end_date:
349 348 cmd_template += ' --until "$until"'
350 349 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
351 350 if branch_name:
352 351 cmd_template += ' $branch_name'
353 352 cmd_params['branch_name'] = branch_name
354 353 else:
355 354 cmd_template += ' --all'
356 355
357 356 cmd = Template(cmd_template).safe_substitute(**cmd_params)
358 357 revs = self.run_git_command(cmd)[0].splitlines()
359 358 start_pos = 0
360 359 end_pos = len(revs)
361 360 if start:
362 361 _start = self._get_revision(start)
363 362 try:
364 363 start_pos = revs.index(_start)
365 364 except ValueError:
366 365 pass
367 366
368 367 if end is not None:
369 368 _end = self._get_revision(end)
370 369 try:
371 370 end_pos = revs.index(_end)
372 371 except ValueError:
373 372 pass
374 373
375 374 if None not in [start, end] and start_pos > end_pos:
376 375 raise RepositoryError('start cannot be after end')
377 376
378 377 if end_pos is not None:
379 378 end_pos += 1
380 379
381 380 revs = revs[start_pos:end_pos]
382 381 if reverse:
383 382 revs = reversed(revs)
384 383 for rev in revs:
385 384 yield self.get_changeset(rev)
386 385
387 386 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
388 387 context=3):
389 388 """
390 389 Returns (git like) *diff*, as plain text. Shows changes introduced by
391 390 ``rev2`` since ``rev1``.
392 391
393 392 :param rev1: Entry point from which diff is shown. Can be
394 393 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
395 394 the changes since empty state of the repository until ``rev2``
396 395 :param rev2: Until which revision changes should be shown.
397 396 :param ignore_whitespace: If set to ``True``, would not show whitespace
398 397 changes. Defaults to ``False``.
399 398 :param context: How many lines before/after changed lines should be
400 399 shown. Defaults to ``3``.
401 400 """
402 401 flags = ['-U%s' % context]
403 402 if ignore_whitespace:
404 403 flags.append('-w')
405 404
406 405 if rev1 == self.EMPTY_CHANGESET:
407 406 rev2 = self.get_changeset(rev2).raw_id
408 407 cmd = ' '.join(['show'] + flags + [rev2])
409 408 else:
410 409 rev1 = self.get_changeset(rev1).raw_id
411 410 rev2 = self.get_changeset(rev2).raw_id
412 411 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
413 412
414 413 if path:
415 414 cmd += ' -- "%s"' % path
416 415 stdout, stderr = self.run_git_command(cmd)
417 416 # If we used 'show' command, strip first few lines (until actual diff
418 417 # starts)
419 418 if rev1 == self.EMPTY_CHANGESET:
420 419 lines = stdout.splitlines()
421 420 x = 0
422 421 for line in lines:
423 422 if line.startswith('diff'):
424 423 break
425 424 x += 1
426 425 # Append new line just like 'diff' command do
427 426 stdout = '\n'.join(lines[x:]) + '\n'
428 427 return stdout
429 428
430 429 @LazyProperty
431 430 def in_memory_changeset(self):
432 431 """
433 432 Returns ``GitInMemoryChangeset`` object for this repository.
434 433 """
435 434 return GitInMemoryChangeset(self)
436 435
437 436 def clone(self, url, update_after_clone=True, bare=False):
438 437 """
439 438 Tries to clone changes from external location.
440 439
441 440 :param update_after_clone: If set to ``False``, git won't checkout
442 441 working directory
443 442 :param bare: If set to ``True``, repository would be cloned into
444 443 *bare* git repository (no working directory at all).
445 444 """
446 445 url = self._get_url(url)
447 446 cmd = ['clone']
448 447 if bare:
449 448 cmd.append('--bare')
450 449 elif not update_after_clone:
451 450 cmd.append('--no-checkout')
452 451 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
453 452 cmd = ' '.join(cmd)
454 453 # If error occurs run_git_command raises RepositoryError already
455 454 self.run_git_command(cmd)
456 455
457 456 @LazyProperty
458 457 def workdir(self):
459 458 """
460 459 Returns ``Workdir`` instance for this repository.
461 460 """
462 461 return GitWorkdir(self)
463 462
464 463 def get_config_value(self, section, name, config_file=None):
465 464 """
466 465 Returns configuration value for a given [``section``] and ``name``.
467 466
468 467 :param section: Section we want to retrieve value from
469 468 :param name: Name of configuration we want to retrieve
470 469 :param config_file: A path to file which should be used to retrieve
471 470 configuration from (might also be a list of file paths)
472 471 """
473 472 if config_file is None:
474 473 config_file = []
475 474 elif isinstance(config_file, basestring):
476 475 config_file = [config_file]
477 476
478 477 def gen_configs():
479 478 for path in config_file + self._config_files:
480 479 try:
481 480 yield ConfigFile.from_path(path)
482 481 except (IOError, OSError, ValueError):
483 482 continue
484 483
485 484 for config in gen_configs():
486 485 try:
487 486 return config.get(section, name)
488 487 except KeyError:
489 488 continue
490 489 return None
491 490
492 491 def get_user_name(self, config_file=None):
493 492 """
494 493 Returns user's name from global configuration file.
495 494
496 495 :param config_file: A path to file which should be used to retrieve
497 496 configuration from (might also be a list of file paths)
498 497 """
499 498 return self.get_config_value('user', 'name', config_file)
500 499
501 500 def get_user_email(self, config_file=None):
502 501 """
503 502 Returns user's email from global configuration file.
504 503
505 504 :param config_file: A path to file which should be used to retrieve
506 505 configuration from (might also be a list of file paths)
507 506 """
508 507 return self.get_config_value('user', 'email', config_file)
General Comments 0
You need to be logged in to leave comments. Login now