Changeset - 09fd90d91b6a
docs/changelog.rst
Show inline comments
 
.. _changelog:
 

	
 
=========
 
Changelog
 
=========
 

	
 
1.3.5 (**2012-XX-XX**)
 
----------------------
 

	
 
:status: in-progress
 
:branch: beta
 

	
 
news
 
++++
 

	
 
- use ext_json for json module
 
- unified annotation view with file source view
 
- notification improvements, better inbox + css
 
- #419 don't strip passwords for login forms, make rhodecode 
 
  more compatible with LDAP servers
 
- Added HTTP_X_FORWARDED_FOR as another method of extracting 
 
  IP for pull/push logs. - moved all to base controller  
 
- #415: Adding comment to changeset causes reload. 
 
  Comments are now added via ajax and doesn't reload the page
 
- #374 LDAP config is discarded when LDAP can't be activated
 
- limited push/pull operations are now logged for git in the journal
 
- bumped mercurial to 2.2.X series
 
- added support for displaying submodules in file-browser
 

	
 
fixes
 
+++++
 

	
 
- fixed dev-version marker for stable when served from source codes
 
- fixed missing permission checks on show forks page
 
- #418 cast to unicode fixes in notification objects
 
- #426 fixed mention extracting regex
 
- fixed remote-pulling for git remotes remopositories
 
- fixed #434: Error when accessing files or changesets of a git repository 
 
  with submodules
 

	
 
1.3.4 (**2012-03-28**)
 
----------------------
 

	
 
news
 
++++
 

	
 
- Whoosh logging is now controlled by the .ini files logging setup
 
- added clone-url into edit form on /settings page
 
- added help text into repo add/edit forms
 
- created rcextensions module with additional mappings (ref #322) and
 
  post push/pull/create repo hooks callbacks
 
- implemented #377 Users view for his own permissions on account page
 
- #399 added inheritance of permissions for users group on repos groups
 
- #401 repository group is automatically pre-selected when adding repos 
 
  inside a repository group
 
- added alternative HTTP 403 response when client failed to authenticate. Helps 
 
  solving issues with Mercurial and LDAP
 
- #402 removed group prefix from repository name when listing repositories 
 
  inside a group
 
- added gravatars into permission view and permissions autocomplete
 
- #347 when running multiple RhodeCode instances, properly invalidates cache 
 
  for all registered servers
 

	
 
fixes
 
+++++
 

	
 
- fixed #390 cache invalidation problems on repos inside group
 
- fixed #385 clone by ID url was loosing proxy prefix in URL
 
- fixed some unicode problems with waitress
 
- fixed issue with escaping < and > in changeset commits
 
- fixed error occurring during recursive group creation in API 
 
  create_repo function
 
- fixed #393 py2.5 fixes for routes url generator
 
- fixed #397 Private repository groups shows up before login
 
- fixed #396 fixed problems with revoking users in nested groups
 
- fixed mysql unicode issues + specified InnoDB as default engine with 
 
  utf8 charset
 
- #406 trim long branch/tag names in changelog to not break UI
 
  
 
1.3.3 (**2012-03-02**)
 
----------------------
 

	
 
news
 
++++
 

	
 

	
 
fixes
 
+++++
 

	
 
- fixed some python2.5 compatibility issues 
 
- fixed issues with removed repos was accidentally added as groups, after
 
  full rescan of paths
 
- fixes #376 Cannot edit user (using container auth)
 
- fixes #378 Invalid image urls on changeset screen with proxy-prefix 
 
  configuration
 
- fixed initial sorting of repos inside repo group
 
- fixes issue when user tried to resubmit same permission into user/user_groups
 
- bumped beaker version that fixes #375 leap error bug
 
- fixed raw_changeset for git. It was generated with hg patch headers
 
- fixed vcs issue with last_changeset for filenodes
 
- fixed missing commit after hook delete
 
- fixed #372 issues with git operation detection that caused a security issue 
 
  for git repos
 

	
 
1.3.2 (**2012-02-28**)
 
----------------------
 

	
 
news
 
++++
 

	
 

	
 
fixes
 
+++++
 

	
 
- fixed git protocol issues with repos-groups
 
- fixed git remote repos validator that prevented from cloning remote git repos
 
- fixes #370 ending slashes fixes for repo and groups
 
- fixes #368 improved git-protocol detection to handle other clients
 
- fixes #366 When Setting Repository Group To Blank Repo Group Wont Be 
 
  Moved To Root
 
- fixes #371 fixed issues with beaker/sqlalchemy and non-ascii cache keys 
 
- fixed #373 missing cascade drop on user_group_to_perm table
 

	
 
1.3.1 (**2012-02-27**)
 
----------------------
 

	
 
news
 
++++
 

	
 

	
 
fixes
 
+++++
 

	
 
- redirection loop occurs when remember-me wasn't checked during login
 
- fixes issues with git blob history generation 
 
- don't fetch branch for git in file history dropdown. Causes unneeded slowness
 

	
 
1.3.0 (**2012-02-26**)
 
----------------------
 

	
 
news
 
++++
 

	
 
- code review, inspired by github code-comments 
 
- #215 rst and markdown README files support
 
- #252 Container-based and proxy pass-through authentication support
 
- #44 branch browser. Filtering of changelog by branches
 
- mercurial bookmarks support
 
- new hover top menu, optimized to add maximum size for important views
 
- configurable clone url template with possibility to specify  protocol like 
 
  ssh:// or http:// and also manually alter other parts of clone_url.
 
- enabled largefiles extension by default
 
- optimized summary file pages and saved a lot of unused space in them
 
- #239 option to manually mark repository as fork
 
- #320 mapping of commit authors to RhodeCode users
 
- #304 hashes are displayed using monospace font    
 
- diff configuration, toggle white lines and context lines
 
- #307 configurable diffs, whitespace toggle, increasing context lines
 
- sorting on branches, tags and bookmarks using YUI datatable
 
- improved file filter on files page
 
- implements #330 api method for listing nodes ar particular revision
 
- #73 added linking issues in commit messages to chosen issue tracker url
 
  based on user defined regular expression
 
- added linking of changesets in commit messages  
 
- new compact changelog with expandable commit messages
 
- firstname and lastname are optional in user creation
 
- #348 added post-create repository hook
 
- #212 global encoding settings is now configurable from .ini files 
 
- #227 added repository groups permissions
 
- markdown gets codehilite extensions
 
- new API methods, delete_repositories, grante/revoke permissions for groups 
 
  and repos
 
  
 
    
 
fixes
 
+++++
 

	
 
- rewrote dbsession management for atomic operations, and better error handling
 
- fixed sorting of repo tables
 
- #326 escape of special html entities in diffs
 
- normalized user_name => username in api attributes
 
- fixes #298 ldap created users with mixed case emails created conflicts 
 
  on saving a form
 
- fixes issue when owner of a repo couldn't revoke permissions for users 
 
  and groups
 
- fixes #271 rare JSON serialization problem with statistics
 
- fixes #337 missing validation check for conflicting names of a group with a
 
  repositories group
 
- #340 fixed session problem for mysql and celery tasks
 
- fixed #331 RhodeCode mangles repository names if the a repository group 
 
  contains the "full path" to the repositories
 
- #355 RhodeCode doesn't store encrypted LDAP passwords
 

	
 
1.2.5 (**2012-01-28**)
 
----------------------
 

	
 
news
 
++++
 

	
 
fixes
 
+++++
 

	
 
- #340 Celery complains about MySQL server gone away, added session cleanup
 
  for celery tasks
 
- #341 "scanning for repositories in None" log message during Rescan was missing
 
  a parameter
 
- fixed creating archives with subrepos. Some hooks were triggered during that
 
  operation leading to crash.
 
- fixed missing email in account page.
 
- Reverted Mercurial to 2.0.1 for windows due to bug in Mercurial that makes
 
  forking on windows impossible 
 

	
 
1.2.4 (**2012-01-19**)
 
----------------------
 

	
 
news
 
++++
 

	
 
- RhodeCode is bundled with mercurial series 2.0.X by default, with
 
  full support to largefiles extension. Enabled by default in new installations
 
- #329 Ability to Add/Remove Groups to/from a Repository via AP
 
- added requires.txt file with requirements
 
     
 
fixes
 
+++++
 

	
 
- fixes db session issues with celery when emailing admins
 
- #331 RhodeCode mangles repository names if the a repository group 
 
  contains the "full path" to the repositories
 
- #298 Conflicting e-mail addresses for LDAP and RhodeCode users
 
- DB session cleanup after hg protocol operations, fixes issues with
 
  `mysql has gone away` errors
 
- #333 doc fixes for get_repo api function
 
- #271 rare JSON serialization problem with statistics enabled
 
- #337 Fixes issues with validation of repository name conflicting with 
 
  a group name. A proper message is now displayed.
 
- #292 made ldap_dn in user edit readonly, to get rid of confusion that field
 
  doesn't work   
 
- #316 fixes issues with web description in hgrc files 
 

	
 
1.2.3 (**2011-11-02**)
 
----------------------
 

	
 
news
 
++++
 

	
 
- added option to manage repos group for non admin users
 
- added following API methods for get_users, create_user, get_users_groups, 
 
  get_users_group, create_users_group, add_user_to_users_groups, get_repos, 
 
  get_repo, create_repo, add_user_to_repo
 
- implements #237 added password confirmation for my account 
 
  and admin edit user.
 
- implements #291 email notification for global events are now sent to all
 
  administrator users, and global config email.
 
     
 
fixes
 
+++++
 

	
 
- added option for passing auth method for smtp mailer
 
- #276 issue with adding a single user with id>10 to usergroups
 
- #277 fixes windows LDAP settings in which missing values breaks the ldap auth 
 
- #288 fixes managing of repos in a group for non admin user
 

	
 
1.2.2 (**2011-10-17**)
 
----------------------
 

	
 
news
 
++++
 

	
 
- #226 repo groups are available by path instead of numerical id
 
 
 
fixes
 
+++++
 

	
 
- #259 Groups with the same name but with different parent group
 
- #260 Put repo in group, then move group to another group -> repo becomes unavailable
 
- #258 RhodeCode 1.2 assumes egg folder is writable (lockfiles problems)
 
- #265 ldap save fails sometimes on converting attributes to booleans, 
 
  added getter and setter into model that will prevent from this on db model level
 
- fixed problems with timestamps issues #251 and #213
 
- fixes #266 RhodeCode allows to create repo with the same name and in 
 
  the same parent as group
 
- fixes #245 Rescan of the repositories on Windows
 
- fixes #248 cannot edit repos inside a group on windows
 
- fixes #219 forking problems on windows
 

	
 
1.2.1 (**2011-10-08**)
 
----------------------
 

	
 
news
 
++++
 

	
 

	
 
fixes
 
+++++
 

	
 
- fixed problems with basic auth and push problems 
 
- gui fixes
 
- fixed logger
 

	
 
1.2.0 (**2011-10-07**)
 
----------------------
 

	
 
news
 
++++
 

	
 
- implemented #47 repository groups
 
- implemented #89 Can setup google analytics code from settings menu
 
- implemented #91 added nicer looking archive urls with more download options
 
  like tags, branches
 
- implemented #44 into file browsing, and added follow branch option
 
- implemented #84 downloads can be enabled/disabled for each repository
 
- anonymous repository can be cloned without having to pass default:default
 
  into clone url
 
- fixed #90 whoosh indexer can index chooses repositories passed in command 
 
  line
 
- extended journal with day aggregates and paging
 
- implemented #107 source code lines highlight ranges
 
- implemented #93 customizable changelog on combined revision ranges - 
 
  equivalent of githubs compare view 
 
- implemented #108 extended and more powerful LDAP configuration
 
- implemented #56 users groups
 
- major code rewrites optimized codes for speed and memory usage
 
- raw and diff downloads are now in git format
 
- setup command checks for write access to given path
 
- fixed many issues with international characters and unicode. It uses utf8
 
  decode with replace to provide less errors even with non utf8 encoded strings
 
- #125 added API KEY access to feeds
 
- #109 Repository can be created from external Mercurial link (aka. remote 
 
  repository, and manually updated (via pull) from admin panel
 
- beta git support - push/pull server + basic view for git repos
 
- added followers page and forks page
 
- server side file creation (with binary file upload interface) 
 
  and edition with commits powered by codemirror 
 
- #111 file browser file finder, quick lookup files on whole file tree 
 
- added quick login sliding menu into main page
 
- changelog uses lazy loading of affected files details, in some scenarios 
 
  this can improve speed of changelog page dramatically especially for 
 
  larger repositories.
 
- implements #214 added support for downloading subrepos in download menu.
 
- Added basic API for direct operations on rhodecode via JSON
 
- Implemented advanced hook management
 

	
 
fixes
 
+++++
 

	
 
- fixed file browser bug, when switching into given form revision the url was 
 
  not changing
 
- fixed propagation to error controller on simplehg and simplegit middlewares
 
- fixed error when trying to make a download on empty repository
 
- fixed problem with '[' chars in commit messages in journal
 
- fixed #99 Unicode errors, on file node paths with non utf-8 characters
 
- journal fork fixes
 
- removed issue with space inside renamed repository after deletion
 
- fixed strange issue on formencode imports
 
- fixed #126 Deleting repository on Windows, rename used incompatible chars. 
 
- #150 fixes for errors on repositories mapped in db but corrupted in 
 
  filesystem
 
- fixed problem with ascendant characters in realm #181
 
- fixed problem with sqlite file based database connection pool
 
- whoosh indexer and code stats share the same dynamic extensions map
 
- fixes #188 - relationship delete of repo_to_perm entry on user removal
 
- fixes issue #189 Trending source files shows "show more" when no more exist
 
- fixes issue #197 Relative paths for pidlocks
 
- fixes issue #198 password will require only 3 chars now for login form
 
- fixes issue #199 wrong redirection for non admin users after creating a repository
 
- fixes issues #202, bad db constraint made impossible to attach same group 
 
  more than one time. Affects only mysql/postgres
 
- fixes #218 os.kill patch for windows was missing sig param
 
- improved rendering of dag (they are not trimmed anymore when number of 
 
  heads exceeds 5)
 
    
 
1.1.8 (**2011-04-12**)
 
----------------------
 

	
 
news
 
++++
 

	
 
- improved windows support
 

	
 
fixes
 
+++++
 

	
 
- fixed #140 freeze of python dateutil library, since new version is python2.x
 
  incompatible
 
- setup-app will check for write permission in given path
 
- cleaned up license info issue #149
 
- fixes for issues #137,#116 and problems with unicode and accented characters.
 
- fixes crashes on gravatar, when passed in email as unicode
 
- fixed tooltip flickering problems
 
- fixed came_from redirection on windows
 
- fixed logging modules, and sql formatters
 
- windows fixes for os.kill issue #133
 
- fixes path splitting for windows issues #148
 
- fixed issue #143 wrong import on migration to 1.1.X
 
- fixed problems with displaying binary files, thanks to Thomas Waldmann
 
- removed name from archive files since it's breaking ui for long repo names
 
- fixed issue with archive headers sent to browser, thanks to Thomas Waldmann
 
- fixed compatibility for 1024px displays, and larger dpi settings, thanks to 
 
  Thomas Waldmann
 
- fixed issue #166 summary pager was skipping 10 revisions on second page
 

	
 

	
 
1.1.7 (**2011-03-23**)
 
----------------------
 

	
 
news
 
++++
 

	
 
fixes
 
+++++
 

	
 
- fixed (again) #136 installation support for FreeBSD
 

	
 

	
 
1.1.6 (**2011-03-21**)
 
----------------------
 

	
 
news
 
++++
 

	
 
fixes
 
+++++
 

	
 
- fixed #136 installation support for FreeBSD
 
- RhodeCode will check for python version during installation
 

	
 
1.1.5 (**2011-03-17**)
 
----------------------
 

	
 
news
 
++++
 

	
 
- basic windows support, by exchanging pybcrypt into sha256 for windows only
 
  highly inspired by idea of mantis406
 

	
 
fixes
 
+++++
 

	
 
- fixed sorting by author in main page
 
- fixed crashes with diffs on binary files
 
- fixed #131 problem with boolean values for LDAP
 
- fixed #122 mysql problems thanks to striker69 
 
- fixed problem with errors on calling raw/raw_files/annotate functions 
 
  with unknown revisions
 
- fixed returned rawfiles attachment names with international character
 
- cleaned out docs, big thanks to Jason Harris
 

	
 
1.1.4 (**2011-02-19**)
 
----------------------
 

	
 
news
 
++++
 

	
 
fixes
 
+++++
 

	
 
- fixed formencode import problem on settings page, that caused server crash
 
  when that page was accessed as first after server start
 
- journal fixes
 
- fixed option to access repository just by entering http://server/<repo_name> 
 

	
 
1.1.3 (**2011-02-16**)
 
----------------------
 

	
 
news
 
++++
 

	
 
- implemented #102 allowing the '.' character in username
 
- added option to access repository just by entering http://server/<repo_name>
 
- celery task ignores result for better performance
 

	
 
fixes
 
+++++
 

	
 
- fixed ehlo command and non auth mail servers on smtp_lib. Thanks to 
 
  apollo13 and Johan Walles
 
- small fixes in journal
 
- fixed problems with getting setting for celery from .ini files
 
- registration, password reset and login boxes share the same title as main 
 
  application now
 
- fixed #113: to high permissions to fork repository
 
- fixed problem with '[' chars in commit messages in journal
 
- removed issue with space inside renamed repository after deletion
 
- db transaction fixes when filesystem repository creation failed
 
- fixed #106 relation issues on databases different than sqlite
 
- fixed static files paths links to use of url() method
 

	
 
1.1.2 (**2011-01-12**)
 
----------------------
 

	
 
news
 
++++
 

	
 

	
 
fixes
 
+++++
 

	
 
- fixes #98 protection against float division of percentage stats
 
- fixed graph bug
 
- forced webhelpers version since it was making troubles during installation 
 

	
 
1.1.1 (**2011-01-06**)
 
----------------------
 
 
 
news
 
++++
 

	
 
- added force https option into ini files for easier https usage (no need to
 
  set server headers with this options)
 
- small css updates
 

	
 
fixes
 
+++++
 

	
 
- fixed #96 redirect loop on files view on repositories without changesets
 
- fixed #97 unicode string passed into server header in special cases (mod_wsgi)
 
  and server crashed with errors
 
- fixed large tooltips problems on main page
 
- fixed #92 whoosh indexer is more error proof
 

	
 
1.1.0 (**2010-12-18**)
 
----------------------
 

	
 
news
 
++++
 

	
 
- rewrite of internals for vcs >=0.1.10
 
- uses mercurial 1.7 with dotencode disabled for maintaining compatibility 
 
  with older clients
 
- anonymous access, authentication via ldap
 
- performance upgrade for cached repos list - each repository has its own 
 
  cache that's invalidated when needed.
 
- performance upgrades on repositories with large amount of commits (20K+)
 
- main page quick filter for filtering repositories
 
- user dashboards with ability to follow chosen repositories actions
 
- sends email to admin on new user registration
 
- added cache/statistics reset options into repository settings
 
- more detailed action logger (based on hooks) with pushed changesets lists
 
  and options to disable those hooks from admin panel
 
- introduced new enhanced changelog for merges that shows more accurate results
 
- new improved and faster code stats (based on pygments lexers mapping tables, 
 
  showing up to 10 trending sources for each repository. Additionally stats
 
  can be disabled in repository settings.
 
- gui optimizations, fixed application width to 1024px
 
- added cut off (for large files/changesets) limit into config files
 
- whoosh, celeryd, upgrade moved to paster command
 
- other than sqlite database backends can be used
 

	
 
fixes
 
+++++
 

	
 
- fixes #61 forked repo was showing only after cache expired
 
- fixes #76 no confirmation on user deletes
 
- fixes #66 Name field misspelled
 
- fixes #72 block user removal when he owns repositories
 
- fixes #69 added password confirmation fields
 
- fixes #87 RhodeCode crashes occasionally on updating repository owner
 
- fixes #82 broken annotations on files with more than 1 blank line at the end
 
- a lot of fixes and tweaks for file browser
 
- fixed detached session issues
 
- fixed when user had no repos he would see all repos listed in my account
 
- fixed ui() instance bug when global hgrc settings was loaded for server 
 
  instance and all hgrc options were merged with our db ui() object
 
- numerous small bugfixes
 
 
 
(special thanks for TkSoh for detailed feedback)
 

	
 

	
 
1.0.2 (**2010-11-12**)
 
----------------------
 

	
 
news
 
++++
 

	
 
- tested under python2.7
 
- bumped sqlalchemy and celery versions
 

	
 
fixes
 
+++++
 

	
 
- fixed #59 missing graph.js
 
- fixed repo_size crash when repository had broken symlinks
 
- fixed python2.5 crashes.
 

	
 

	
 
1.0.1 (**2010-11-10**)
 
----------------------
 

	
 
news
 
++++
 

	
 
- small css updated
 

	
 
fixes
 
+++++
 

	
 
- fixed #53 python2.5 incompatible enumerate calls
 
- fixed #52 disable mercurial extension for web
 
- fixed #51 deleting repositories don't delete it's dependent objects
 

	
 

	
 
1.0.0 (**2010-11-02**)
 
----------------------
 

	
 
- security bugfix simplehg wasn't checking for permissions on commands
 
  other than pull or push.
 
- fixed doubled messages after push or pull in admin journal
 
- templating and css corrections, fixed repo switcher on chrome, updated titles
 
- admin menu accessible from options menu on repository view
 
- permissions cached queries
 

	
 
1.0.0rc4  (**2010-10-12**)
 
--------------------------
 

	
 
- fixed python2.5 missing simplejson imports (thanks to Jens Bäckman)
 
- removed cache_manager settings from sqlalchemy meta
 
- added sqlalchemy cache settings to ini files
 
- validated password length and added second try of failure on paster setup-app
 
- fixed setup database destroy prompt even when there was no db
 

	
 

	
 
1.0.0rc3 (**2010-10-11**)
 
-------------------------
 

	
 
- fixed i18n during installation.
 

	
 
1.0.0rc2 (**2010-10-11**)
 
-------------------------
 

	
 
- Disabled dirsize in file browser, it's causing nasty bug when dir renames 
 
  occure. After vcs is fixed it'll be put back again.
 
- templating/css rewrites, optimized css.
 
\ No newline at end of file
docs/conf.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
#
 
# RhodeCode documentation build configuration file, created by
 
# sphinx-quickstart on Sun Oct 10 16:46:37 2010.
 
#
 
# This file is execfile()d with the current directory set to its containing dir.
 
#
 
# Note that not all possible configuration values are present in this
 
# autogenerated file.
 
#
 
# All configuration values have a default; values that are commented out
 
# serve to show the default.
 

	
 
import sys
 
import os
 
import datetime
 

	
 
# If extensions (or modules to document with autodoc) are in another directory,
 
# add these directories to sys.path here. If the directory is relative to the
 
# documentation root, use os.path.abspath to make it absolute, like shown here.
 
sys.path.insert(0, os.path.abspath('..'))
 

	
 
# -- General configuration -----------------------------------------------------
 

	
 
# If your documentation needs a minimal Sphinx version, state it here.
 
#needs_sphinx = '1.0'
 

	
 
# Add any Sphinx extension module names here, as strings. They can be extensions
 
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
 
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 
 
              'sphinx.ext.intersphinx', 'sphinx.ext.todo', 
 
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest',
 
              'sphinx.ext.intersphinx', 'sphinx.ext.todo',
 
              'sphinx.ext.viewcode']
 

	
 
# Add any paths that contain templates here, relative to this directory.
 
templates_path = ['_templates']
 

	
 
# The suffix of source filenames.
 
source_suffix = '.rst'
 

	
 
# The encoding of source files.
 
#source_encoding = 'utf-8-sig'
 

	
 
# The master toctree document.
 
master_doc = 'index'
 

	
 
# General information about the project.
 
project = u'RhodeCode'
 
copyright = u'%s, Marcin Kuzminski' % (datetime.datetime.now().year)
 

	
 
# The version info for the project you're documenting, acts as replacement for
 
# |version| and |release|, also used in various other places throughout the
 
# built documents.
 
#
 
# The short X.Y version.
 
root = os.path.dirname(os.path.dirname(__file__))
 
sys.path.append(root)
 
from rhodecode import get_version, __version__
 
version = get_version()
 
# The full version, including alpha/beta/rc tags.
 
release = __version__
 

	
 
# The language for content autogenerated by Sphinx. Refer to documentation
 
# for a list of supported languages.
 
#language = None
 

	
 
# There are two options for replacing |today|: either, you set today to some
 
# non-false value, then it is used:
 
#today = ''
 
# Else, today_fmt is used as the format for a strftime call.
 
#today_fmt = '%B %d, %Y'
 

	
 
# List of patterns, relative to source directory, that match files and
 
# directories to ignore when looking for source files.
 
exclude_patterns = ['_build']
 

	
 
# The reST default role (used for this markup: `text`) to use for all documents.
 
#default_role = None
 

	
 
# If true, '()' will be appended to :func: etc. cross-reference text.
 
#add_function_parentheses = True
 

	
 
# If true, the current module name will be prepended to all description
 
# unit titles (such as .. function::).
 
#add_module_names = True
 

	
 
# If true, sectionauthor and moduleauthor directives will be shown in the
 
# output. They are ignored by default.
 
#show_authors = False
 

	
 
# The name of the Pygments (syntax highlighting) style to use.
 
pygments_style = 'sphinx'
 

	
 
# A list of ignored prefixes for module index sorting.
 
#modindex_common_prefix = []
 

	
 

	
 
# -- Options for HTML output ---------------------------------------------------
 

	
 
# The theme to use for HTML and HTML Help pages.  See the documentation for
 
# a list of builtin themes.
 
html_theme = 'nature'
 

	
 
# Theme options are theme-specific and customize the look and feel of a theme
 
# further.  For a list of options available for each theme, see the
 
# documentation.
 
#html_theme_options = {}
 

	
 
# Add any paths that contain custom themes here, relative to this directory.
 
html_theme_path = ['theme']
 

	
 
# The name for this set of Sphinx documents.  If None, it defaults to
 
# "<project> v<release> documentation".
 
#html_title = None
 

	
 
# A shorter title for the navigation bar.  Default is the same as html_title.
 
#html_short_title = None
 

	
 
# The name of an image file (relative to this directory) to place at the top
 
# of the sidebar.
 
#html_logo = None
 

	
 
# The name of an image file (within the static path) to use as favicon of the
 
# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
 
# pixels large.
 
#html_favicon = None
 

	
 
# Add any paths that contain custom static files (such as style sheets) here,
 
# relative to this directory. They are copied after the builtin static files,
 
# so a file named "default.css" will overwrite the builtin "default.css".
 
#html_static_path = ['_static']
 

	
 
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 
# using the given strftime format.
 
#html_last_updated_fmt = '%b %d, %Y'
 

	
 
# If true, SmartyPants will be used to convert quotes and dashes to
 
# typographically correct entities.
 
#html_use_smartypants = True
 

	
 
# Custom sidebar templates, maps document names to template names.
 
#html_sidebars = {}
 

	
 
# Additional templates that should be rendered to pages, maps page names to
 
# template names.
 
#html_additional_pages = {}
 

	
 
# If false, no module index is generated.
 
#html_domain_indices = True
 

	
 
# If false, no index is generated.
 
#html_use_index = True
 

	
 
# If true, the index is split into individual pages for each letter.
 
#html_split_index = False
 

	
 
# If true, links to the reST sources are added to the pages.
 
#html_show_sourcelink = True
 

	
 
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
 
#html_show_sphinx = True
 

	
 
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
 
#html_show_copyright = True
 

	
 
# If true, an OpenSearch description file will be output, and all pages will
 
# contain a <link> tag referring to it.  The value of this option must be the
 
# base URL from which the finished HTML is served.
 
#html_use_opensearch = ''
 

	
 
# This is the file name suffix for HTML files (e.g. ".xhtml").
 
#html_file_suffix = None
 

	
 
# Output file base name for HTML help builder.
 
htmlhelp_basename = 'RhodeCodedoc'
 

	
 

	
 
# -- Options for LaTeX output --------------------------------------------------
 

	
 
# The paper size ('letter' or 'a4').
 
#latex_paper_size = 'letter'
 

	
 
# The font size ('10pt', '11pt' or '12pt').
 
#latex_font_size = '10pt'
 

	
 
# Grouping the document tree into LaTeX files. List of tuples
 
# (source start file, target name, title, author, documentclass [howto/manual]).
 
latex_documents = [
 
  ('index', 'RhodeCode.tex', u'RhodeCode Documentation',
 
   u'Marcin Kuzminski', 'manual'),
 
]
 

	
 
# The name of an image file (relative to this directory) to place at the top of
 
# the title page.
 
#latex_logo = None
 

	
 
# For "manual" documents, if this is true, then toplevel headings are parts,
 
# not chapters.
 
#latex_use_parts = False
 

	
 
# If true, show page references after internal links.
 
#latex_show_pagerefs = False
 

	
 
# If true, show URL addresses after external links.
 
#latex_show_urls = False
 

	
 
# Additional stuff for the LaTeX preamble.
 
#latex_preamble = ''
 

	
 
# Documents to append as an appendix to all manuals.
 
#latex_appendices = []
 

	
 
# If false, no module index is generated.
 
#latex_domain_indices = True
 

	
 

	
 
# -- Options for manual page output --------------------------------------------
 

	
 
# One entry per manual page. List of tuples
 
# (source start file, name, description, authors, manual section).
 
man_pages = [
 
    ('index', 'rhodecode', u'RhodeCode Documentation',
 
     [u'Marcin Kuzminski'], 1)
 
]
 

	
 

	
 
# Example configuration for intersphinx: refer to the Python standard library.
 
intersphinx_mapping = {'http://docs.python.org/': None}
requires.txt
Show inline comments
 
Pylons==1.0.0
 
Beaker==1.6.3
 
WebHelpers==1.3
 
formencode==1.2.4
 
SQLAlchemy==0.7.6
 
Mako==0.7.0
 
pygments>=1.4
 
whoosh>=2.3.0,<2.4
 
whoosh>=2.4.0,<2.5
 
celery>=2.2.5,<2.3
 
babel
 
python-dateutil>=1.5.0,<2.0.0
 
dulwich>=0.8.5,<0.9.0
 
webob==1.0.8
 
markdown==2.1.1
 
docutils==0.8.1
 
py-bcrypt
 
mercurial>=2.2,<2.3
 
\ No newline at end of file
 
mercurial>=2.2.1,<2.3
 
\ No newline at end of file
rhodecode/__init__.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
"""
 
    rhodecode.__init__
 
    ~~~~~~~~~~~~~~~~~~
 

	
 
    RhodeCode, a web based repository management based on pylons
 
    versioning implementation: http://www.python.org/dev/peps/pep-0386/
 

	
 
    :created_on: Apr 9, 2010
 
    :author: marcink
 
    :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
 
    :license: GPLv3, see COPYING for more details.
 
"""
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
import sys
 
import platform
 

	
 
VERSION = (1, 4, 0, 'b')
 

	
 
try:
 
    from rhodecode.lib import get_current_revision
 
    _rev = get_current_revision()
 
    if _rev and len(VERSION) > 3:
 
        VERSION += ('dev%s' % _rev[0],)
 
except ImportError:
 
    pass
 

	
 
__version__ = ('.'.join((str(each) for each in VERSION[:3])) +
 
               '.'.join(VERSION[3:]))
 
__dbversion__ = 6  # defines current db version for migrations
 
__platform__ = platform.system()
 
__license__ = 'GPLv3'
 
__py_version__ = sys.version_info
 

	
 
PLATFORM_WIN = ('Windows')
 
PLATFORM_OTHERS = ('Linux', 'Darwin', 'FreeBSD', 'OpenBSD', 'SunOS')
 

	
 
requirements = [
 
    "Pylons==1.0.0",
 
    "Beaker==1.6.3",
 
    "WebHelpers==1.3",
 
    "formencode==1.2.4",
 
    "SQLAlchemy==0.7.6",
 
    "Mako==0.7.0",
 
    "pygments>=1.4",
 
    "whoosh>=2.3.0,<2.4",
 
    "whoosh>=2.4.0,<2.5",
 
    "celery>=2.2.5,<2.3",
 
    "babel",
 
    "python-dateutil>=1.5.0,<2.0.0",
 
    "dulwich>=0.8.5,<0.9.0",
 
    "webob==1.0.8",
 
    "markdown==2.1.1",
 
    "docutils==0.8.1",
 
]
 

	
 
if __py_version__ < (2, 6):
 
    requirements.append("simplejson")
 
    requirements.append("pysqlite")
 

	
 
if __platform__ in PLATFORM_WIN:
 
    requirements.append("mercurial>=2.2,<2.3")
 
    requirements.append("mercurial>=2.2.1,<2.3")
 
else:
 
    requirements.append("py-bcrypt")
 
    requirements.append("mercurial>=2.2,<2.3")
 
    requirements.append("mercurial>=2.2.1,<2.3")
 

	
 

	
 
def get_version():
 
    """Returns shorter version (digit parts only) as string."""
 

	
 
    return '.'.join((str(each) for each in VERSION[:3]))
 

	
 
BACKENDS = {
 
    'hg': 'Mercurial repository',
 
    'git': 'Git repository',
 
}
 

	
 
CELERY_ON = False
 

	
 
# link to config for pylons
 
CONFIG = {}
 

	
 
# Linked module for extensions
 
EXTENSIONS = {}
rhodecode/controllers/changelog.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
"""
 
    rhodecode.controllers.changelog
 
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

	
 
    changelog controller for rhodecode
 

	
 
    :created_on: Apr 21, 2010
 
    :author: marcink
 
    :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
 
    :license: GPLv3, see COPYING for more details.
 
"""
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 

	
 
import logging
 
import traceback
 

	
 
from mercurial import graphmod
 
from pylons import request, url, session, tmpl_context as c
 
from pylons.controllers.util import redirect
 
from pylons.i18n.translation import _
 

	
 
import rhodecode.lib.helpers as h
 
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
 
from rhodecode.lib.base import BaseRepoController, render
 
from rhodecode.lib.helpers import RepoPage
 
from rhodecode.lib.compat import json
 

	
 
from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError
 
from rhodecode.model.db import Repository
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
class ChangelogController(BaseRepoController):
 

	
 
    @LoginRequired()
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    def __before__(self):
 
        super(ChangelogController, self).__before__()
 
        c.affected_files_cut_off = 60
 

	
 
    def index(self):
 
        limit = 100
 
        default = 20
 
        if request.params.get('size'):
 
            try:
 
                int_size = int(request.params.get('size'))
 
            except ValueError:
 
                int_size = default
 
            int_size = int_size if int_size <= limit else limit
 
            c.size = int_size
 
            session['changelog_size'] = c.size
 
            session.save()
 
        else:
 
            c.size = int(session.get('changelog_size', default))
 

	
 
        p = int(request.params.get('page', 1))
 
        branch_name = request.params.get('branch', None)
 
        try:
 
            if branch_name:
 
                collection = [z for z in
 
                              c.rhodecode_repo.get_changesets(start=0,
 
                                                    branch_name=branch_name)]
 
                c.total_cs = len(collection)
 
            else:
 
                collection = c.rhodecode_repo
 
                c.total_cs = len(c.rhodecode_repo)
 

	
 
            c.pagination = RepoPage(collection, page=p, item_count=c.total_cs,
 
                                    items_per_page=c.size, branch=branch_name)
 
            collection = list(c.pagination)
 
            page_revisions = [x.raw_id for x in collection]
 
            c.comments = c.rhodecode_db_repo.comments(page_revisions)
 
            c.statuses = c.rhodecode_db_repo.statuses(page_revisions)
 
        except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
 
            log.error(traceback.format_exc())
 
            h.flash(str(e), category='warning')
 
            return redirect(url('home'))
 

	
 
        self._graph(c.rhodecode_repo, collection, c.total_cs, c.size, p)
 

	
 
        c.branch_name = branch_name
 
        c.branch_filters = [('', _('All Branches'))] + \
 
            [(k, k) for k in c.rhodecode_repo.branches.keys()]
 

	
 
        return render('changelog/changelog.html')
 

	
 
    def changelog_details(self, cs):
 
        if request.environ.get('HTTP_X_PARTIAL_XHR'):
 
            c.cs = c.rhodecode_repo.get_changeset(cs)
 
            return render('changelog/changelog_details.html')
 

	
 
    def _graph(self, repo, collection, repo_size, size, p):
 
        """
 
        Generates a DAG graph for mercurial
 

	
 
        :param repo: repo instance
 
        :param size: number of commits to show
 
        :param p: page number
 
        """
 
        if not collection:
 
            c.jsdata = json.dumps([])
 
            return
 

	
 
        data = []
 
        revs = [x.revision for x in collection]
 

	
 
        if repo.alias == 'git':
 
            for _ in revs:
 
                vtx = [0, 1]
 
                edges = [[0, 0, 1]]
 
                data.append(['', vtx, edges])
 

	
 
        elif repo.alias == 'hg':
 
            dag = graphmod.dagwalker(repo._repo, revs)
 
            try:
 
                c.dag = graphmod.colored(dag)
 
            except:
 
                #HG 2.2+
 
                c.dag = graphmod.colored(dag, repo._repo)
 

	
 
            c.dag = graphmod.colored(dag, repo._repo)
 
            for (id, type, ctx, vtx, edges) in c.dag:
 
                if type != graphmod.CHANGESET:
 
                    continue
 
                data.append(['', vtx, edges])
 

	
 
        c.jsdata = json.dumps(data)
rhodecode/lib/diffs.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
"""
 
    rhodecode.lib.diffs
 
    ~~~~~~~~~~~~~~~~~~~
 

	
 
    Set of diffing helpers, previously part of vcs
 

	
 

	
 
    :created_on: Dec 4, 2011
 
    :author: marcink
 
    :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
 
    :original copyright: 2007-2008 by Armin Ronacher
 
    :license: GPLv3, see COPYING for more details.
 
"""
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 

	
 
import re
 
import difflib
 
import markupsafe
 
from itertools import tee, imap
 

	
 
from pylons.i18n.translation import _
 

	
 
from rhodecode.lib.vcs.exceptions import VCSError
 
from rhodecode.lib.vcs.nodes import FileNode
 

	
 
from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
 
from rhodecode.lib.helpers import escape
 
from rhodecode.lib.utils import EmptyChangeset
 

	
 

	
 
def wrap_to_table(str_):
 
    return '''<table class="code-difftable">
 
                <tr class="line no-comment">
 
                <td class="lineno new"></td>
 
                <td class="code no-comment"><pre>%s</pre></td>
 
                </tr>
 
              </table>''' % str_
 

	
 

	
 
def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
 
                ignore_whitespace=True, line_context=3,
 
                enable_comments=False):
 
    """
 
    returns a wrapped diff into a table, checks for cut_off_limit and presents
 
    proper message
 
    """
 

	
 
    if filenode_old is None:
 
        filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
 

	
 
    if filenode_old.is_binary or filenode_new.is_binary:
 
        diff = wrap_to_table(_('binary file'))
 
        stats = (0, 0)
 
        size = 0
 

	
 
    elif cut_off_limit != -1 and (cut_off_limit is None or
 
    (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
 

	
 
        f_gitdiff = get_gitdiff(filenode_old, filenode_new,
 
                                ignore_whitespace=ignore_whitespace,
 
                                context=line_context)
 
        diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
 

	
 
        diff = diff_processor.as_html(enable_comments=enable_comments)
 
        stats = diff_processor.stat()
 
        size = len(diff or '')
 
    else:
 
        diff = wrap_to_table(_('Changeset was to big and was cut off, use '
 
                               'diff menu to display this diff'))
 
        stats = (0, 0)
 
        size = 0
 

	
 
    if not diff:
 
        diff = wrap_to_table(_('No changes detected'))
 
        submodules = filter(lambda o: isinstance(o, SubModuleNode),
 
                            [filenode_new, filenode_old])
 
        if submodules:
 
            diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
 
        else:
 
            diff = wrap_to_table(_('No changes detected'))
 

	
 
    cs1 = filenode_old.changeset.raw_id
 
    cs2 = filenode_new.changeset.raw_id
 

	
 
    return size, cs1, cs2, diff, stats
 

	
 

	
 
def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
 
    """
 
    Returns git style diff between given ``filenode_old`` and ``filenode_new``.
 

	
 
    :param ignore_whitespace: ignore whitespaces in diff
 
    """
 
    # make sure we pass in default context
 
    context = context or 3
 
    submodules = filter(lambda o: isinstance(o, SubModuleNode),
 
                        [filenode_new, filenode_old])
 
    if submodules:
 
        return ''
 

	
 
    for filenode in (filenode_old, filenode_new):
 
        if not isinstance(filenode, FileNode):
 
            raise VCSError("Given object should be FileNode object, not %s"
 
                % filenode.__class__)
 

	
 
    repo = filenode_new.changeset.repository
 
    old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
 
    new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
 

	
 
    vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
 
                                 ignore_whitespace, context)
 

	
 
    return vcs_gitdiff
 

	
 

	
 
class DiffProcessor(object):
 
    """
 
    Give it a unified diff and it returns a list of the files that were
 
    mentioned in the diff together with a dict of meta information that
 
    can be used to render it in a HTML template.
 
    """
 
    _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
 

	
 
    def __init__(self, diff, differ='diff', format='udiff'):
 
        """
 
        :param diff:   a text in diff format or generator
 
        :param format: format of diff passed, `udiff` or `gitdiff`
 
        """
 
        if isinstance(diff, basestring):
 
            diff = [diff]
 

	
 
        self.__udiff = diff
 
        self.__format = format
 
        self.adds = 0
 
        self.removes = 0
 

	
 
        if isinstance(self.__udiff, basestring):
 
            self.lines = iter(self.__udiff.splitlines(1))
 

	
 
        elif self.__format == 'gitdiff':
 
            udiff_copy = self.copy_iterator()
 
            self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
 
        else:
 
            udiff_copy = self.copy_iterator()
 
            self.lines = imap(self.escaper, udiff_copy)
 

	
 
        # Select a differ.
 
        if differ == 'difflib':
 
            self.differ = self._highlight_line_difflib
 
        else:
 
            self.differ = self._highlight_line_udiff
 

	
 
    def escaper(self, string):
 
        return markupsafe.escape(string)
 

	
 
    def copy_iterator(self):
 
        """
 
        make a fresh copy of generator, we should not iterate thru
 
        an original as it's needed for repeating operations on
 
        this instance of DiffProcessor
 
        """
 
        self.__udiff, iterator_copy = tee(self.__udiff)
 
        return iterator_copy
 

	
 
    def _extract_rev(self, line1, line2):
 
        """
 
        Extract the filename and revision hint from a line.
 
        """
 

	
 
        try:
 
            if line1.startswith('--- ') and line2.startswith('+++ '):
 
                l1 = line1[4:].split(None, 1)
 
                old_filename = (l1[0].replace('a/', '', 1)
 
                                if len(l1) >= 1 else None)
 
                old_rev = l1[1] if len(l1) == 2 else 'old'
 

	
 
                l2 = line2[4:].split(None, 1)
 
                new_filename = (l2[0].replace('b/', '', 1)
 
                                if len(l1) >= 1 else None)
 
                new_rev = l2[1] if len(l2) == 2 else 'new'
 

	
 
                filename = (old_filename
 
                            if old_filename != '/dev/null' else new_filename)
 

	
 
                return filename, new_rev, old_rev
 
        except (ValueError, IndexError):
 
            pass
 

	
 
        return None, None, None
 

	
 
    def _parse_gitdiff(self, diffiterator):
 
        def line_decoder(l):
 
            if l.startswith('+') and not l.startswith('+++'):
 
                self.adds += 1
 
            elif l.startswith('-') and not l.startswith('---'):
 
                self.removes += 1
 
            return l.decode('utf8', 'replace')
 

	
 
        output = list(diffiterator)
 
        size = len(output)
 

	
 
        if size == 2:
 
            l = []
 
            l.extend([output[0]])
 
            l.extend(output[1].splitlines(1))
 
            return map(line_decoder, l)
 
        elif size == 1:
 
            return  map(line_decoder, output[0].splitlines(1))
 
        elif size == 0:
 
            return []
 

	
 
        raise Exception('wrong size of diff %s' % size)
 

	
 
    def _highlight_line_difflib(self, line, next_):
 
        """
 
        Highlight inline changes in both lines.
 
        """
 

	
 
        if line['action'] == 'del':
 
            old, new = line, next_
 
        else:
 
            old, new = next_, line
 

	
 
        oldwords = re.split(r'(\W)', old['line'])
 
        newwords = re.split(r'(\W)', new['line'])
 

	
 
        sequence = difflib.SequenceMatcher(None, oldwords, newwords)
 

	
 
        oldfragments, newfragments = [], []
 
        for tag, i1, i2, j1, j2 in sequence.get_opcodes():
 
            oldfrag = ''.join(oldwords[i1:i2])
 
            newfrag = ''.join(newwords[j1:j2])
 
            if tag != 'equal':
 
                if oldfrag:
 
                    oldfrag = '<del>%s</del>' % oldfrag
 
                if newfrag:
 
                    newfrag = '<ins>%s</ins>' % newfrag
 
            oldfragments.append(oldfrag)
 
            newfragments.append(newfrag)
 

	
 
        old['line'] = "".join(oldfragments)
 
        new['line'] = "".join(newfragments)
 

	
 
    def _highlight_line_udiff(self, line, next_):
 
        """
 
        Highlight inline changes in both lines.
 
        """
 
        start = 0
 
        limit = min(len(line['line']), len(next_['line']))
 
        while start < limit and line['line'][start] == next_['line'][start]:
 
            start += 1
 
        end = -1
 
        limit -= start
 
        while -end <= limit and line['line'][end] == next_['line'][end]:
 
            end -= 1
 
        end += 1
 
        if start or end:
 
            def do(l):
 
                last = end + len(l['line'])
 
                if l['action'] == 'add':
 
                    tag = 'ins'
 
                else:
 
                    tag = 'del'
 
                l['line'] = '%s<%s>%s</%s>%s' % (
 
                    l['line'][:start],
 
                    tag,
 
                    l['line'][start:last],
 
                    tag,
 
                    l['line'][last:]
 
                )
 
            do(line)
 
            do(next_)
 

	
 
    def _parse_udiff(self):
 
        """
 
        Parse the diff an return data for the template.
 
        """
 
        lineiter = self.lines
 
        files = []
 
        try:
 
            line = lineiter.next()
 
            # skip first context
 
            skipfirst = True
 
            while 1:
 
                # continue until we found the old file
 
                if not line.startswith('--- '):
 
                    line = lineiter.next()
 
                    continue
 

	
 
                chunks = []
 
                filename, old_rev, new_rev = \
 
                    self._extract_rev(line, lineiter.next())
 
                files.append({
 
                    'filename':         filename,
 
                    'old_revision':     old_rev,
 
                    'new_revision':     new_rev,
 
                    'chunks':           chunks
 
                })
 

	
 
                line = lineiter.next()
 
                while line:
 
                    match = self._chunk_re.match(line)
 
                    if not match:
 
                        break
 

	
 
                    lines = []
 
                    chunks.append(lines)
 

	
 
                    old_line, old_end, new_line, new_end = \
 
                        [int(x or 1) for x in match.groups()[:-1]]
 
                    old_line -= 1
 
                    new_line -= 1
 
                    context = len(match.groups()) == 5
 
                    old_end += old_line
 
                    new_end += new_line
 

	
 
                    if context:
 
                        if not skipfirst:
 
                            lines.append({
 
                                'old_lineno': '...',
 
                                'new_lineno': '...',
 
                                'action':     'context',
 
                                'line':       line,
 
                            })
 
                        else:
 
                            skipfirst = False
 

	
 
                    line = lineiter.next()
 
                    while old_line < old_end or new_line < new_end:
 
                        if line:
 
                            command, line = line[0], line[1:]
 
                        else:
 
                            command = ' '
 
                        affects_old = affects_new = False
 

	
 
                        # ignore those if we don't expect them
 
                        if command in '#@':
 
                            continue
 
                        elif command == '+':
 
                            affects_new = True
 
                            action = 'add'
 
                        elif command == '-':
 
                            affects_old = True
 
                            action = 'del'
 
                        else:
 
                            affects_old = affects_new = True
 
                            action = 'unmod'
 

	
 
                        old_line += affects_old
 
                        new_line += affects_new
 
                        lines.append({
 
                            'old_lineno':   affects_old and old_line or '',
 
                            'new_lineno':   affects_new and new_line or '',
 
                            'action':       action,
 
                            'line':         line
 
                        })
 
                        line = lineiter.next()
 

	
 
        except StopIteration:
 
            pass
 

	
 
        # highlight inline changes
 
        for _ in files:
 
            for chunk in chunks:
 
                lineiter = iter(chunk)
 
                #first = True
 
                try:
 
                    while 1:
 
                        line = lineiter.next()
 
                        if line['action'] != 'unmod':
 
                            nextline = lineiter.next()
 
                            if nextline['action'] == 'unmod' or \
 
                               nextline['action'] == line['action']:
 
                                continue
 
                            self.differ(line, nextline)
 
                except StopIteration:
 
                    pass
 

	
 
        return files
 

	
 
    def prepare(self):
 
        """
 
        Prepare the passed udiff for HTML rendering. It'l return a list
 
        of dicts
 
        """
 
        return self._parse_udiff()
 

	
 
    def _safe_id(self, idstring):
 
        """Make a string safe for including in an id attribute.
 

	
 
        The HTML spec says that id attributes 'must begin with
 
        a letter ([A-Za-z]) and may be followed by any number
 
        of letters, digits ([0-9]), hyphens ("-"), underscores
 
        ("_"), colons (":"), and periods (".")'. These regexps
 
        are slightly over-zealous, in that they remove colons
 
        and periods unnecessarily.
 

	
 
        Whitespace is transformed into underscores, and then
 
        anything which is not a hyphen or a character that
 
        matches \w (alphanumerics and underscore) is removed.
 

	
 
        """
 
        # Transform all whitespace to underscore
 
        idstring = re.sub(r'\s', "_", '%s' % idstring)
 
        # Remove everything that is not a hyphen or a member of \w
 
        idstring = re.sub(r'(?!-)\W', "", idstring).lower()
 
        return idstring
 

	
 
    def raw_diff(self):
 
        """
 
        Returns raw string as udiff
 
        """
 
        udiff_copy = self.copy_iterator()
 
        if self.__format == 'gitdiff':
 
            udiff_copy = self._parse_gitdiff(udiff_copy)
 
        return u''.join(udiff_copy)
 

	
 
    def as_html(self, table_class='code-difftable', line_class='line',
 
                new_lineno_class='lineno old', old_lineno_class='lineno new',
 
                code_class='code', enable_comments=False):
 
        """
 
        Return udiff as html table with customized css classes
 
        """
 
        def _link_to_if(condition, label, url):
 
            """
 
            Generates a link if condition is meet or just the label if not.
 
            """
 

	
 
            if condition:
 
                return '''<a href="%(url)s">%(label)s</a>''' % {
 
                    'url': url,
 
                    'label': label
 
                }
 
            else:
 
                return label
 
        diff_lines = self.prepare()
 
        _html_empty = True
 
        _html = []
 
        _html.append('''<table class="%(table_class)s">\n''' % {
 
            'table_class': table_class
 
        })
 
        for diff in diff_lines:
 
            for line in diff['chunks']:
 
                _html_empty = False
 
                for change in line:
 
                    _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
 
                        'lc': line_class,
 
                        'action': change['action']
 
                    })
 
                    anchor_old_id = ''
 
                    anchor_new_id = ''
 
                    anchor_old = "%(filename)s_o%(oldline_no)s" % {
 
                        'filename': self._safe_id(diff['filename']),
 
                        'oldline_no': change['old_lineno']
 
                    }
 
                    anchor_new = "%(filename)s_n%(oldline_no)s" % {
 
                        'filename': self._safe_id(diff['filename']),
 
                        'oldline_no': change['new_lineno']
 
                    }
 
                    cond_old = (change['old_lineno'] != '...' and
 
                                change['old_lineno'])
 
                    cond_new = (change['new_lineno'] != '...' and
 
                                change['new_lineno'])
 
                    if cond_old:
 
                        anchor_old_id = 'id="%s"' % anchor_old
 
                    if cond_new:
 
                        anchor_new_id = 'id="%s"' % anchor_new
 
                    ###########################################################
 
                    # OLD LINE NUMBER
 
                    ###########################################################
 
                    _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
 
                        'a_id': anchor_old_id,
 
                        'olc': old_lineno_class
 
                    })
 

	
 
                    _html.append('''%(link)s''' % {
 
                        'link': _link_to_if(True, change['old_lineno'],
 
                                            '#%s' % anchor_old)
 
                    })
 
                    _html.append('''</td>\n''')
 
                    ###########################################################
 
                    # NEW LINE NUMBER
 
                    ###########################################################
 

	
 
                    _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
 
                        'a_id': anchor_new_id,
 
                        'nlc': new_lineno_class
 
                    })
 

	
 
                    _html.append('''%(link)s''' % {
 
                        'link': _link_to_if(True, change['new_lineno'],
 
                                            '#%s' % anchor_new)
 
                    })
 
                    _html.append('''</td>\n''')
 
                    ###########################################################
 
                    # CODE
 
                    ###########################################################
 
                    comments = '' if enable_comments else 'no-comment'
 
                    _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
 
                        'cc': code_class,
 
                        'inc': comments
 
                    })
 
                    _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
 
                        'code': change['line']
 
                    })
 
                    _html.append('''\t</td>''')
 
                    _html.append('''\n</tr>\n''')
 
        _html.append('''</table>''')
 
        if _html_empty:
 
            return None
 
        return ''.join(_html)
 

	
 
    def stat(self):
 
        """
 
        Returns tuple of added, and removed lines for this instance
 
        """
 
        return self.adds, self.removes
rhodecode/lib/hooks.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
"""
 
    rhodecode.lib.hooks
 
    ~~~~~~~~~~~~~~~~~~~
 

	
 
    Hooks runned by rhodecode
 

	
 
    :created_on: Aug 6, 2010
 
    :author: marcink
 
    :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
 
    :license: GPLv3, see COPYING for more details.
 
"""
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
import os
 
import sys
 

	
 
from mercurial.scmutil import revrange
 
from mercurial.node import nullrev
 
from rhodecode import EXTENSIONS
 
from rhodecode.lib import helpers as h
 
from rhodecode.lib.utils import action_logger
 
from inspect import isfunction
 

	
 

	
 
def _get_scm_size(alias, root_path):
 

	
 
    if not alias.startswith('.'):
 
        alias += '.'
 

	
 
    size_scm, size_root = 0, 0
 
    for path, dirs, files in os.walk(root_path):
 
        if path.find(alias) != -1:
 
            for f in files:
 
                try:
 
                    size_scm += os.path.getsize(os.path.join(path, f))
 
                except OSError:
 
                    pass
 
        else:
 
            for f in files:
 
                try:
 
                    size_root += os.path.getsize(os.path.join(path, f))
 
                except OSError:
 
                    pass
 

	
 
    size_scm_f = h.format_byte_size(size_scm)
 
    size_root_f = h.format_byte_size(size_root)
 
    size_total_f = h.format_byte_size(size_root + size_scm)
 

	
 
    return size_scm_f, size_root_f, size_total_f
 

	
 

	
 
def repo_size(ui, repo, hooktype=None, **kwargs):
 
    """
 
    Presents size of repository after push
 

	
 
    :param ui:
 
    :param repo:
 
    :param hooktype:
 
    """
 

	
 
    size_hg_f, size_root_f, size_total_f = _get_scm_size('.hg', repo.root)
 

	
 
    last_cs = repo[len(repo) - 1]
 

	
 
    msg = ('Repository size .hg:%s repo:%s total:%s\n'
 
           'Last revision is now r%s:%s\n') % (
 
        size_hg_f, size_root_f, size_total_f, last_cs.rev(), last_cs.hex()[:12]
 
    )
 

	
 
    sys.stdout.write(msg)
 

	
 

	
 
def log_pull_action(ui, repo, **kwargs):
 
    """
 
    Logs user last pull action
 

	
 
    :param ui:
 
    :param repo:
 
    """
 

	
 
    extras = dict(repo.ui.configitems('rhodecode_extras'))
 
    username = extras['username']
 
    repository = extras['repository']
 
    scm = extras['scm']
 
    action = 'pull'
 

	
 
    action_logger(username, action, repository, extras['ip'], commit=True)
 
    # extension hook call
 
    callback = getattr(EXTENSIONS, 'PULL_HOOK', None)
 

	
 
    if isfunction(callback):
 
        kw = {}
 
        kw.update(extras)
 
        callback(**kw)
 
    return 0
 

	
 

	
 
def log_push_action(ui, repo, **kwargs):
 
    """
 
    Maps user last push action to new changeset id, from mercurial
 

	
 
    :param ui:
 
    :param repo:
 
    :param repo: repo object containing the `ui` object
 
    """
 

	
 
    extras = dict(repo.ui.configitems('rhodecode_extras'))
 
    username = extras['username']
 
    repository = extras['repository']
 
    action = extras['action'] + ':%s'
 
    scm = extras['scm']
 

	
 
    if scm == 'hg':
 
        node = kwargs['node']
 

	
 
        def get_revs(repo, rev_opt):
 
            if rev_opt:
 
                revs = revrange(repo, rev_opt)
 

	
 
                if len(revs) == 0:
 
                    return (nullrev, nullrev)
 
                return (max(revs), min(revs))
 
            else:
 
                return (len(repo) - 1, 0)
 

	
 
        stop, start = get_revs(repo, [node + ':'])
 

	
 
        revs = (str(repo[r]) for r in xrange(start, stop + 1))
 
    elif scm == 'git':
 
        revs = []
 

	
 
    action = action % ','.join(revs)
 

	
 
    action_logger(username, action, repository, extras['ip'], commit=True)
 

	
 
    # extension hook call
 
    callback = getattr(EXTENSIONS, 'PUSH_HOOK', None)
 
    if isfunction(callback):
 
        kw = {'pushed_revs': revs}
 
        kw.update(extras)
 
        callback(**kw)
 
    return 0
 

	
 

	
 
def log_create_repository(repository_dict, created_by, **kwargs):
 
    """
 
    Post create repository Hook. This is a dummy function for admins to re-use
 
    if needed. It's taken from rhodecode-extensions module and executed
 
    if present
 

	
 
    :param repository: dict dump of repository object
 
    :param created_by: username who created repository
 
    :param created_date: date of creation
 

	
 
    available keys of repository_dict:
 

	
 
     'repo_type',
 
     'description',
 
     'private',
 
     'created_on',
 
     'enable_downloads',
 
     'repo_id',
 
     'user_id',
 
     'enable_statistics',
 
     'clone_uri',
 
     'fork_id',
 
     'group_id',
 
     'repo_name'
 

	
 
    """
 

	
 
    callback = getattr(EXTENSIONS, 'CREATE_REPO_HOOK', None)
 
    if isfunction(callback):
 
        kw = {}
 
        kw.update(repository_dict)
 
        kw.update({'created_by': created_by})
 
        kw.update(kwargs)
 
        return callback(**kw)
 

	
 
    return 0
rhodecode/lib/middleware/simplegit.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
"""
 
    rhodecode.lib.middleware.simplegit
 
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

	
 
    SimpleGit middleware for handling git protocol request (push/clone etc.)
 
    It's implemented with basic auth function
 

	
 
    :created_on: Apr 28, 2010
 
    :author: marcink
 
    :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
 
    :license: GPLv3, see COPYING for more details.
 
"""
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 

	
 
import os
 
import re
 
import logging
 
import traceback
 

	
 
from dulwich import server as dulserver
 

	
 

	
 
class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
 

	
 
    def handle(self):
 
        write = lambda x: self.proto.write_sideband(1, x)
 

	
 
        graph_walker = dulserver.ProtocolGraphWalker(self,
 
                                                     self.repo.object_store,
 
                                                     self.repo.get_peeled)
 
        objects_iter = self.repo.fetch_objects(
 
          graph_walker.determine_wants, graph_walker, self.progress,
 
          get_tagged=self.get_tagged)
 

	
 
        # Did the process short-circuit (e.g. in a stateless RPC call)? Note
 
        # that the client still expects a 0-object pack in most cases.
 
        if objects_iter is None:
 
            return
 

	
 
        self.progress("counting objects: %d, done.\n" % len(objects_iter))
 
        dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
 
                                     objects_iter)
 
        messages = []
 
        messages.append('thank you for using rhodecode')
 

	
 
        for msg in messages:
 
            self.progress(msg + "\n")
 
        # we are done
 
        self.proto.write("0000")
 

	
 

	
 
dulserver.DEFAULT_HANDLERS = {
 
  'git-upload-pack': SimpleGitUploadPackHandler,
 
  'git-receive-pack': dulserver.ReceivePackHandler,
 
}
 

	
 
from dulwich.repo import Repo
 
from dulwich.web import make_wsgi_chain
 

	
 
from paste.httpheaders import REMOTE_USER, AUTH_TYPE
 

	
 
from rhodecode.lib.utils2 import safe_str
 
from rhodecode.lib.base import BaseVCSController
 
from rhodecode.lib.auth import get_container_username
 
from rhodecode.lib.utils import is_valid_repo, make_ui
 
from rhodecode.model.db import User
 

	
 
from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')
 

	
 

	
 
def is_git(environ):
 
    path_info = environ['PATH_INFO']
 
    isgit_path = GIT_PROTO_PAT.match(path_info)
 
    log.debug('pathinfo: %s detected as GIT %s' % (
 
        path_info, isgit_path != None)
 
    )
 
    return isgit_path
 

	
 

	
 
class SimpleGit(BaseVCSController):
 

	
 
    def _handle_request(self, environ, start_response):
 

	
 
        if not is_git(environ):
 
            return self.application(environ, start_response)
 

	
 
        ipaddr = self._get_ip_addr(environ)
 
        username = None
 
        self._git_first_op = False
 
        # skip passing error to error controller
 
        environ['pylons.status_code_redirect'] = True
 

	
 
        #======================================================================
 
        # EXTRACT REPOSITORY NAME FROM ENV
 
        #======================================================================
 
        try:
 
            repo_name = self.__get_repository(environ)
 
            log.debug('Extracted repo name is %s' % repo_name)
 
        except:
 
            return HTTPInternalServerError()(environ, start_response)
 

	
 
        # quick check if that dir exists...
 
        if is_valid_repo(repo_name, self.basepath) is False:
 
            return HTTPNotFound()(environ, start_response)
 

	
 
        #======================================================================
 
        # GET ACTION PULL or PUSH
 
        #======================================================================
 
        action = self.__get_action(environ)
 

	
 
        #======================================================================
 
        # CHECK ANONYMOUS PERMISSION
 
        #======================================================================
 
        if action in ['pull', 'push']:
 
            anonymous_user = self.__get_user('default')
 
            username = anonymous_user.username
 
            anonymous_perm = self._check_permission(action, anonymous_user,
 
                                                    repo_name)
 

	
 
            if anonymous_perm is not True or anonymous_user.active is False:
 
                if anonymous_perm is not True:
 
                    log.debug('Not enough credentials to access this '
 
                              'repository as anonymous user')
 
                if anonymous_user.active is False:
 
                    log.debug('Anonymous access is disabled, running '
 
                              'authentication')
 
                #==============================================================
 
                # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
 
                # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
 
                #==============================================================
 

	
 
                # Attempting to retrieve username from the container
 
                username = get_container_username(environ, self.config)
 

	
 
                # If not authenticated by the container, running basic auth
 
                if not username:
 
                    self.authenticate.realm = \
 
                        safe_str(self.config['rhodecode_realm'])
 
                    result = self.authenticate(environ)
 
                    if isinstance(result, str):
 
                        AUTH_TYPE.update(environ, 'basic')
 
                        REMOTE_USER.update(environ, result)
 
                        username = result
 
                    else:
 
                        return result.wsgi_application(environ, start_response)
 

	
 
                #==============================================================
 
                # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
 
                #==============================================================
 
                if action in ['pull', 'push']:
 
                    try:
 
                        user = self.__get_user(username)
 
                        if user is None or not user.active:
 
                            return HTTPForbidden()(environ, start_response)
 
                        username = user.username
 
                    except:
 
                        log.error(traceback.format_exc())
 
                        return HTTPInternalServerError()(environ,
 
                                                         start_response)
 

	
 
                    #check permissions for this repository
 
                    perm = self._check_permission(action, user, repo_name)
 
                    if perm is not True:
 
                        return HTTPForbidden()(environ, start_response)
 
        extras = {
 
            'ip': ipaddr,
 
            'username': username,
 
            'action': action,
 
            'repository': repo_name,
 
            'scm': 'git',
 
        }
 

	
 
        #===================================================================
 
        # GIT REQUEST HANDLING
 
        #===================================================================
 
        repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
 
        log.debug('Repository path is %s' % repo_path)
 

	
 
        baseui = make_ui('db')
 
        self.__inject_extras(repo_path, baseui, extras)
 

	
 

	
 
        try:
 
            # invalidate cache on push
 
            if action == 'push':
 
                self._invalidate_cache(repo_name)
 
            self._handle_githooks(action, baseui, environ)
 
            self._handle_githooks(repo_name, action, baseui, environ)
 

	
 
            log.info('%s action on GIT repo "%s"' % (action, repo_name))
 
            app = self.__make_app(repo_name, repo_path)
 
            return app(environ, start_response)
 
        except Exception:
 
            log.error(traceback.format_exc())
 
            return HTTPInternalServerError()(environ, start_response)
 

	
 
    def __make_app(self, repo_name, repo_path):
 
        """
 
        Make an wsgi application using dulserver
 

	
 
        :param repo_name: name of the repository
 
        :param repo_path: full path to the repository
 
        """
 
        _d = {'/' + repo_name: Repo(repo_path)}
 
        backend = dulserver.DictBackend(_d)
 
        gitserve = make_wsgi_chain(backend)
 

	
 
        return gitserve
 

	
 
    def __get_repository(self, environ):
 
        """
 
        Get's repository name out of PATH_INFO header
 

	
 
        :param environ: environ where PATH_INFO is stored
 
        """
 
        try:
 
            environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
 
            repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
 
        except:
 
            log.error(traceback.format_exc())
 
            raise
 

	
 
        return repo_name
 

	
 
    def __get_user(self, username):
 
        return User.get_by_username(username)
 

	
 
    def __get_action(self, environ):
 
        """
 
        Maps git request commands into a pull or push command.
 

	
 
        :param environ:
 
        """
 
        service = environ['QUERY_STRING'].split('=')
 

	
 
        if len(service) > 1:
 
            service_cmd = service[1]
 
            mapping = {
 
                'git-receive-pack': 'push',
 
                'git-upload-pack': 'pull',
 
            }
 
            op = mapping[service_cmd]
 
            self._git_stored_op = op
 
            return op
 
        else:
 
            # try to fallback to stored variable as we don't know if the last
 
            # operation is pull/push
 
            op = getattr(self, '_git_stored_op', 'pull')
 
        return op
 

	
 
    def _handle_githooks(self, action, baseui, environ):
 
    def _handle_githooks(self, repo_name, action, baseui, environ):
 
        from rhodecode.lib.hooks import log_pull_action, log_push_action
 
        service = environ['QUERY_STRING'].split('=')
 
        if len(service) < 2:
 
            return
 

	
 
        from rhodecode.model.db import Repository
 
        _repo = Repository.get_by_repo_name(repo_name)
 
        _repo = _repo.scm_instance
 
        _repo._repo.ui = baseui
 

	
 
        push_hook = 'pretxnchangegroup.push_logger'
 
        pull_hook = 'preoutgoing.pull_logger'
 
        _hooks = dict(baseui.configitems('hooks')) or {}
 
        if action == 'push' and _hooks.get(push_hook):
 
            log_push_action(ui=baseui, repo=repo._repo)
 
            log_push_action(ui=baseui, repo=_repo._repo)
 
        elif action == 'pull' and _hooks.get(pull_hook):
 
            log_pull_action(ui=baseui, repo=repo._repo)
 
            log_pull_action(ui=baseui, repo=_repo._repo)
 

	
 
    def __inject_extras(self, repo_path, baseui, extras={}):
 
        """
 
        Injects some extra params into baseui instance
 

	
 
        :param baseui: baseui instance
 
        :param extras: dict with extra params to put into baseui
 
        """
 

	
 
        # make our hgweb quiet so it doesn't print output
 
        baseui.setconfig('ui', 'quiet', 'true')
 

	
 
        #inject some additional parameters that will be available in ui
 
        #for hooks
 
        for k, v in extras.items():
 
            baseui.setconfig('rhodecode_extras', k, v)
rhodecode/lib/vcs/backends/base.py
Show inline comments
 
@@ -144,768 +144,813 @@ class BaseRepository(object):
 
        """
 
        Allows Repository objects to be iterated.
 

	
 
        *Requires* implementation of ``__getitem__`` method.
 
        """
 
        for revision in self.revisions:
 
            yield self.get_changeset(revision)
 

	
 
    def get_changesets(self, start=None, end=None, start_date=None,
 
                       end_date=None, branch_name=None, reverse=False):
 
        """
 
        Returns iterator of ``MercurialChangeset`` objects from start to end
 
        not inclusive This should behave just like a list, ie. end is not
 
        inclusive
 

	
 
        :param start: None or str
 
        :param end: None or str
 
        :param start_date:
 
        :param end_date:
 
        :param branch_name:
 
        :param reversed:
 
        """
 
        raise NotImplementedError
 

	
 
    def __getslice__(self, i, j):
 
        """
 
        Returns a iterator of sliced repository
 
        """
 
        for rev in self.revisions[i:j]:
 
            yield self.get_changeset(rev)
 

	
 
    def __getitem__(self, key):
 
        return self.get_changeset(key)
 

	
 
    def count(self):
 
        return len(self.revisions)
 

	
 
    def tag(self, name, user, revision=None, message=None, date=None, **opts):
 
        """
 
        Creates and returns a tag for the given ``revision``.
 

	
 
        :param name: name for new tag
 
        :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
 
        :param revision: changeset id for which new tag would be created
 
        :param message: message of the tag's commit
 
        :param date: date of tag's commit
 

	
 
        :raises TagAlreadyExistError: if tag with same name already exists
 
        """
 
        raise NotImplementedError
 

	
 
    def remove_tag(self, name, user, message=None, date=None):
 
        """
 
        Removes tag with the given ``name``.
 

	
 
        :param name: name of the tag to be removed
 
        :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
 
        :param message: message of the tag's removal commit
 
        :param date: date of tag's removal commit
 

	
 
        :raises TagDoesNotExistError: if tag with given name does not exists
 
        """
 
        raise NotImplementedError
 

	
 
    def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
 
            context=3):
 
        """
 
        Returns (git like) *diff*, as plain text. Shows changes introduced by
 
        ``rev2`` since ``rev1``.
 

	
 
        :param rev1: Entry point from which diff is shown. Can be
 
          ``self.EMPTY_CHANGESET`` - in this case, patch showing all
 
          the changes since empty state of the repository until ``rev2``
 
        :param rev2: Until which revision changes should be shown.
 
        :param ignore_whitespace: If set to ``True``, would not show whitespace
 
          changes. Defaults to ``False``.
 
        :param context: How many lines before/after changed lines should be
 
          shown. Defaults to ``3``.
 
        """
 
        raise NotImplementedError
 

	
 
    # ========== #
 
    # COMMIT API #
 
    # ========== #
 

	
 
    @LazyProperty
 
    def in_memory_changeset(self):
 
        """
 
        Returns ``InMemoryChangeset`` object for this repository.
 
        """
 
        raise NotImplementedError
 

	
 
    def add(self, filenode, **kwargs):
 
        """
 
        Commit api function that will add given ``FileNode`` into this
 
        repository.
 

	
 
        :raises ``NodeAlreadyExistsError``: if there is a file with same path
 
          already in repository
 
        :raises ``NodeAlreadyAddedError``: if given node is already marked as
 
          *added*
 
        """
 
        raise NotImplementedError
 

	
 
    def remove(self, filenode, **kwargs):
 
        """
 
        Commit api function that will remove given ``FileNode`` into this
 
        repository.
 

	
 
        :raises ``EmptyRepositoryError``: if there are no changesets yet
 
        :raises ``NodeDoesNotExistError``: if there is no file with given path
 
        """
 
        raise NotImplementedError
 

	
 
    def commit(self, message, **kwargs):
 
        """
 
        Persists current changes made on this repository and returns newly
 
        created changeset.
 

	
 
        :raises ``NothingChangedError``: if no changes has been made
 
        """
 
        raise NotImplementedError
 

	
 
    def get_state(self):
 
        """
 
        Returns dictionary with ``added``, ``changed`` and ``removed`` lists
 
        containing ``FileNode`` objects.
 
        """
 
        raise NotImplementedError
 

	
 
    def get_config_value(self, section, name, config_file=None):
 
        """
 
        Returns configuration value for a given [``section``] and ``name``.
 

	
 
        :param section: Section we want to retrieve value from
 
        :param name: Name of configuration we want to retrieve
 
        :param config_file: A path to file which should be used to retrieve
 
          configuration from (might also be a list of file paths)
 
        """
 
        raise NotImplementedError
 

	
 
    def get_user_name(self, config_file=None):
 
        """
 
        Returns user's name from global configuration file.
 

	
 
        :param config_file: A path to file which should be used to retrieve
 
          configuration from (might also be a list of file paths)
 
        """
 
        raise NotImplementedError
 

	
 
    def get_user_email(self, config_file=None):
 
        """
 
        Returns user's email from global configuration file.
 

	
 
        :param config_file: A path to file which should be used to retrieve
 
          configuration from (might also be a list of file paths)
 
        """
 
        raise NotImplementedError
 

	
 
    # =========== #
 
    # WORKDIR API #
 
    # =========== #
 

	
 
    @LazyProperty
 
    def workdir(self):
 
        """
 
        Returns ``Workdir`` instance for this repository.
 
        """
 
        raise NotImplementedError
 

	
 

	
 
class BaseChangeset(object):
 
    """
 
    Each backend should implement it's changeset representation.
 

	
 
    **Attributes**
 

	
 
        ``repository``
 
            repository object within which changeset exists
 

	
 
        ``id``
 
            may be ``raw_id`` or i.e. for mercurial's tip just ``tip``
 

	
 
        ``raw_id``
 
            raw changeset representation (i.e. full 40 length sha for git
 
            backend)
 

	
 
        ``short_id``
 
            shortened (if apply) version of ``raw_id``; it would be simple
 
            shortcut for ``raw_id[:12]`` for git/mercurial backends or same
 
            as ``raw_id`` for subversion
 

	
 
        ``revision``
 
            revision number as integer
 

	
 
        ``files``
 
            list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
 

	
 
        ``dirs``
 
            list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
 

	
 
        ``nodes``
 
            combined list of ``Node`` objects
 

	
 
        ``author``
 
            author of the changeset, as unicode
 

	
 
        ``message``
 
            message of the changeset, as unicode
 

	
 
        ``parents``
 
            list of parent changesets
 

	
 
        ``last``
 
            ``True`` if this is last changeset in repository, ``False``
 
            otherwise; trying to access this attribute while there is no
 
            changesets would raise ``EmptyRepositoryError``
 
    """
 
    def __str__(self):
 
        return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
 
            self.short_id)
 

	
 
    def __repr__(self):
 
        return self.__str__()
 

	
 
    def __unicode__(self):
 
        return u'%s:%s' % (self.revision, self.short_id)
 

	
 
    def __eq__(self, other):
 
        return self.raw_id == other.raw_id
 

	
 
    @LazyProperty
 
    def last(self):
 
        if self.repository is None:
 
            raise ChangesetError("Cannot check if it's most recent revision")
 
        return self.raw_id == self.repository.revisions[-1]
 

	
 
    @LazyProperty
 
    def parents(self):
 
        """
 
        Returns list of parents changesets.
 
        """
 
        raise NotImplementedError
 

	
 
    @LazyProperty
 
    def id(self):
 
        """
 
        Returns string identifying this changeset.
 
        """
 
        raise NotImplementedError
 

	
 
    @LazyProperty
 
    def raw_id(self):
 
        """
 
        Returns raw string identifying this changeset.
 
        """
 
        raise NotImplementedError
 

	
 
    @LazyProperty
 
    def short_id(self):
 
        """
 
        Returns shortened version of ``raw_id`` attribute, as string,
 
        identifying this changeset, useful for web representation.
 
        """
 
        raise NotImplementedError
 

	
 
    @LazyProperty
 
    def revision(self):
 
        """
 
        Returns integer identifying this changeset.
 

	
 
        """
 
        raise NotImplementedError
 

	
 
    @LazyProperty
 
    def author(self):
 
        """
 
        Returns Author for given commit
 
        """
 

	
 
        raise NotImplementedError
 

	
 
    @LazyProperty
 
    def author_name(self):
 
        """
 
        Returns Author name for given commit
 
        """
 

	
 
        return author_name(self.author)
 

	
 
    @LazyProperty
 
    def author_email(self):
 
        """
 
        Returns Author email address for given commit
 
        """
 

	
 
        return author_email(self.author)
 

	
 
    def get_file_mode(self, path):
 
        """
 
        Returns stat mode of the file at the given ``path``.
 
        """
 
        raise NotImplementedError
 

	
 
    def get_file_content(self, path):
 
        """
 
        Returns content of the file at the given ``path``.
 
        """
 
        raise NotImplementedError
 

	
 
    def get_file_size(self, path):
 
        """
 
        Returns size of the file at the given ``path``.
 
        """
 
        raise NotImplementedError
 

	
 
    def get_file_changeset(self, path):
 
        """
 
        Returns last commit of the file at the given ``path``.
 
        """
 
        raise NotImplementedError
 

	
 
    def get_file_history(self, path):
 
        """
 
        Returns history of file as reversed list of ``Changeset`` objects for
 
        which file at given ``path`` has been modified.
 
        """
 
        raise NotImplementedError
 

	
 
    def get_nodes(self, path):
 
        """
 
        Returns combined ``DirNode`` and ``FileNode`` objects list representing
 
        state of changeset at the given ``path``.
 

	
 
        :raises ``ChangesetError``: if node at the given ``path`` is not
 
          instance of ``DirNode``
 
        """
 
        raise NotImplementedError
 

	
 
    def get_node(self, path):
 
        """
 
        Returns ``Node`` object from the given ``path``.
 

	
 
        :raises ``NodeDoesNotExistError``: if there is no node at the given
 
          ``path``
 
        """
 
        raise NotImplementedError
 

	
 
    def fill_archive(self, stream=None, kind='tgz', prefix=None):
 
        """
 
        Fills up given stream.
 

	
 
        :param stream: file like object.
 
        :param kind: one of following: ``zip``, ``tar``, ``tgz``
 
            or ``tbz2``. Default: ``tgz``.
 
        :param prefix: name of root directory in archive.
 
            Default is repository name and changeset's raw_id joined with dash.
 

	
 
            repo-tip.<kind>
 
        """
 

	
 
        raise NotImplementedError
 

	
 
    def get_chunked_archive(self, **kwargs):
 
        """
 
        Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
 

	
 
        :param chunk_size: extra parameter which controls size of returned
 
            chunks. Default:8k.
 
        """
 

	
 
        chunk_size = kwargs.pop('chunk_size', 8192)
 
        stream = kwargs.get('stream')
 
        self.fill_archive(**kwargs)
 
        while True:
 
            data = stream.read(chunk_size)
 
            if not data:
 
                break
 
            yield data
 

	
 
    @LazyProperty
 
    def root(self):
 
        """
 
        Returns ``RootNode`` object for this changeset.
 
        """
 
        return self.get_node('')
 

	
 
    def next(self, branch=None):
 
        """
 
        Returns next changeset from current, if branch is gives it will return
 
        next changeset belonging to this branch
 

	
 
        :param branch: show changesets within the given named branch
 
        """
 
        raise NotImplementedError
 

	
 
    def prev(self, branch=None):
 
        """
 
        Returns previous changeset from current, if branch is gives it will
 
        return previous changeset belonging to this branch
 

	
 
        :param branch: show changesets within the given named branch
 
        """
 
        raise NotImplementedError
 

	
 
    @LazyProperty
 
    def added(self):
 
        """
 
        Returns list of added ``FileNode`` objects.
 
        """
 
        raise NotImplementedError
 

	
 
    @LazyProperty
 
    def changed(self):
 
        """
 
        Returns list of modified ``FileNode`` objects.
 
        """
 
        raise NotImplementedError
 

	
 
    @LazyProperty
 
    def removed(self):
 
        """
 
        Returns list of removed ``FileNode`` objects.
 
        """
 
        raise NotImplementedError
 

	
 
    @LazyProperty
 
    def size(self):
 
        """
 
        Returns total number of bytes from contents of all filenodes.
 
        """
 
        return sum((node.size for node in self.get_filenodes_generator()))
 

	
 
    def walk(self, topurl=''):
 
        """
 
        Similar to os.walk method. Insted of filesystem it walks through
 
        changeset starting at given ``topurl``.  Returns generator of tuples
 
        (topnode, dirnodes, filenodes).
 
        """
 
        topnode = self.get_node(topurl)
 
        yield (topnode, topnode.dirs, topnode.files)
 
        for dirnode in topnode.dirs:
 
            for tup in self.walk(dirnode.path):
 
                yield tup
 

	
 
    def get_filenodes_generator(self):
 
        """
 
        Returns generator that yields *all* file nodes.
 
        """
 
        for topnode, dirs, files in self.walk():
 
            for node in files:
 
                yield node
 

	
 
    def as_dict(self):
 
        """
 
        Returns dictionary with changeset's attributes and their values.
 
        """
 
        data = get_dict_for_attrs(self, ['id', 'raw_id', 'short_id',
 
            'revision', 'date', 'message'])
 
        data['author'] = {'name': self.author_name, 'email': self.author_email}
 
        data['added'] = [node.path for node in self.added]
 
        data['changed'] = [node.path for node in self.changed]
 
        data['removed'] = [node.path for node in self.removed]
 
        return data
 

	
 

	
 
class BaseWorkdir(object):
 
    """
 
    Working directory representation of single repository.
 

	
 
    :attribute: repository: repository object of working directory
 
    """
 

	
 
    def __init__(self, repository):
 
        self.repository = repository
 

	
 
    def get_branch(self):
 
        """
 
        Returns name of current branch.
 
        """
 
        raise NotImplementedError
 

	
 
    def get_changeset(self):
 
        """
 
        Returns current changeset.
 
        """
 
        raise NotImplementedError
 

	
 
    def get_added(self):
 
        """
 
        Returns list of ``FileNode`` objects marked as *new* in working
 
        directory.
 
        """
 
        raise NotImplementedError
 

	
 
    def get_changed(self):
 
        """
 
        Returns list of ``FileNode`` objects *changed* in working directory.
 
        """
 
        raise NotImplementedError
 

	
 
    def get_removed(self):
 
        """
 
        Returns list of ``RemovedFileNode`` objects marked as *removed* in
 
        working directory.
 
        """
 
        raise NotImplementedError
 

	
 
    def get_untracked(self):
 
        """
 
        Returns list of ``FileNode`` objects which are present within working
 
        directory however are not tracked by repository.
 
        """
 
        raise NotImplementedError
 

	
 
    def get_status(self):
 
        """
 
        Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
 
        lists.
 
        """
 
        raise NotImplementedError
 

	
 
    def commit(self, message, **kwargs):
 
        """
 
        Commits local (from working directory) changes and returns newly
 
        created
 
        ``Changeset``. Updates repository's ``revisions`` list.
 

	
 
        :raises ``CommitError``: if any error occurs while committing
 
        """
 
        raise NotImplementedError
 

	
 
    def update(self, revision=None):
 
        """
 
        Fetches content of the given revision and populates it within working
 
        directory.
 
        """
 
        raise NotImplementedError
 

	
 
    def checkout_branch(self, branch=None):
 
        """
 
        Checks out ``branch`` or the backend's default branch.
 

	
 
        Raises ``BranchDoesNotExistError`` if the branch does not exist.
 
        """
 
        raise NotImplementedError
 

	
 

	
 
class BaseInMemoryChangeset(object):
 
    """
 
    Represents differences between repository's state (most recent head) and
 
    changes made *in place*.
 

	
 
    **Attributes**
 

	
 
        ``repository``
 
            repository object for this in-memory-changeset
 

	
 
        ``added``
 
            list of ``FileNode`` objects marked as *added*
 

	
 
        ``changed``
 
            list of ``FileNode`` objects marked as *changed*
 

	
 
        ``removed``
 
            list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
 
            *removed*
 

	
 
        ``parents``
 
            list of ``Changeset`` representing parents of in-memory changeset.
 
            Should always be 2-element sequence.
 

	
 
    """
 

	
 
    def __init__(self, repository):
 
        self.repository = repository
 
        self.added = []
 
        self.changed = []
 
        self.removed = []
 
        self.parents = []
 

	
 
    def add(self, *filenodes):
 
        """
 
        Marks given ``FileNode`` objects as *to be committed*.
 

	
 
        :raises ``NodeAlreadyExistsError``: if node with same path exists at
 
          latest changeset
 
        :raises ``NodeAlreadyAddedError``: if node with same path is already
 
          marked as *added*
 
        """
 
        # Check if not already marked as *added* first
 
        for node in filenodes:
 
            if node.path in (n.path for n in self.added):
 
                raise NodeAlreadyAddedError("Such FileNode %s is already "
 
                    "marked for addition" % node.path)
 
        for node in filenodes:
 
            self.added.append(node)
 

	
 
    def change(self, *filenodes):
 
        """
 
        Marks given ``FileNode`` objects to be *changed* in next commit.
 

	
 
        :raises ``EmptyRepositoryError``: if there are no changesets yet
 
        :raises ``NodeAlreadyExistsError``: if node with same path is already
 
          marked to be *changed*
 
        :raises ``NodeAlreadyRemovedError``: if node with same path is already
 
          marked to be *removed*
 
        :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
 
          changeset
 
        :raises ``NodeNotChangedError``: if node hasn't really be changed
 
        """
 
        for node in filenodes:
 
            if node.path in (n.path for n in self.removed):
 
                raise NodeAlreadyRemovedError("Node at %s is already marked "
 
                    "as removed" % node.path)
 
        try:
 
            self.repository.get_changeset()
 
        except EmptyRepositoryError:
 
            raise EmptyRepositoryError("Nothing to change - try to *add* new "
 
                "nodes rather than changing them")
 
        for node in filenodes:
 
            if node.path in (n.path for n in self.changed):
 
                raise NodeAlreadyChangedError("Node at '%s' is already "
 
                    "marked as changed" % node.path)
 
            self.changed.append(node)
 

	
 
    def remove(self, *filenodes):
 
        """
 
        Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
 
        *removed* in next commit.
 

	
 
        :raises ``NodeAlreadyRemovedError``: if node has been already marked to
 
          be *removed*
 
        :raises ``NodeAlreadyChangedError``: if node has been already marked to
 
          be *changed*
 
        """
 
        for node in filenodes:
 
            if node.path in (n.path for n in self.removed):
 
                raise NodeAlreadyRemovedError("Node is already marked to "
 
                    "for removal at %s" % node.path)
 
            if node.path in (n.path for n in self.changed):
 
                raise NodeAlreadyChangedError("Node is already marked to "
 
                    "be changed at %s" % node.path)
 
            # We only mark node as *removed* - real removal is done by
 
            # commit method
 
            self.removed.append(node)
 

	
 
    def reset(self):
 
        """
 
        Resets this instance to initial state (cleans ``added``, ``changed``
 
        and ``removed`` lists).
 
        """
 
        self.added = []
 
        self.changed = []
 
        self.removed = []
 
        self.parents = []
 

	
 
    def get_ipaths(self):
 
        """
 
        Returns generator of paths from nodes marked as added, changed or
 
        removed.
 
        """
 
        for node in chain(self.added, self.changed, self.removed):
 
            yield node.path
 

	
 
    def get_paths(self):
 
        """
 
        Returns list of paths from nodes marked as added, changed or removed.
 
        """
 
        return list(self.get_ipaths())
 

	
 
    def check_integrity(self, parents=None):
 
        """
 
        Checks in-memory changeset's integrity. Also, sets parents if not
 
        already set.
 

	
 
        :raises CommitError: if any error occurs (i.e.
 
          ``NodeDoesNotExistError``).
 
        """
 
        if not self.parents:
 
            parents = parents or []
 
            if len(parents) == 0:
 
                try:
 
                    parents = [self.repository.get_changeset(), None]
 
                except EmptyRepositoryError:
 
                    parents = [None, None]
 
            elif len(parents) == 1:
 
                parents += [None]
 
            self.parents = parents
 

	
 
        # Local parents, only if not None
 
        parents = [p for p in self.parents if p]
 

	
 
        # Check nodes marked as added
 
        for p in parents:
 
            for node in self.added:
 
                try:
 
                    p.get_node(node.path)
 
                except NodeDoesNotExistError:
 
                    pass
 
                else:
 
                    raise NodeAlreadyExistsError("Node at %s already exists "
 
                        "at %s" % (node.path, p))
 

	
 
        # Check nodes marked as changed
 
        missing = set(self.changed)
 
        not_changed = set(self.changed)
 
        if self.changed and not parents:
 
            raise NodeDoesNotExistError(str(self.changed[0].path))
 
        for p in parents:
 
            for node in self.changed:
 
                try:
 
                    old = p.get_node(node.path)
 
                    missing.remove(node)
 
                    if old.content != node.content:
 
                        not_changed.remove(node)
 
                except NodeDoesNotExistError:
 
                    pass
 
        if self.changed and missing:
 
            raise NodeDoesNotExistError("Node at %s is missing "
 
                "(parents: %s)" % (node.path, parents))
 

	
 
        if self.changed and not_changed:
 
            raise NodeNotChangedError("Node at %s wasn't actually changed "
 
                "since parents' changesets: %s" % (not_changed.pop().path,
 
                    parents)
 
            )
 

	
 
        # Check nodes marked as removed
 
        if self.removed and not parents:
 
            raise NodeDoesNotExistError("Cannot remove node at %s as there "
 
                "were no parents specified" % self.removed[0].path)
 
        really_removed = set()
 
        for p in parents:
 
            for node in self.removed:
 
                try:
 
                    p.get_node(node.path)
 
                    really_removed.add(node)
 
                except ChangesetError:
 
                    pass
 
        not_removed = set(self.removed) - really_removed
 
        if not_removed:
 
            raise NodeDoesNotExistError("Cannot remove node at %s from "
 
                "following parents: %s" % (not_removed[0], parents))
 

	
 
    def commit(self, message, author, parents=None, branch=None, date=None,
 
            **kwargs):
 
        """
 
        Performs in-memory commit (doesn't check workdir in any way) and
 
        returns newly created ``Changeset``. Updates repository's
 
        ``revisions``.
 

	
 
        .. note::
 
            While overriding this method each backend's should call
 
            ``self.check_integrity(parents)`` in the first place.
 

	
 
        :param message: message of the commit
 
        :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
 
        :param parents: single parent or sequence of parents from which commit
 
          would be derieved
 
        :param date: ``datetime.datetime`` instance. Defaults to
 
          ``datetime.datetime.now()``.
 
        :param branch: branch name, as string. If none given, default backend's
 
          branch would be used.
 

	
 
        :raises ``CommitError``: if any error occurs while committing
 
        """
 
        raise NotImplementedError
 

	
 

	
 
class EmptyChangeset(BaseChangeset):
 
    """
 
    An dummy empty changeset. It's possible to pass hash when creating
 
    an EmptyChangeset
 
    """
 

	
 
    def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
 
                 alias=None):
 
        self._empty_cs = cs
 
        self.revision = -1
 
        self.message = ''
 
        self.author = ''
 
        self.date = ''
 
        self.repository = repo
 
        self.requested_revision = requested_revision
 
        self.alias = alias
 

	
 
    @LazyProperty
 
    def raw_id(self):
 
        """
 
        Returns raw string identifying this changeset, useful for web
 
        representation.
 
        """
 

	
 
        return self._empty_cs
 

	
 
    @LazyProperty
 
    def branch(self):
 
        from rhodecode.lib.vcs.backends import get_backend
 
        return get_backend(self.alias).DEFAULT_BRANCH_NAME
 

	
 
    @LazyProperty
 
    def short_id(self):
 
        return self.raw_id[:12]
 

	
 
    def get_file_changeset(self, path):
 
        return self
 

	
 
    def get_file_content(self, path):
 
        return u''
 

	
 
    def get_file_size(self, path):
 
        return 0
rhodecode/lib/vcs/backends/git/changeset.py
Show inline comments
 
import re
 
from itertools import chain
 
from dulwich import objects
 
from subprocess import Popen, PIPE
 
from rhodecode.lib.vcs.conf import settings
 
from rhodecode.lib.vcs.exceptions import RepositoryError
 
from rhodecode.lib.vcs.exceptions import ChangesetError
 
from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
 
from rhodecode.lib.vcs.exceptions import VCSError
 
from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
 
from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError
 
from rhodecode.lib.vcs.backends.base import BaseChangeset
 
from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, RemovedFileNode
 
from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, \
 
    RemovedFileNode, SubModuleNode
 
from rhodecode.lib.vcs.utils import safe_unicode
 
from rhodecode.lib.vcs.utils import date_fromtimestamp
 
from rhodecode.lib.vcs.utils.lazy import LazyProperty
 

	
 

	
 
class GitChangeset(BaseChangeset):
 
    """
 
    Represents state of the repository at single revision.
 
    """
 

	
 
    def __init__(self, repository, revision):
 
        self._stat_modes = {}
 
        self.repository = repository
 
        self.raw_id = revision
 
        self.revision = repository.revisions.index(revision)
 

	
 
        self.short_id = self.raw_id[:12]
 
        self.id = self.raw_id
 
        try:
 
            commit = self.repository._repo.get_object(self.raw_id)
 
        except KeyError:
 
            raise RepositoryError("Cannot get object with id %s" % self.raw_id)
 
        self._commit = commit
 
        self._tree_id = commit.tree
 

	
 
        try:
 
            self.message = safe_unicode(commit.message[:-1])
 
            # Always strip last eol
 
        except UnicodeDecodeError:
 
            self.message = commit.message[:-1].decode(commit.encoding
 
                or 'utf-8')
 
        #self.branch = None
 
        self.tags = []
 
        #tree = self.repository.get_object(self._tree_id)
 
        self.nodes = {}
 
        self._paths = {}
 

	
 
    @LazyProperty
 
    def author(self):
 
        return safe_unicode(self._commit.committer)
 

	
 
    @LazyProperty
 
    def date(self):
 
        return date_fromtimestamp(self._commit.commit_time,
 
                                  self._commit.commit_timezone)
 

	
 
    @LazyProperty
 
    def status(self):
 
        """
 
        Returns modified, added, removed, deleted files for current changeset
 
        """
 
        return self.changed, self.added, self.removed
 

	
 
    @LazyProperty
 
    def branch(self):
 

	
 
        heads = self.repository._heads(reverse=False)
 

	
 
        ref = heads.get(self.raw_id)
 
        if ref:
 
            return safe_unicode(ref)
 

	
 
    def _fix_path(self, path):
 
        """
 
        Paths are stored without trailing slash so we need to get rid off it if
 
        needed.
 
        """
 
        if path.endswith('/'):
 
            path = path.rstrip('/')
 
        return path
 

	
 
    def _get_id_for_path(self, path):
 

	
 
        # FIXME: Please, spare a couple of minutes and make those codes cleaner;
 
        if not path in self._paths:
 
            path = path.strip('/')
 
            # set root tree
 
            tree = self.repository._repo[self._commit.tree]
 
            if path == '':
 
                self._paths[''] = tree.id
 
                return tree.id
 
            splitted = path.split('/')
 
            dirs, name = splitted[:-1], splitted[-1]
 
            curdir = ''
 

	
 
            # initially extract things from root dir
 
            for item, stat, id in tree.iteritems():
 
                if curdir:
 
                    name = '/'.join((curdir, item))
 
                else:
 
                    name = item
 
                self._paths[name] = id
 
                self._stat_modes[name] = stat
 

	
 
            for dir in dirs:
 
                if curdir:
 
                    curdir = '/'.join((curdir, dir))
 
                else:
 
                    curdir = dir
 
                dir_id = None
 
                for item, stat, id in tree.iteritems():
 
                    if dir == item:
 
                        dir_id = id
 
                if dir_id:
 
                    # Update tree
 
                    tree = self.repository._repo[dir_id]
 
                    if not isinstance(tree, objects.Tree):
 
                        raise ChangesetError('%s is not a directory' % curdir)
 
                else:
 
                    raise ChangesetError('%s have not been found' % curdir)
 

	
 
                # cache all items from the given traversed tree
 
                for item, stat, id in tree.iteritems():
 
                    if curdir:
 
                        name = '/'.join((curdir, item))
 
                    else:
 
                        name = item
 
                    self._paths[name] = id
 
                    self._stat_modes[name] = stat
 
            if not path in self._paths:
 
                raise NodeDoesNotExistError("There is no file nor directory "
 
                    "at the given path %r at revision %r"
 
                    % (path, self.short_id))
 
        return self._paths[path]
 

	
 
    def _get_kind(self, path):
 
        id = self._get_id_for_path(path)
 
        obj = self.repository._repo[id]
 
        if isinstance(obj, objects.Blob):
 
            return NodeKind.FILE
 
        elif isinstance(obj, objects.Tree):
 
            return NodeKind.DIR
 

	
 
    def _get_file_nodes(self):
 
        return chain(*(t[2] for t in self.walk()))
 

	
 
    @LazyProperty
 
    def parents(self):
 
        """
 
        Returns list of parents changesets.
 
        """
 
        return [self.repository.get_changeset(parent)
 
            for parent in self._commit.parents]
 

	
 
    def next(self, branch=None):
 

	
 
        if branch and self.branch != branch:
 
            raise VCSError('Branch option used on changeset not belonging '
 
                           'to that branch')
 

	
 
        def _next(changeset, branch):
 
            try:
 
                next_ = changeset.revision + 1
 
                next_rev = changeset.repository.revisions[next_]
 
            except IndexError:
 
                raise ChangesetDoesNotExistError
 
            cs = changeset.repository.get_changeset(next_rev)
 

	
 
            if branch and branch != cs.branch:
 
                return _next(cs, branch)
 

	
 
            return cs
 

	
 
        return _next(self, branch)
 

	
 
    def prev(self, branch=None):
 
        if branch and self.branch != branch:
 
            raise VCSError('Branch option used on changeset not belonging '
 
                           'to that branch')
 

	
 
        def _prev(changeset, branch):
 
            try:
 
                prev_ = changeset.revision - 1
 
                if prev_ < 0:
 
                    raise IndexError
 
                prev_rev = changeset.repository.revisions[prev_]
 
            except IndexError:
 
                raise ChangesetDoesNotExistError
 

	
 
            cs = changeset.repository.get_changeset(prev_rev)
 

	
 
            if branch and branch != cs.branch:
 
                return _prev(cs, branch)
 

	
 
            return cs
 

	
 
        return _prev(self, branch)
 

	
 
    def get_file_mode(self, path):
 
        """
 
        Returns stat mode of the file at the given ``path``.
 
        """
 
        # ensure path is traversed
 
        self._get_id_for_path(path)
 
        return self._stat_modes[path]
 

	
 
    def get_file_content(self, path):
 
        """
 
        Returns content of the file at given ``path``.
 
        """
 
        id = self._get_id_for_path(path)
 
        blob = self.repository._repo[id]
 
        return blob.as_pretty_string()
 

	
 
    def get_file_size(self, path):
 
        """
 
        Returns size of the file at given ``path``.
 
        """
 
        id = self._get_id_for_path(path)
 
        blob = self.repository._repo[id]
 
        return blob.raw_length()
 

	
 
    def get_file_changeset(self, path):
 
        """
 
        Returns last commit of the file at the given ``path``.
 
        """
 
        node = self.get_node(path)
 
        return node.history[0]
 

	
 
    def get_file_history(self, path):
 
        """
 
        Returns history of file as reversed list of ``Changeset`` objects for
 
        which file at given ``path`` has been modified.
 

	
 
        TODO: This function now uses os underlying 'git' and 'grep' commands
 
        which is generally not good. Should be replaced with algorithm
 
        iterating commits.
 
        """
 
        cmd = 'log --pretty="format: %%H" --name-status -p %s -- "%s"' % (
 
                  self.id, path
 
               )
 
        so, se = self.repository.run_git_command(cmd)
 
        ids = re.findall(r'\w{40}', so)
 
        return [self.repository.get_changeset(id) for id in ids]
 

	
 
    def get_file_annotate(self, path):
 
        """
 
        Returns a list of three element tuples with lineno,changeset and line
 

	
 
        TODO: This function now uses os underlying 'git' command which is
 
        generally not good. Should be replaced with algorithm iterating
 
        commits.
 
        """
 
        cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
 
        # -l     ==> outputs long shas (and we need all 40 characters)
 
        # --root ==> doesn't put '^' character for bounderies
 
        # -r sha ==> blames for the given revision
 
        so, se = self.repository.run_git_command(cmd)
 
        annotate = []
 
        for i, blame_line in enumerate(so.split('\n')[:-1]):
 
            ln_no = i + 1
 
            id, line = re.split(r' \(.+?\) ', blame_line, 1)
 
            annotate.append((ln_no, self.repository.get_changeset(id), line))
 
        return annotate
 

	
 
    def fill_archive(self, stream=None, kind='tgz', prefix=None,
 
                     subrepos=False):
 
        """
 
        Fills up given stream.
 

	
 
        :param stream: file like object.
 
        :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
 
            Default: ``tgz``.
 
        :param prefix: name of root directory in archive.
 
            Default is repository name and changeset's raw_id joined with dash
 
            (``repo-tip.<KIND>``).
 
        :param subrepos: include subrepos in this archive.
 

	
 
        :raise ImproperArchiveTypeError: If given kind is wrong.
 
        :raise VcsError: If given stream is None
 

	
 
        """
 
        allowed_kinds = settings.ARCHIVE_SPECS.keys()
 
        if kind not in allowed_kinds:
 
            raise ImproperArchiveTypeError('Archive kind not supported use one'
 
                'of %s', allowed_kinds)
 

	
 
        if prefix is None:
 
            prefix = '%s-%s' % (self.repository.name, self.short_id)
 
        elif prefix.startswith('/'):
 
            raise VCSError("Prefix cannot start with leading slash")
 
        elif prefix.strip() == '':
 
            raise VCSError("Prefix cannot be empty")
 

	
 
        if kind == 'zip':
 
            frmt = 'zip'
 
        else:
 
            frmt = 'tar'
 
        cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
 
            self.raw_id)
 
        if kind == 'tgz':
 
            cmd += ' | gzip -9'
 
        elif kind == 'tbz2':
 
            cmd += ' | bzip2 -9'
 

	
 
        if stream is None:
 
            raise VCSError('You need to pass in a valid stream for filling'
 
                           ' with archival data')
 
        popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
 
            cwd=self.repository.path)
 

	
 
        buffer_size = 1024 * 8
 
        chunk = popen.stdout.read(buffer_size)
 
        while chunk:
 
            stream.write(chunk)
 
            chunk = popen.stdout.read(buffer_size)
 
        # Make sure all descriptors would be read
 
        popen.communicate()
 

	
 
    def get_nodes(self, path):
 
        if self._get_kind(path) != NodeKind.DIR:
 
            raise ChangesetError("Directory does not exist for revision %r at "
 
                " %r" % (self.revision, path))
 
        path = self._fix_path(path)
 
        id = self._get_id_for_path(path)
 
        tree = self.repository._repo[id]
 
        dirnodes = []
 
        filenodes = []
 
        als = self.repository.alias
 
        for name, stat, id in tree.iteritems():
 
            if objects.S_ISGITLINK(stat):
 
                dirnodes.append(SubModuleNode(name, url=None, changeset=id,
 
                                              alias=als))
 
                continue
 

	
 
            obj = self.repository._repo.get_object(id)
 
            if path != '':
 
                obj_path = '/'.join((path, name))
 
            else:
 
                obj_path = name
 
            if obj_path not in self._stat_modes:
 
                self._stat_modes[obj_path] = stat
 
            if isinstance(obj, objects.Tree):
 
                dirnodes.append(DirNode(obj_path, changeset=self))
 
            elif isinstance(obj, objects.Blob):
 
                filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
 
            else:
 
                raise ChangesetError("Requested object should be Tree "
 
                                     "or Blob, is %r" % type(obj))
 
        nodes = dirnodes + filenodes
 
        for node in nodes:
 
            if not node.path in self.nodes:
 
                self.nodes[node.path] = node
 
        nodes.sort()
 
        return nodes
 

	
 
    def get_node(self, path):
 
        if isinstance(path, unicode):
 
            path = path.encode('utf-8')
 
        path = self._fix_path(path)
 
        if not path in self.nodes:
 
            try:
 
                id = self._get_id_for_path(path)
 
                id_ = self._get_id_for_path(path)
 
            except ChangesetError:
 
                raise NodeDoesNotExistError("Cannot find one of parents' "
 
                    "directories for a given path: %s" % path)
 
            obj = self.repository._repo.get_object(id)
 
            if isinstance(obj, objects.Tree):
 
                if path == '':
 
                    node = RootNode(changeset=self)
 

	
 
            als = self.repository.alias
 
            _GL = lambda m: m and objects.S_ISGITLINK(m)
 
            if _GL(self._stat_modes.get(path)):
 
                node = SubModuleNode(path, url=None, changeset=id_, alias=als)
 
            else:
 
                obj = self.repository._repo.get_object(id_)
 

	
 
                if isinstance(obj, objects.Tree):
 
                    if path == '':
 
                        node = RootNode(changeset=self)
 
                    else:
 
                        node = DirNode(path, changeset=self)
 
                    node._tree = obj
 
                elif isinstance(obj, objects.Blob):
 
                    node = FileNode(path, changeset=self)
 
                    node._blob = obj
 
                else:
 
                    node = DirNode(path, changeset=self)
 
                node._tree = obj
 
            elif isinstance(obj, objects.Blob):
 
                node = FileNode(path, changeset=self)
 
                node._blob = obj
 
            else:
 
                raise NodeDoesNotExistError("There is no file nor directory "
 
                    "at the given path %r at revision %r"
 
                    % (path, self.short_id))
 
                    raise NodeDoesNotExistError("There is no file nor directory "
 
                        "at the given path %r at revision %r"
 
                        % (path, self.short_id))
 
            # cache node
 
            self.nodes[path] = node
 
        return self.nodes[path]
 

	
 
    @LazyProperty
 
    def affected_files(self):
 
        """
 
        Get's a fast accessible file changes for given changeset
 
        """
 

	
 
        return self.added + self.changed
 

	
 
    @LazyProperty
 
    def _diff_name_status(self):
 
        output = []
 
        for parent in self.parents:
 
            cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id, self.raw_id)
 
            so, se = self.repository.run_git_command(cmd)
 
            output.append(so.strip())
 
        return '\n'.join(output)
 

	
 
    def _get_paths_for_status(self, status):
 
        """
 
        Returns sorted list of paths for given ``status``.
 

	
 
        :param status: one of: *added*, *modified* or *deleted*
 
        """
 
        paths = set()
 
        char = status[0].upper()
 
        for line in self._diff_name_status.splitlines():
 
            if not line:
 
                continue
 

	
 
            if line.startswith(char):
 
                splitted = line.split(char, 1)
 
                if not len(splitted) == 2:
 
                    raise VCSError("Couldn't parse diff result:\n%s\n\n and "
 
                        "particularly that line: %s" % (self._diff_name_status,
 
                        line))
 
                _path = splitted[1].strip()
 
                paths.add(_path)
 

	
 
        return sorted(paths)
 

	
 
    @LazyProperty
 
    def added(self):
 
        """
 
        Returns list of added ``FileNode`` objects.
 
        """
 
        if not self.parents:
 
            return list(self._get_file_nodes())
 
        return [self.get_node(path) for path in self._get_paths_for_status('added')]
 

	
 
    @LazyProperty
 
    def changed(self):
 
        """
 
        Returns list of modified ``FileNode`` objects.
 
        """
 
        if not self.parents:
 
            return []
 
        return [self.get_node(path) for path in self._get_paths_for_status('modified')]
 

	
 
    @LazyProperty
 
    def removed(self):
 
        """
 
        Returns list of removed ``FileNode`` objects.
 
        """
 
        if not self.parents:
 
            return []
 
        return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')]
rhodecode/lib/vcs/backends/git/repository.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
"""
 
    vcs.backends.git
 
    ~~~~~~~~~~~~~~~~
 

	
 
    Git backend implementation.
 

	
 
    :created_on: Apr 8, 2010
 
    :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
 
"""
 

	
 
import os
 
import re
 
import time
 
import posixpath
 
from dulwich.repo import Repo, NotGitRepository
 
#from dulwich.config import ConfigFile
 
from string import Template
 
from subprocess import Popen, PIPE
 
from rhodecode.lib.vcs.backends.base import BaseRepository
 
from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
 
from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
 
from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
 
from rhodecode.lib.vcs.exceptions import RepositoryError
 
from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
 
from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
 
from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
 
from rhodecode.lib.vcs.utils.lazy import LazyProperty
 
from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
 
from rhodecode.lib.vcs.utils.paths import abspath
 
from rhodecode.lib.vcs.utils.paths import get_user_home
 
from .workdir import GitWorkdir
 
from .changeset import GitChangeset
 
from .inmemory import GitInMemoryChangeset
 
from .config import ConfigFile
 

	
 

	
 
class GitRepository(BaseRepository):
 
    """
 
    Git repository backend.
 
    """
 
    DEFAULT_BRANCH_NAME = 'master'
 
    scm = 'git'
 

	
 
    def __init__(self, repo_path, create=False, src_url=None,
 
                 update_after_clone=False, bare=False):
 

	
 
        self.path = abspath(repo_path)
 
        self._repo = self._get_repo(create, src_url, update_after_clone, bare)
 
        #temporary set that to now at later we will move it to constructor
 
        baseui = None
 
        if baseui is None:
 
            from mercurial.ui import ui
 
            baseui = ui()
 
        # patch the instance of GitRepo with an "FAKE" ui object to add 
 
        # patch the instance of GitRepo with an "FAKE" ui object to add
 
        # compatibility layer with Mercurial
 
        setattr(self._repo, 'ui', baseui)
 

	
 
        try:
 
            self.head = self._repo.head()
 
        except KeyError:
 
            self.head = None
 

	
 
        self._config_files = [
 
            bare and abspath(self.path, 'config') or abspath(self.path, '.git',
 
                'config'),
 
            abspath(get_user_home(), '.gitconfig'),
 
        ]
 

	
 
    @LazyProperty
 
    def revisions(self):
 
        """
 
        Returns list of revisions' ids, in ascending order.  Being lazy
 
        attribute allows external tools to inject shas from cache.
 
        """
 
        return self._get_all_revisions()
 

	
 
    def run_git_command(self, cmd):
 
        """
 
        Runs given ``cmd`` as git command and returns tuple
 
        (returncode, stdout, stderr).
 

	
 
        .. note::
 
           This method exists only until log/blame functionality is implemented
 
           at Dulwich (see https://bugs.launchpad.net/bugs/645142). Parsing
 
           os command's output is road to hell...
 

	
 
        :param cmd: git command to be executed
 
        """
 

	
 
        _copts = ['-c', 'core.quotepath=false', ]
 
        _str_cmd = False
 
        if isinstance(cmd, basestring):
 
            cmd = [cmd]
 
            _str_cmd = True
 

	
 
        cmd = ['GIT_CONFIG_NOGLOBAL=1', 'git'] + _copts + cmd
 
        if _str_cmd:
 
            cmd = ' '.join(cmd)
 
        try:
 
            opts = dict(
 
                shell=isinstance(cmd, basestring),
 
                stdout=PIPE,
 
                stderr=PIPE)
 
            if os.path.isdir(self.path):
 
                opts['cwd'] = self.path
 
            p = Popen(cmd, **opts)
 
        except OSError, err:
 
            raise RepositoryError("Couldn't run git command (%s).\n"
 
                "Original error was:%s" % (cmd, err))
 
        so, se = p.communicate()
 
        if not se.startswith("fatal: bad default revision 'HEAD'") and \
 
            p.returncode != 0:
 
            raise RepositoryError("Couldn't run git command (%s).\n"
 
                "stderr:\n%s" % (cmd, se))
 
        return so, se
 

	
 
    def _check_url(self, url):
 
        """
 
        Functon will check given url and try to verify if it's a valid
 
        link. Sometimes it may happened that mercurial will issue basic
 
        auth request that can cause whole API to hang when used from python
 
        or other external calls.
 

	
 
        On failures it'll raise urllib2.HTTPError
 
        """
 

	
 
        #TODO: implement this
 
        pass
 

	
 
    def _get_repo(self, create, src_url=None, update_after_clone=False,
 
            bare=False):
 
        if create and os.path.exists(self.path):
 
            raise RepositoryError("Location already exist")
 
        if src_url and not create:
 
            raise RepositoryError("Create should be set to True if src_url is "
 
                                  "given (clone operation creates repository)")
 
        try:
 
            if create and src_url:
 
                self._check_url(src_url)
 
                self.clone(src_url, update_after_clone, bare)
 
                return Repo(self.path)
 
            elif create:
 
                os.mkdir(self.path)
 
                if bare:
 
                    return Repo.init_bare(self.path)
 
                else:
 
                    return Repo.init(self.path)
 
            else:
 
                return Repo(self.path)
 
        except (NotGitRepository, OSError), err:
 
            raise RepositoryError(err)
 

	
 
    def _get_all_revisions(self):
 
        cmd = 'rev-list --all --date-order'
 
        try:
 
            so, se = self.run_git_command(cmd)
 
        except RepositoryError:
 
            # Can be raised for empty repositories
 
            return []
 
        revisions = so.splitlines()
 
        revisions.reverse()
 
        return revisions
 

	
 
    def _get_revision(self, revision):
 
        """
 
        For git backend we always return integer here. This way we ensure
 
        that changset's revision attribute would become integer.
 
        """
 
        pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
 
        is_bstr = lambda o: isinstance(o, (str, unicode))
 
        is_null = lambda o: len(o) == revision.count('0')
 

	
 
        if len(self.revisions) == 0:
 
            raise EmptyRepositoryError("There are no changesets yet")
 

	
 
        if revision in (None, '', 'tip', 'HEAD', 'head', -1):
 
            revision = self.revisions[-1]
 

	
 
        if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
 
            or isinstance(revision, int) or is_null(revision)):
 
            try:
 
                revision = self.revisions[int(revision)]
 
            except:
 
                raise ChangesetDoesNotExistError("Revision %r does not exist "
 
                    "for this repository %s" % (revision, self))
 

	
 
        elif is_bstr(revision):
 
            if not pattern.match(revision) or revision not in self.revisions:
 
                raise ChangesetDoesNotExistError("Revision %r does not exist "
 
                    "for this repository %s" % (revision, self))
 

	
 
        # Ensure we return full id
 
        if not pattern.match(str(revision)):
 
            raise ChangesetDoesNotExistError("Given revision %r not recognized"
 
                % revision)
 
        return revision
 

	
 
    def _get_archives(self, archive_name='tip'):
 

	
 
        for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
 
                yield {"type": i[0], "extension": i[1], "node": archive_name}
 

	
 
    def _get_url(self, url):
 
        """
 
        Returns normalized url. If schema is not given, would fall to
 
        filesystem (``file:///``) schema.
 
        """
 
        url = str(url)
 
        if url != 'default' and not '://' in url:
 
            url = ':///'.join(('file', url))
 
        return url
 

	
 
    @LazyProperty
 
    def name(self):
 
        return os.path.basename(self.path)
 

	
 
    @LazyProperty
 
    def last_change(self):
 
        """
 
        Returns last change made on this repository as datetime object
 
        """
 
        return date_fromtimestamp(self._get_mtime(), makedate()[1])
 

	
 
    def _get_mtime(self):
 
        try:
 
            return time.mktime(self.get_changeset().date.timetuple())
 
        except RepositoryError:
 
            # fallback to filesystem
 
            in_path = os.path.join(self.path, '.git', "index")
 
            he_path = os.path.join(self.path, '.git', "HEAD")
 
            if os.path.exists(in_path):
 
                return os.stat(in_path).st_mtime
 
            else:
 
                return os.stat(he_path).st_mtime
 

	
 
    @LazyProperty
 
    def description(self):
 
        undefined_description = u'unknown'
 
        description_path = os.path.join(self.path, '.git', 'description')
 
        if os.path.isfile(description_path):
 
            return safe_unicode(open(description_path).read())
 
        else:
 
            return undefined_description
 

	
 
    @LazyProperty
 
    def contact(self):
 
        undefined_contact = u'Unknown'
 
        return undefined_contact
 

	
 
    @property
 
    def branches(self):
 
        if not self.revisions:
 
            return {}
 
        refs = self._repo.refs.as_dict()
 
        sortkey = lambda ctx: ctx[0]
 
        _branches = [('/'.join(ref.split('/')[2:]), head)
 
            for ref, head in refs.items()
 
            if ref.startswith('refs/heads/') and not ref.endswith('/HEAD')]
 
        return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
 

	
 
    def _heads(self, reverse=False):
 
        refs = self._repo.get_refs()
 
        heads = {}
 

	
 
        for key, val in refs.items():
 
            for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
 
                if key.startswith(ref_key):
 
                    n = key[len(ref_key):]
 
                    if n not in ['HEAD']:
 
                        heads[n] = val
 

	
 
        return heads if reverse else dict((y,x) for x,y in heads.iteritems())
 

	
 
    def _get_tags(self):
 
        if not self.revisions:
 
            return {}
 
        sortkey = lambda ctx: ctx[0]
 
        _tags = [('/'.join(ref.split('/')[2:]), head) for ref, head in
 
            self._repo.get_refs().items() if ref.startswith('refs/tags/')]
 
        return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
 

	
 
    @LazyProperty
 
    def tags(self):
 
        return self._get_tags()
 

	
 
    def tag(self, name, user, revision=None, message=None, date=None,
 
            **kwargs):
 
        """
 
        Creates and returns a tag for the given ``revision``.
 

	
 
        :param name: name for new tag
 
        :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
 
        :param revision: changeset id for which new tag would be created
 
        :param message: message of the tag's commit
 
        :param date: date of tag's commit
 

	
 
        :raises TagAlreadyExistError: if tag with same name already exists
 
        """
 
        if name in self.tags:
 
            raise TagAlreadyExistError("Tag %s already exists" % name)
 
        changeset = self.get_changeset(revision)
 
        message = message or "Added tag %s for commit %s" % (name,
 
            changeset.raw_id)
 
        self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
 

	
 
        self.tags = self._get_tags()
 
        return changeset
 

	
 
    def remove_tag(self, name, user, message=None, date=None):
 
        """
 
        Removes tag with the given ``name``.
 

	
 
        :param name: name of the tag to be removed
 
        :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
 
        :param message: message of the tag's removal commit
 
        :param date: date of tag's removal commit
 

	
 
        :raises TagDoesNotExistError: if tag with given name does not exists
 
        """
 
        if name not in self.tags:
 
            raise TagDoesNotExistError("Tag %s does not exist" % name)
 
        tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
 
        try:
 
            os.remove(tagpath)
 
            self.tags = self._get_tags()
 
        except OSError, e:
 
            raise RepositoryError(e.strerror)
 

	
 
    def get_changeset(self, revision=None):
 
        """
 
        Returns ``GitChangeset`` object representing commit from git repository
 
        at the given revision or head (most recent commit) if None given.
 
        """
 
        if isinstance(revision, GitChangeset):
 
            return revision
 
        revision = self._get_revision(revision)
 
        changeset = GitChangeset(repository=self, revision=revision)
 
        return changeset
 

	
 
    def get_changesets(self, start=None, end=None, start_date=None,
 
           end_date=None, branch_name=None, reverse=False):
 
        """
 
        Returns iterator of ``GitChangeset`` objects from start to end (both
 
        are inclusive), in ascending date order (unless ``reverse`` is set).
 

	
 
        :param start: changeset ID, as str; first returned changeset
 
        :param end: changeset ID, as str; last returned changeset
 
        :param start_date: if specified, changesets with commit date less than
 
          ``start_date`` would be filtered out from returned set
 
        :param end_date: if specified, changesets with commit date greater than
 
          ``end_date`` would be filtered out from returned set
 
        :param branch_name: if specified, changesets not reachable from given
 
          branch would be filtered out from returned set
 
        :param reverse: if ``True``, returned generator would be reversed
 
          (meaning that returned changesets would have descending date order)
 

	
 
        :raise BranchDoesNotExistError: If given ``branch_name`` does not
 
            exist.
 
        :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
 
          ``end`` could not be found.
 

	
 
        """
 
        if branch_name and branch_name not in self.branches:
 
            raise BranchDoesNotExistError("Branch '%s' not found" \
 
                                          % branch_name)
 
        # %H at format means (full) commit hash, initial hashes are retrieved
 
        # in ascending date order
 
        cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
 
        cmd_params = {}
 
        if start_date:
 
            cmd_template += ' --since "$since"'
 
            cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
 
        if end_date:
 
            cmd_template += ' --until "$until"'
 
            cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
 
        if branch_name:
 
            cmd_template += ' $branch_name'
 
            cmd_params['branch_name'] = branch_name
 
        else:
 
            cmd_template += ' --all'
 

	
 
        cmd = Template(cmd_template).safe_substitute(**cmd_params)
 
        revs = self.run_git_command(cmd)[0].splitlines()
 
        start_pos = 0
 
        end_pos = len(revs)
 
        if start:
 
            _start = self._get_revision(start)
 
            try:
 
                start_pos = revs.index(_start)
 
            except ValueError:
 
                pass
 

	
 
        if end is not None:
 
            _end = self._get_revision(end)
 
            try:
 
                end_pos = revs.index(_end)
 
            except ValueError:
 
                pass
 

	
 
        if None not in [start, end] and start_pos > end_pos:
 
            raise RepositoryError('start cannot be after end')
 

	
 
        if end_pos is not None:
 
            end_pos += 1
 

	
 
        revs = revs[start_pos:end_pos]
 
        if reverse:
 
            revs = reversed(revs)
 
        for rev in revs:
 
            yield self.get_changeset(rev)
 

	
 
    def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
 
            context=3):
 
                 context=3):
 
        """
 
        Returns (git like) *diff*, as plain text. Shows changes introduced by
 
        ``rev2`` since ``rev1``.
 

	
 
        :param rev1: Entry point from which diff is shown. Can be
 
          ``self.EMPTY_CHANGESET`` - in this case, patch showing all
 
          the changes since empty state of the repository until ``rev2``
 
        :param rev2: Until which revision changes should be shown.
 
        :param ignore_whitespace: If set to ``True``, would not show whitespace
 
          changes. Defaults to ``False``.
 
        :param context: How many lines before/after changed lines should be
 
          shown. Defaults to ``3``.
 
        """
 
        flags = ['-U%s' % context]
 
        if ignore_whitespace:
 
            flags.append('-w')
 

	
 
        if rev1 == self.EMPTY_CHANGESET:
 
            rev2 = self.get_changeset(rev2).raw_id
 
            cmd = ' '.join(['show'] + flags + [rev2])
 
        else:
 
            rev1 = self.get_changeset(rev1).raw_id
 
            rev2 = self.get_changeset(rev2).raw_id
 
            cmd = ' '.join(['diff'] + flags + [rev1, rev2])
 

	
 
        if path:
 
            cmd += ' -- "%s"' % path
 
        stdout, stderr = self.run_git_command(cmd)
 
        # If we used 'show' command, strip first few lines (until actual diff
 
        # starts)
 
        if rev1 == self.EMPTY_CHANGESET:
 
            lines = stdout.splitlines()
 
            x = 0
 
            for line in lines:
 
                if line.startswith('diff'):
 
                    break
 
                x += 1
 
            # Append new line just like 'diff' command do
 
            stdout = '\n'.join(lines[x:]) + '\n'
 
        return stdout
 

	
 
    @LazyProperty
 
    def in_memory_changeset(self):
 
        """
 
        Returns ``GitInMemoryChangeset`` object for this repository.
 
        """
 
        return GitInMemoryChangeset(self)
 

	
 
    def clone(self, url, update_after_clone=True, bare=False):
 
        """
 
        Tries to clone changes from external location.
 

	
 
        :param update_after_clone: If set to ``False``, git won't checkout
 
          working directory
 
        :param bare: If set to ``True``, repository would be cloned into
 
          *bare* git repository (no working directory at all).
 
        """
 
        url = self._get_url(url)
 
        cmd = ['clone']
 
        if bare:
 
            cmd.append('--bare')
 
        elif not update_after_clone:
 
            cmd.append('--no-checkout')
 
        cmd += ['--', '"%s"' % url, '"%s"' % self.path]
 
        cmd = ' '.join(cmd)
 
        # If error occurs run_git_command raises RepositoryError already
 
        self.run_git_command(cmd)
 

	
 
    def pull(self, url):
 
        """
 
        Tries to pull changes from external location.
 
        """
 
        url = self._get_url(url)
 
        cmd = ['pull']
 
        cmd.append("--ff-only")
 
        cmd.append(url)
 
        cmd = ' '.join(cmd)
 
        # If error occurs run_git_command raises RepositoryError already
 
        self.run_git_command(cmd)
 

	
 
    @LazyProperty
 
    def workdir(self):
 
        """
 
        Returns ``Workdir`` instance for this repository.
 
        """
 
        return GitWorkdir(self)
 

	
 
    def get_config_value(self, section, name, config_file=None):
 
        """
 
        Returns configuration value for a given [``section``] and ``name``.
 

	
 
        :param section: Section we want to retrieve value from
 
        :param name: Name of configuration we want to retrieve
 
        :param config_file: A path to file which should be used to retrieve
 
          configuration from (might also be a list of file paths)
 
        """
 
        if config_file is None:
 
            config_file = []
 
        elif isinstance(config_file, basestring):
 
            config_file = [config_file]
 

	
 
        def gen_configs():
 
            for path in config_file + self._config_files:
 
                try:
 
                    yield ConfigFile.from_path(path)
 
                except (IOError, OSError, ValueError):
 
                    continue
 

	
 
        for config in gen_configs():
 
            try:
 
                return config.get(section, name)
 
            except KeyError:
 
                continue
 
        return None
 

	
 
    def get_user_name(self, config_file=None):
 
        """
 
        Returns user's name from global configuration file.
 

	
 
        :param config_file: A path to file which should be used to retrieve
 
          configuration from (might also be a list of file paths)
 
        """
 
        return self.get_config_value('user', 'name', config_file)
 

	
 
    def get_user_email(self, config_file=None):
 
        """
 
        Returns user's email from global configuration file.
 

	
 
        :param config_file: A path to file which should be used to retrieve
 
          configuration from (might also be a list of file paths)
 
        """
 
        return self.get_config_value('user', 'email', config_file)
rhodecode/lib/vcs/backends/hg/changeset.py
Show inline comments
 
import os
 
import posixpath
 

	
 
from rhodecode.lib.vcs.backends.base import BaseChangeset
 
from rhodecode.lib.vcs.conf import settings
 
from rhodecode.lib.vcs.exceptions import  ChangesetDoesNotExistError, \
 
    ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, VCSError
 
from rhodecode.lib.vcs.nodes import AddedFileNodesGenerator, ChangedFileNodesGenerator, \
 
    DirNode, FileNode, NodeKind, RemovedFileNodesGenerator, RootNode
 
from rhodecode.lib.vcs.nodes import AddedFileNodesGenerator, \
 
    ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, \
 
    RemovedFileNodesGenerator, RootNode, SubModuleNode
 

	
 
from rhodecode.lib.vcs.utils import safe_str, safe_unicode, date_fromtimestamp
 
from rhodecode.lib.vcs.utils.lazy import LazyProperty
 
from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
 

	
 
from ...utils.hgcompat import archival, hex
 

	
 

	
 
class MercurialChangeset(BaseChangeset):
 
    """
 
    Represents state of the repository at the single revision.
 
    """
 

	
 
    def __init__(self, repository, revision):
 
        self.repository = repository
 
        self.raw_id = revision
 
        self._ctx = repository._repo[revision]
 
        self.revision = self._ctx._rev
 
        self.nodes = {}
 

	
 
    @LazyProperty
 
    def tags(self):
 
        return map(safe_unicode, self._ctx.tags())
 

	
 
    @LazyProperty
 
    def branch(self):
 
        return  safe_unicode(self._ctx.branch())
 

	
 
    @LazyProperty
 
    def message(self):
 
        return safe_unicode(self._ctx.description())
 

	
 
    @LazyProperty
 
    def author(self):
 
        return safe_unicode(self._ctx.user())
 

	
 
    @LazyProperty
 
    def date(self):
 
        return date_fromtimestamp(*self._ctx.date())
 

	
 
    @LazyProperty
 
    def status(self):
 
        """
 
        Returns modified, added, removed, deleted files for current changeset
 
        """
 
        return self.repository._repo.status(self._ctx.p1().node(),
 
                                            self._ctx.node())
 

	
 
    @LazyProperty
 
    def _file_paths(self):
 
        return list(self._ctx)
 

	
 
    @LazyProperty
 
    def _dir_paths(self):
 
        p = list(set(get_dirs_for_path(*self._file_paths)))
 
        p.insert(0, '')
 
        return p
 

	
 
    @LazyProperty
 
    def _paths(self):
 
        return self._dir_paths + self._file_paths
 

	
 
    @LazyProperty
 
    def id(self):
 
        if self.last:
 
            return u'tip'
 
        return self.short_id
 

	
 
    @LazyProperty
 
    def short_id(self):
 
        return self.raw_id[:12]
 

	
 
    @LazyProperty
 
    def parents(self):
 
        """
 
        Returns list of parents changesets.
 
        """
 
        return [self.repository.get_changeset(parent.rev())
 
                for parent in self._ctx.parents() if parent.rev() >= 0]
 

	
 
    def next(self, branch=None):
 

	
 
        if branch and self.branch != branch:
 
            raise VCSError('Branch option used on changeset not belonging '
 
                           'to that branch')
 

	
 
        def _next(changeset, branch):
 
            try:
 
                next_ = changeset.revision + 1
 
                next_rev = changeset.repository.revisions[next_]
 
            except IndexError:
 
                raise ChangesetDoesNotExistError
 
            cs = changeset.repository.get_changeset(next_rev)
 

	
 
            if branch and branch != cs.branch:
 
                return _next(cs, branch)
 

	
 
            return cs
 

	
 
        return _next(self, branch)
 

	
 
    def prev(self, branch=None):
 
        if branch and self.branch != branch:
 
            raise VCSError('Branch option used on changeset not belonging '
 
                           'to that branch')
 

	
 
        def _prev(changeset, branch):
 
            try:
 
                prev_ = changeset.revision - 1
 
                if prev_ < 0:
 
                    raise IndexError
 
                prev_rev = changeset.repository.revisions[prev_]
 
            except IndexError:
 
                raise ChangesetDoesNotExistError
 

	
 
            cs = changeset.repository.get_changeset(prev_rev)
 

	
 
            if branch and branch != cs.branch:
 
                return _prev(cs, branch)
 

	
 
            return cs
 

	
 
        return _prev(self, branch)
 

	
 
    def _fix_path(self, path):
 
        """
 
        Paths are stored without trailing slash so we need to get rid off it if
 
        needed. Also mercurial keeps filenodes as str so we need to decode
 
        from unicode to str
 
        """
 
        if path.endswith('/'):
 
            path = path.rstrip('/')
 

	
 
        return safe_str(path)
 

	
 
    def _get_kind(self, path):
 
        path = self._fix_path(path)
 
        if path in self._file_paths:
 
            return NodeKind.FILE
 
        elif path in self._dir_paths:
 
            return NodeKind.DIR
 
        else:
 
            raise ChangesetError("Node does not exist at the given path %r"
 
                % (path))
 

	
 
    def _get_filectx(self, path):
 
        path = self._fix_path(path)
 
        if self._get_kind(path) != NodeKind.FILE:
 
            raise ChangesetError("File does not exist for revision %r at "
 
                " %r" % (self.revision, path))
 
        return self._ctx.filectx(path)
 

	
 
    def _extract_submodules(self):
 
        """
 
        returns a dictionary with submodule information from substate file
 
        of hg repository
 
        """
 
        return self._ctx.substate
 

	
 
    def get_file_mode(self, path):
 
        """
 
        Returns stat mode of the file at the given ``path``.
 
        """
 
        fctx = self._get_filectx(path)
 
        if 'x' in fctx.flags():
 
            return 0100755
 
        else:
 
            return 0100644
 

	
 
    def get_file_content(self, path):
 
        """
 
        Returns content of the file at given ``path``.
 
        """
 
        fctx = self._get_filectx(path)
 
        return fctx.data()
 

	
 
    def get_file_size(self, path):
 
        """
 
        Returns size of the file at given ``path``.
 
        """
 
        fctx = self._get_filectx(path)
 
        return fctx.size()
 

	
 
    def get_file_changeset(self, path):
 
        """
 
        Returns last commit of the file at the given ``path``.
 
        """
 
        node = self.get_node(path)
 
        return node.history[0]
 

	
 
    def get_file_history(self, path):
 
        """
 
        Returns history of file as reversed list of ``Changeset`` objects for
 
        which file at given ``path`` has been modified.
 
        """
 
        fctx = self._get_filectx(path)
 
        nodes = [fctx.filectx(x).node() for x in fctx.filelog()]
 
        changesets = [self.repository.get_changeset(hex(node))
 
            for node in reversed(nodes)]
 
        return changesets
 

	
 
    def get_file_annotate(self, path):
 
        """
 
        Returns a list of three element tuples with lineno,changeset and line
 
        """
 
        fctx = self._get_filectx(path)
 
        annotate = []
 
        for i, annotate_data in enumerate(fctx.annotate()):
 
            ln_no = i + 1
 
            annotate.append((ln_no, self.repository\
 
                             .get_changeset(hex(annotate_data[0].node())),
 
                             annotate_data[1],))
 

	
 
        return annotate
 

	
 
    def fill_archive(self, stream=None, kind='tgz', prefix=None,
 
                     subrepos=False):
 
        """
 
        Fills up given stream.
 

	
 
        :param stream: file like object.
 
        :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
 
            Default: ``tgz``.
 
        :param prefix: name of root directory in archive.
 
            Default is repository name and changeset's raw_id joined with dash
 
            (``repo-tip.<KIND>``).
 
        :param subrepos: include subrepos in this archive.
 

	
 
        :raise ImproperArchiveTypeError: If given kind is wrong.
 
        :raise VcsError: If given stream is None
 
        """
 

	
 
        allowed_kinds = settings.ARCHIVE_SPECS.keys()
 
        if kind not in allowed_kinds:
 
            raise ImproperArchiveTypeError('Archive kind not supported use one'
 
                'of %s', allowed_kinds)
 

	
 
        if stream is None:
 
            raise VCSError('You need to pass in a valid stream for filling'
 
                           ' with archival data')
 

	
 
        if prefix is None:
 
            prefix = '%s-%s' % (self.repository.name, self.short_id)
 
        elif prefix.startswith('/'):
 
            raise VCSError("Prefix cannot start with leading slash")
 
        elif prefix.strip() == '':
 
            raise VCSError("Prefix cannot be empty")
 

	
 
        archival.archive(self.repository._repo, stream, self.raw_id,
 
                         kind, prefix=prefix, subrepos=subrepos)
 

	
 
        #stream.close()
 

	
 
        if stream.closed and hasattr(stream, 'name'):
 
            stream = open(stream.name, 'rb')
 
        elif hasattr(stream, 'mode') and 'r' not in stream.mode:
 
            stream = open(stream.name, 'rb')
 
        else:
 
            stream.seek(0)
 

	
 
    def get_nodes(self, path):
 
        """
 
        Returns combined ``DirNode`` and ``FileNode`` objects list representing
 
        state of changeset at the given ``path``. If node at the given ``path``
 
        is not instance of ``DirNode``, ChangesetError would be raised.
 
        """
 

	
 
        if self._get_kind(path) != NodeKind.DIR:
 
            raise ChangesetError("Directory does not exist for revision %r at "
 
                " %r" % (self.revision, path))
 
        path = self._fix_path(path)
 

	
 
        filenodes = [FileNode(f, changeset=self) for f in self._file_paths
 
            if os.path.dirname(f) == path]
 
        dirs = path == '' and '' or [d for d in self._dir_paths
 
            if d and posixpath.dirname(d) == path]
 
        dirnodes = [DirNode(d, changeset=self) for d in dirs
 
            if os.path.dirname(d) == path]
 

	
 
        als = self.repository.alias
 
        for k, vals in self._extract_submodules().iteritems():
 
            #vals = url,rev,type
 
            loc = vals[0]
 
            cs = vals[1]
 
            dirnodes.append(SubModuleNode(k, url=loc, changeset=cs,
 
                                          alias=als))
 
        nodes = dirnodes + filenodes
 
        # cache nodes
 
        for node in nodes:
 
            self.nodes[node.path] = node
 
        nodes.sort()
 

	
 
        return nodes
 

	
 
    def get_node(self, path):
 
        """
 
        Returns ``Node`` object from the given ``path``. If there is no node at
 
        the given ``path``, ``ChangesetError`` would be raised.
 
        """
 

	
 
        path = self._fix_path(path)
 

	
 
        if not path in self.nodes:
 
            if path in self._file_paths:
 
                node = FileNode(path, changeset=self)
 
            elif path in self._dir_paths or path in self._dir_paths:
 
                if path == '':
 
                    node = RootNode(changeset=self)
 
                else:
 
                    node = DirNode(path, changeset=self)
 
            else:
 
                raise NodeDoesNotExistError("There is no file nor directory "
 
                    "at the given path: %r at revision %r"
 
                    % (path, self.short_id))
 
            # cache node
 
            self.nodes[path] = node
 
        return self.nodes[path]
 

	
 
    @LazyProperty
 
    def affected_files(self):
 
        """
 
        Get's a fast accessible file changes for given changeset
 
        """
 
        return self._ctx.files()
 

	
 
    @property
 
    def added(self):
 
        """
 
        Returns list of added ``FileNode`` objects.
 
        """
 
        return AddedFileNodesGenerator([n for n in self.status[1]], self)
 

	
 
    @property
 
    def changed(self):
 
        """
 
        Returns list of modified ``FileNode`` objects.
 
        """
 
        return ChangedFileNodesGenerator([n for n in  self.status[0]], self)
 

	
 
    @property
 
    def removed(self):
 
        """
 
        Returns list of removed ``FileNode`` objects.
 
        """
 
        return RemovedFileNodesGenerator([n for n in self.status[2]], self)
rhodecode/lib/vcs/nodes.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
"""
 
    vcs.nodes
 
    ~~~~~~~~~
 

	
 
    Module holding everything related to vcs nodes.
 

	
 
    :created_on: Apr 8, 2010
 
    :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
 
"""
 
import os
 
import stat
 
import posixpath
 
import mimetypes
 

	
 
from pygments import lexers
 

	
 
from rhodecode.lib.vcs.utils.lazy import LazyProperty
 
from rhodecode.lib.vcs.utils import safe_unicode
 
from rhodecode.lib.vcs.utils import safe_unicode, safe_str
 
from rhodecode.lib.vcs.exceptions import NodeError
 
from rhodecode.lib.vcs.exceptions import RemovedFileNodeError
 

	
 
from pygments import lexers
 
from rhodecode.lib.vcs.backends.base import EmptyChangeset
 

	
 

	
 
class NodeKind:
 
    SUBMODULE = -1
 
    DIR = 1
 
    FILE = 2
 

	
 

	
 
class NodeState:
 
    ADDED = u'added'
 
    CHANGED = u'changed'
 
    NOT_CHANGED = u'not changed'
 
    REMOVED = u'removed'
 

	
 

	
 
class NodeGeneratorBase(object):
 
    """
 
    Base class for removed added and changed filenodes, it's a lazy generator
 
    class that will create filenodes only on iteration or call
 

	
 
    The len method doesn't need to create filenodes at all
 
    """
 

	
 
    def __init__(self, current_paths, cs):
 
        self.cs = cs
 
        self.current_paths = current_paths
 

	
 
    def __call__(self):
 
        return [n for n in self]
 

	
 
    def __getslice__(self, i, j):
 
        for p in self.current_paths[i:j]:
 
            yield self.cs.get_node(p)
 

	
 
    def __len__(self):
 
        return len(self.current_paths)
 

	
 
    def __iter__(self):
 
        for p in self.current_paths:
 
            yield self.cs.get_node(p)
 

	
 

	
 
class AddedFileNodesGenerator(NodeGeneratorBase):
 
    """
 
    Class holding Added files for current changeset
 
    """
 
    pass
 

	
 

	
 
class ChangedFileNodesGenerator(NodeGeneratorBase):
 
    """
 
    Class holding Changed files for current changeset
 
    """
 
    pass
 

	
 

	
 
class RemovedFileNodesGenerator(NodeGeneratorBase):
 
    """
 
    Class holding removed files for current changeset
 
    """
 
    def __iter__(self):
 
        for p in self.current_paths:
 
            yield RemovedFileNode(path=p)
 

	
 
    def __getslice__(self, i, j):
 
        for p in self.current_paths[i:j]:
 
            yield RemovedFileNode(path=p)
 

	
 

	
 
class Node(object):
 
    """
 
    Simplest class representing file or directory on repository.  SCM backends
 
    should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
 
    directly.
 

	
 
    Node's ``path`` cannot start with slash as we operate on *relative* paths
 
    only. Moreover, every single node is identified by the ``path`` attribute,
 
    so it cannot end with slash, too. Otherwise, path could lead to mistakes.
 
    """
 

	
 
    def __init__(self, path, kind):
 
        if path.startswith('/'):
 
            raise NodeError("Cannot initialize Node objects with slash at "
 
                "the beginning as only relative paths are supported")
 
        self.path = path.rstrip('/')
 
        if path == '' and kind != NodeKind.DIR:
 
            raise NodeError("Only DirNode and its subclasses may be "
 
                            "initialized with empty path")
 
        self.kind = kind
 
        #self.dirs, self.files = [], []
 
        if self.is_root() and not self.is_dir():
 
            raise NodeError("Root node cannot be FILE kind")
 

	
 
    @LazyProperty
 
    def parent(self):
 
        parent_path = self.get_parent_path()
 
        if parent_path:
 
            if self.changeset:
 
                return self.changeset.get_node(parent_path)
 
            return DirNode(parent_path)
 
        return None
 

	
 
    @LazyProperty
 
    def unicode_path(self):
 
        return safe_unicode(self.path)
 

	
 
    @LazyProperty
 
    def name(self):
 
        """
 
        Returns name of the node so if its path
 
        then only last part is returned.
 
        """
 
        return safe_unicode(self.path.rstrip('/').split('/')[-1])
 

	
 
    def _get_kind(self):
 
        return self._kind
 

	
 
    def _set_kind(self, kind):
 
        if hasattr(self, '_kind'):
 
            raise NodeError("Cannot change node's kind")
 
        else:
 
            self._kind = kind
 
            # Post setter check (path's trailing slash)
 
            if self.path.endswith('/'):
 
                raise NodeError("Node's path cannot end with slash")
 

	
 
    kind = property(_get_kind, _set_kind)
 

	
 
    def __cmp__(self, other):
 
        """
 
        Comparator using name of the node, needed for quick list sorting.
 
        """
 
        kind_cmp = cmp(self.kind, other.kind)
 
        if kind_cmp:
 
            return kind_cmp
 
        return cmp(self.name, other.name)
 

	
 
    def __eq__(self, other):
 
        for attr in ['name', 'path', 'kind']:
 
            if getattr(self, attr) != getattr(other, attr):
 
                return False
 
        if self.is_file():
 
            if self.content != other.content:
 
                return False
 
        else:
 
            # For DirNode's check without entering each dir
 
            self_nodes_paths = list(sorted(n.path for n in self.nodes))
 
            other_nodes_paths = list(sorted(n.path for n in self.nodes))
 
            if self_nodes_paths != other_nodes_paths:
 
                return False
 
        return True
 

	
 
    def __nq__(self, other):
 
        return not self.__eq__(other)
 

	
 
    def __repr__(self):
 
        return '<%s %r>' % (self.__class__.__name__, self.path)
 

	
 
    def __str__(self):
 
        return self.__repr__()
 

	
 
    def __unicode__(self):
 
        return self.name
 

	
 
    def get_parent_path(self):
 
        """
 
        Returns node's parent path or empty string if node is root.
 
        """
 
        if self.is_root():
 
            return ''
 
        return posixpath.dirname(self.path.rstrip('/')) + '/'
 

	
 
    def is_file(self):
 
        """
 
        Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
 
        otherwise.
 
        """
 
        return self.kind == NodeKind.FILE
 

	
 
    def is_dir(self):
 
        """
 
        Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
 
        otherwise.
 
        """
 
        return self.kind == NodeKind.DIR
 

	
 
    def is_root(self):
 
        """
 
        Returns ``True`` if node is a root node and ``False`` otherwise.
 
        """
 
        return self.kind == NodeKind.DIR and self.path == ''
 

	
 
    def is_submodule(self):
 
        """
 
        Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
 
        otherwise.
 
        """
 
        return self.kind == NodeKind.SUBMODULE
 

	
 
    @LazyProperty
 
    def added(self):
 
        return self.state is NodeState.ADDED
 

	
 
    @LazyProperty
 
    def changed(self):
 
        return self.state is NodeState.CHANGED
 

	
 
    @LazyProperty
 
    def not_changed(self):
 
        return self.state is NodeState.NOT_CHANGED
 

	
 
    @LazyProperty
 
    def removed(self):
 
        return self.state is NodeState.REMOVED
 

	
 

	
 
class FileNode(Node):
 
    """
 
    Class representing file nodes.
 

	
 
    :attribute: path: path to the node, relative to repostiory's root
 
    :attribute: content: if given arbitrary sets content of the file
 
    :attribute: changeset: if given, first time content is accessed, callback
 
    :attribute: mode: octal stat mode for a node. Default is 0100644.
 
    """
 

	
 
    def __init__(self, path, content=None, changeset=None, mode=None):
 
        """
 
        Only one of ``content`` and ``changeset`` may be given. Passing both
 
        would raise ``NodeError`` exception.
 

	
 
        :param path: relative path to the node
 
        :param content: content may be passed to constructor
 
        :param changeset: if given, will use it to lazily fetch content
 
        :param mode: octal representation of ST_MODE (i.e. 0100644)
 
        """
 

	
 
        if content and changeset:
 
            raise NodeError("Cannot use both content and changeset")
 
        super(FileNode, self).__init__(path, kind=NodeKind.FILE)
 
        self.changeset = changeset
 
        self._content = content
 
        self._mode = mode or 0100644
 

	
 
    @LazyProperty
 
    def mode(self):
 
        """
 
        Returns lazily mode of the FileNode. If ``changeset`` is not set, would
 
        use value given at initialization or 0100644 (default).
 
        """
 
        if self.changeset:
 
            mode = self.changeset.get_file_mode(self.path)
 
        else:
 
            mode = self._mode
 
        return mode
 

	
 
    @property
 
    def content(self):
 
        """
 
        Returns lazily content of the FileNode. If possible, would try to
 
        decode content from UTF-8.
 
        """
 
        if self.changeset:
 
            content = self.changeset.get_file_content(self.path)
 
        else:
 
            content = self._content
 

	
 
        if bool(content and '\0' in content):
 
            return content
 
        return safe_unicode(content)
 

	
 
    @LazyProperty
 
    def size(self):
 
        if self.changeset:
 
            return self.changeset.get_file_size(self.path)
 
        raise NodeError("Cannot retrieve size of the file without related "
 
            "changeset attribute")
 

	
 
    @LazyProperty
 
    def message(self):
 
        if self.changeset:
 
            return self.last_changeset.message
 
        raise NodeError("Cannot retrieve message of the file without related "
 
            "changeset attribute")
 

	
 
    @LazyProperty
 
    def last_changeset(self):
 
        if self.changeset:
 
            return self.changeset.get_file_changeset(self.path)
 
        raise NodeError("Cannot retrieve last changeset of the file without "
 
            "related changeset attribute")
 

	
 
    def get_mimetype(self):
 
        """
 
        Mimetype is calculated based on the file's content. If ``_mimetype``
 
        attribute is available, it will be returned (backends which store
 
        mimetypes or can easily recognize them, should set this private
 
        attribute to indicate that type should *NOT* be calculated).
 
        """
 
        if hasattr(self, '_mimetype'):
 
            if (isinstance(self._mimetype, (tuple, list,)) and
 
                len(self._mimetype) == 2):
 
                return self._mimetype
 
            else:
 
                raise NodeError('given _mimetype attribute must be an 2 '
 
                               'element list or tuple')
 

	
 
        mtype, encoding = mimetypes.guess_type(self.name)
 

	
 
        if mtype is None:
 
            if self.is_binary:
 
                mtype = 'application/octet-stream'
 
                encoding = None
 
            else:
 
                mtype = 'text/plain'
 
                encoding = None
 
        return mtype, encoding
 

	
 
    @LazyProperty
 
    def mimetype(self):
 
        """
 
        Wrapper around full mimetype info. It returns only type of fetched
 
        mimetype without the encoding part. use get_mimetype function to fetch
 
        full set of (type,encoding)
 
        """
 
        return self.get_mimetype()[0]
 

	
 
    @LazyProperty
 
    def mimetype_main(self):
 
        return self.mimetype.split('/')[0]
 

	
 
    @LazyProperty
 
    def lexer(self):
 
        """
 
        Returns pygment's lexer class. Would try to guess lexer taking file's
 
        content, name and mimetype.
 
        """
 
        try:
 
            lexer = lexers.guess_lexer_for_filename(self.name, self.content)
 
        except lexers.ClassNotFound:
 
            lexer = lexers.TextLexer()
 
        # returns first alias
 
        return lexer
 

	
 
    @LazyProperty
 
    def lexer_alias(self):
 
        """
 
        Returns first alias of the lexer guessed for this file.
 
        """
 
        return self.lexer.aliases[0]
 

	
 
    @LazyProperty
 
    def history(self):
 
        """
 
        Returns a list of changeset for this file in which the file was changed
 
        """
 
        if self.changeset is None:
 
            raise NodeError('Unable to get changeset for this FileNode')
 
        return self.changeset.get_file_history(self.path)
 

	
 
    @LazyProperty
 
    def annotate(self):
 
        """
 
        Returns a list of three element tuples with lineno,changeset and line
 
        """
 
        if self.changeset is None:
 
            raise NodeError('Unable to get changeset for this FileNode')
 
        return self.changeset.get_file_annotate(self.path)
 

	
 
    @LazyProperty
 
    def state(self):
 
        if not self.changeset:
 
            raise NodeError("Cannot check state of the node if it's not "
 
                "linked with changeset")
 
        elif self.path in (node.path for node in self.changeset.added):
 
            return NodeState.ADDED
 
        elif self.path in (node.path for node in self.changeset.changed):
 
            return NodeState.CHANGED
 
        else:
 
            return NodeState.NOT_CHANGED
 

	
 
    @property
 
    def is_binary(self):
 
        """
 
        Returns True if file has binary content.
 
        """
 
        _bin = '\0' in self.content
 
        return _bin
 

	
 
    @LazyProperty
 
    def extension(self):
 
        """Returns filenode extension"""
 
        return self.name.split('.')[-1]
 

	
 
    def is_executable(self):
 
        """
 
        Returns ``True`` if file has executable flag turned on.
 
        """
 
        return bool(self.mode & stat.S_IXUSR)
 

	
 
    def __repr__(self):
 
        return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
 
                                 self.changeset.short_id)
 

	
 

	
 
class RemovedFileNode(FileNode):
 
    """
 
    Dummy FileNode class - trying to access any public attribute except path,
 
    name, kind or state (or methods/attributes checking those two) would raise
 
    RemovedFileNodeError.
 
    """
 
    ALLOWED_ATTRIBUTES = ['name', 'path', 'state', 'is_root', 'is_file',
 
        'is_dir', 'kind', 'added', 'changed', 'not_changed', 'removed']
 

	
 
    def __init__(self, path):
 
        """
 
        :param path: relative path to the node
 
        """
 
        super(RemovedFileNode, self).__init__(path=path)
 

	
 
    def __getattribute__(self, attr):
 
        if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
 
            return super(RemovedFileNode, self).__getattribute__(attr)
 
        raise RemovedFileNodeError("Cannot access attribute %s on "
 
            "RemovedFileNode" % attr)
 

	
 
    @LazyProperty
 
    def state(self):
 
        return NodeState.REMOVED
 

	
 

	
 
class DirNode(Node):
 
    """
 
    DirNode stores list of files and directories within this node.
 
    Nodes may be used standalone but within repository context they
 
    lazily fetch data within same repositorty's changeset.
 
    """
 

	
 
    def __init__(self, path, nodes=(), changeset=None):
 
        """
 
        Only one of ``nodes`` and ``changeset`` may be given. Passing both
 
        would raise ``NodeError`` exception.
 

	
 
        :param path: relative path to the node
 
        :param nodes: content may be passed to constructor
 
        :param changeset: if given, will use it to lazily fetch content
 
        :param size: always 0 for ``DirNode``
 
        """
 
        if nodes and changeset:
 
            raise NodeError("Cannot use both nodes and changeset")
 
        super(DirNode, self).__init__(path, NodeKind.DIR)
 
        self.changeset = changeset
 
        self._nodes = nodes
 

	
 
    @LazyProperty
 
    def content(self):
 
        raise NodeError("%s represents a dir and has no ``content`` attribute"
 
            % self)
 

	
 
    @LazyProperty
 
    def nodes(self):
 
        if self.changeset:
 
            nodes = self.changeset.get_nodes(self.path)
 
        else:
 
            nodes = self._nodes
 
        self._nodes_dict = dict((node.path, node) for node in nodes)
 
        return sorted(nodes)
 

	
 
    @LazyProperty
 
    def files(self):
 
        return sorted((node for node in self.nodes if node.is_file()))
 

	
 
    @LazyProperty
 
    def dirs(self):
 
        return sorted((node for node in self.nodes if node.is_dir()))
 

	
 
    def __iter__(self):
 
        for node in self.nodes:
 
            yield node
 

	
 
    def get_node(self, path):
 
        """
 
        Returns node from within this particular ``DirNode``, so it is now
 
        allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
 
        'docs'. In order to access deeper nodes one must fetch nodes between
 
        them first - this would work::
 

	
 
           docs = root.get_node('docs')
 
           docs.get_node('api').get_node('index.rst')
 

	
 
        :param: path - relative to the current node
 

	
 
        .. note::
 
           To access lazily (as in example above) node have to be initialized
 
           with related changeset object - without it node is out of
 
           context and may know nothing about anything else than nearest
 
           (located at same level) nodes.
 
        """
 
        try:
 
            path = path.rstrip('/')
 
            if path == '':
 
                raise NodeError("Cannot retrieve node without path")
 
            self.nodes  # access nodes first in order to set _nodes_dict
 
            paths = path.split('/')
 
            if len(paths) == 1:
 
                if not self.is_root():
 
                    path = '/'.join((self.path, paths[0]))
 
                else:
 
                    path = paths[0]
 
                return self._nodes_dict[path]
 
            elif len(paths) > 1:
 
                if self.changeset is None:
 
                    raise NodeError("Cannot access deeper "
 
                                    "nodes without changeset")
 
                else:
 
                    path1, path2 = paths[0], '/'.join(paths[1:])
 
                    return self.get_node(path1).get_node(path2)
 
            else:
 
                raise KeyError
 
        except KeyError:
 
            raise NodeError("Node does not exist at %s" % path)
 

	
 
    @LazyProperty
 
    def state(self):
 
        raise NodeError("Cannot access state of DirNode")
 

	
 
    @LazyProperty
 
    def size(self):
 
        size = 0
 
        for root, dirs, files in self.changeset.walk(self.path):
 
            for f in files:
 
                size += f.size
 

	
 
        return size
 

	
 
    def __repr__(self):
 
        return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
 
                                 self.changeset.short_id)
 

	
 

	
 
class RootNode(DirNode):
 
    """
 
    DirNode being the root node of the repository.
 
    """
 

	
 
    def __init__(self, nodes=(), changeset=None):
 
        super(RootNode, self).__init__(path='', nodes=nodes,
 
            changeset=changeset)
 

	
 
    def __repr__(self):
 
        return '<%s>' % self.__class__.__name__
 

	
 

	
 
class SubModuleNode(Node):
 
    """
 
    represents a SubModule of Git or SubRepo of Mercurial
 
    """
 
    is_binary = False
 
    size = 0
 

	
 
    def __init__(self, name, url=None, changeset=None, alias=None):
 
        self.path = name
 
        self.kind = NodeKind.SUBMODULE
 
        self.alias = alias
 
        # we have to use emptyChangeset here since this can point to svn/git/hg
 
        # submodules we cannot get from repository
 
        self.changeset = EmptyChangeset(str(changeset), alias=alias)
 
        self.url = url or self._extract_submodule_url()
 

	
 
    def __repr__(self):
 
        return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
 
                                 self.changeset.short_id)
 

	
 
    def _extract_submodule_url(self):
 
        if self.alias == 'git':
 
            #TODO: find a way to parse gits submodule file and extract the
 
            # linking URL
 
            return self.path
 
        if self.alias == 'hg':
 
            return self.path
 

	
 
    @LazyProperty
 
    def name(self):
 
        """
 
        Returns name of the node so if its path
 
        then only last part is returned.
 
        """
 
        org = safe_unicode(self.path.rstrip('/').split('/')[-1])
 
        return u'%s @ %s' % (org, self.changeset.short_id)
rhodecode/public/css/style.css
Show inline comments
 
@@ -1964,1536 +1964,1544 @@ div.form div.fields div.field div.button
 
}
 
 
#quick_login .register a {
 
	font-size: 10px;
 
	color: #fff;
 
}
 
 
#quick_login .submit {
 
    margin: -20px 0 0 0px;
 
    position: absolute;
 
    right: 15px;
 
}
 
 
#quick_login .links_left{
 
	float: left;
 
}
 
#quick_login .links_right{
 
    float: right;
 
}
 
#quick_login .full_name{
 
    color: #FFFFFF;
 
    font-weight: bold;
 
    padding: 3px;
 
}
 
#quick_login .big_gravatar{
 
	padding:4px 0px 0px 6px;
 
}
 
#quick_login .inbox{
 
    padding:4px 0px 0px 6px;
 
    color: #FFFFFF;
 
    font-weight: bold;    
 
}
 
#quick_login .inbox a{
 
	color: #FFFFFF;
 
}
 
#quick_login .email,#quick_login .email a{
 
    color: #FFFFFF;
 
    padding: 3px;
 
    
 
}
 
#quick_login .links .logout{
 
 
}
 
 
#quick_login div.form div.fields {
 
	padding-top: 2px;
 
	padding-left: 10px;
 
}
 
 
#quick_login div.form div.fields div.field {
 
	padding: 5px;
 
}
 
 
#quick_login div.form div.fields div.field div.label label {
 
	color: #fff;
 
	padding-bottom: 3px;
 
}
 
 
#quick_login div.form div.fields div.field div.input input {
 
	width: 236px;
 
	background: #FFF;
 
	border-top: 1px solid #b3b3b3;
 
	border-left: 1px solid #b3b3b3;
 
	border-right: 1px solid #eaeaea;
 
	border-bottom: 1px solid #eaeaea;
 
	color: #000;
 
	font-size: 11px;
 
	margin: 0;
 
	padding: 5px 7px 4px;
 
}
 
 
#quick_login div.form div.fields div.buttons {
 
	clear: both;
 
	overflow: hidden;
 
	text-align: right;
 
	margin: 0;
 
	padding: 5px 14px 0px 5px;
 
}
 
 
#quick_login div.form div.links {
 
	clear: both;
 
	overflow: hidden;
 
	margin: 10px 0 0;
 
	padding: 0 0 2px;
 
}
 
 
#quick_login ol.links{
 
    display: block;
 
    font-weight: bold;
 
    list-style: none outside none;
 
    text-align: right;
 
}
 
#quick_login ol.links li{
 
    line-height: 27px;
 
    margin: 0;
 
    padding: 0;
 
    color: #fff;
 
    display: block;
 
    float:none !important;
 
}
 
 
#quick_login ol.links li a{
 
    color: #fff;
 
    display: block;
 
    padding: 2px;
 
}
 
#quick_login ol.links li a:HOVER{
 
    background-color: inherit !important;
 
}
 
 
#register div.title {
 
	clear: both;
 
	overflow: hidden;
 
	position: relative;
 
    background-color: #003B76;
 
    background-repeat: repeat-x;
 
    background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) );
 
    background-image: -moz-linear-gradient(top, #003b76, #00376e);
 
    background-image: -ms-linear-gradient(top, #003b76, #00376e);
 
    background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) );
 
    background-image: -webkit-linear-gradient(top, #003b76, #00376e);
 
    background-image: -o-linear-gradient(top, #003b76, #00376e);
 
    background-image: linear-gradient(top, #003b76, #00376e);
 
    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76',
 
        endColorstr='#00376e', GradientType=0 );
 
	margin: 0 auto;
 
	padding: 0;
 
}
 
 
#register div.inner {
 
	background: #FFF;
 
	border-top: none;
 
	border-bottom: none;
 
	margin: 0 auto;
 
	padding: 20px;
 
}
 
 
#register div.form div.fields div.field div.label {
 
	width: 135px;
 
	float: left;
 
	text-align: right;
 
	margin: 2px 10px 0 0;
 
	padding: 5px 0 0 5px;
 
}
 
 
#register div.form div.fields div.field div.input input {
 
	width: 300px;
 
	background: #FFF;
 
	border-top: 1px solid #b3b3b3;
 
	border-left: 1px solid #b3b3b3;
 
	border-right: 1px solid #eaeaea;
 
	border-bottom: 1px solid #eaeaea;
 
	color: #000;
 
	font-size: 11px;
 
	margin: 0;
 
	padding: 7px 7px 6px;
 
}
 
 
#register div.form div.fields div.buttons {
 
	clear: both;
 
	overflow: hidden;
 
	border-top: 1px solid #DDD;
 
	text-align: left;
 
	margin: 0;
 
	padding: 10px 0 0 150px;
 
}
 
 
#register div.form div.activation_msg {
 
	padding-top: 4px;
 
	padding-bottom: 4px;
 
}
 
 
#journal .journal_day {
 
	font-size: 20px;
 
	padding: 10px 0px;
 
	border-bottom: 2px solid #DDD;
 
	margin-left: 10px;
 
	margin-right: 10px;
 
}
 
 
#journal .journal_container {
 
	padding: 5px;
 
	clear: both;
 
	margin: 0px 5px 0px 10px;
 
}
 
 
#journal .journal_action_container {
 
	padding-left: 38px;
 
}
 
 
#journal .journal_user {
 
	color: #747474;
 
	font-size: 14px;
 
	font-weight: bold;
 
	height: 30px;
 
}
 
 
#journal .journal_icon {
 
	clear: both;
 
	float: left;
 
	padding-right: 4px;
 
	padding-top: 3px;
 
}
 
 
#journal .journal_action {
 
	padding-top: 4px;
 
	min-height: 2px;
 
	float: left
 
}
 
 
#journal .journal_action_params {
 
	clear: left;
 
	padding-left: 22px;
 
}
 
 
#journal .journal_repo {
 
	float: left;
 
	margin-left: 6px;
 
	padding-top: 3px;
 
}
 
 
#journal .date {
 
	clear: both;
 
	color: #777777;
 
	font-size: 11px;
 
	padding-left: 22px;
 
}
 
 
#journal .journal_repo .journal_repo_name {
 
	font-weight: bold;
 
	font-size: 1.1em;
 
}
 
 
#journal .compare_view {
 
	padding: 5px 0px 5px 0px;
 
	width: 95px;
 
}
 
 
.journal_highlight {
 
	font-weight: bold;
 
	padding: 0 2px;
 
	vertical-align: bottom;
 
}
 
 
.trending_language_tbl,.trending_language_tbl td {
 
	border: 0 !important;
 
	margin: 0 !important;
 
	padding: 0 !important;
 
}
 
 
.trending_language_tbl,.trending_language_tbl tr {
 
    border-spacing: 1px;
 
}
 
 
.trending_language {
 
	background-color: #003367;
 
	color: #FFF;
 
	display: block;
 
	min-width: 20px;
 
	text-decoration: none;
 
	height: 12px;
 
	margin-bottom: 0px;
 
	margin-left: 5px;
 
	white-space: pre;
 
	padding: 3px;
 
}
 
 
h3.files_location {
 
	font-size: 1.8em;
 
	font-weight: 700;
 
	border-bottom: none !important;
 
	margin: 10px 0 !important;
 
}
 
 
#files_data dl dt {
 
	float: left;
 
	width: 60px;
 
	margin: 0 !important;
 
	padding: 5px;
 
}
 
 
#files_data dl dd {
 
	margin: 0 !important;
 
	padding: 5px !important;
 
}
 
 
.tablerow0 {
 
	background-color: #F8F8F8;
 
}
 
 
.tablerow1 {
 
    background-color: #FFFFFF;
 
}
 
 
.changeset_id {
 
	font-family: monospace;
 
	color: #666666;
 
}
 
 
.changeset_hash {
 
	color: #000000;
 
}
 
 
#changeset_content {
 
	border-left: 1px solid #CCC;
 
	border-right: 1px solid #CCC;
 
	border-bottom: 1px solid #CCC;
 
	padding: 5px;
 
}
 
 
#changeset_compare_view_content {
 
	border: 1px solid #CCC;
 
	padding: 5px;
 
}
 
 
#changeset_content .container {
 
	min-height: 100px;
 
	font-size: 1.2em;
 
	overflow: hidden;
 
}
 
 
#changeset_compare_view_content .compare_view_commits {
 
	width: auto !important;
 
}
 
 
#changeset_compare_view_content .compare_view_commits td {
 
	padding: 0px 0px 0px 12px !important;
 
}
 
 
#changeset_content .container .right {
 
	float: right;
 
	width: 20%;
 
	text-align: right;
 
}
 
 
#changeset_content .container .left .message {
 
	white-space: pre-wrap;
 
}
 
#changeset_content .container .left .message a:hover {
 
	text-decoration: none;
 
}
 
.cs_files .cur_cs {
 
	margin: 10px 2px;
 
	font-weight: bold;
 
}
 
 
.cs_files .node {
 
	float: left;
 
}
 
 
.cs_files .changes {
 
	float: right;
 
	color:#003367;
 
	
 
}
 
 
.cs_files .changes .added {
 
	background-color: #BBFFBB;
 
	float: left;
 
	text-align: center;
 
	font-size: 9px;
 
    padding: 2px 0px 2px 0px;
 
}
 
 
.cs_files .changes .deleted {
 
	background-color: #FF8888;
 
	float: left;
 
	text-align: center;
 
	font-size: 9px;
 
    padding: 2px 0px 2px 0px;
 
}
 
 
.cs_files .cs_added {
 
	background: url("../images/icons/page_white_add.png") no-repeat scroll
 
		3px;
 
	height: 16px;
 
	padding-left: 20px;
 
	margin-top: 7px;
 
	text-align: left;
 
}
 
 
.cs_files .cs_changed {
 
	background: url("../images/icons/page_white_edit.png") no-repeat scroll
 
		3px;
 
	height: 16px;
 
	padding-left: 20px;
 
	margin-top: 7px;
 
	text-align: left;
 
}
 
 
.cs_files .cs_removed {
 
	background: url("../images/icons/page_white_delete.png") no-repeat
 
		scroll 3px;
 
	height: 16px;
 
	padding-left: 20px;
 
	margin-top: 7px;
 
	text-align: left;
 
}
 
 
#graph {
 
	overflow: hidden;
 
}
 
 
#graph_nodes {
 
	float: left;
 
	margin-right: -6px;
 
	margin-top: 0px;
 
}
 
 
#graph_content {
 
	width: 80%;
 
	float: left;
 
}
 
 
#graph_content .container_header {
 
	border-bottom: 1px solid #DDD;
 
	padding: 10px;
 
	height: 25px;
 
}
 
 
#graph_content #rev_range_container {
 
	padding: 7px 20px;
 
	float: left;
 
}
 
 
#graph_content .container {
 
	border-bottom: 1px solid #DDD;
 
	height: 56px;
 
	overflow: hidden;
 
}
 
 
#graph_content .container .right {
 
	float: right;
 
	width: 23%;
 
	text-align: right;
 
}
 
 
#graph_content .container .left {
 
	float: left;
 
	width: 25%;
 
	padding-left: 5px;
 
}
 
 
#graph_content .container .mid {
 
	float: left;
 
	width: 49%;
 
}
 
 
 
#graph_content .container .left .date {
 
	color: #666;
 
	padding-left: 22px;
 
	font-size: 10px;
 
}
 
 
#graph_content .container .left .author {
 
	height: 22px;
 
}
 
 
#graph_content .container .left .author .user {
 
	color: #444444;
 
	float: left;
 
	margin-left: -4px;
 
	margin-top: 4px;
 
}
 
 
#graph_content .container .mid .message {
 
	white-space: pre-wrap;
 
}
 
 
#graph_content .container .mid .message a:hover{
 
	text-decoration: none;
 
}
 
#content #graph_content .message .revision-link,
 
#changeset_content .container .message .revision-link
 
 {
 
	color:#3F6F9F;
 
    font-weight: bold !important;
 
}
 
 
#content #graph_content .message .issue-tracker-link,
 
#changeset_content .container .message .issue-tracker-link{
 
    color:#3F6F9F;
 
    font-weight: bold !important;
 
}
 
 
.right .changeset-status-container{
 
    padding-right: 5px;
 
    margin-top:1px;
 
    float:right;
 
    height:14px;
 
}
 
.right .changeset-status-container .changeset-status-lbl{
 
	color: rgb(136, 136, 136);
 
    float: left;
 
    padding: 0px 4px 0px 0px;	
 
}
 
.right .comments-container{
 
	padding-right: 5px;
 
	margin-top:1px;
 
	float:right;
 
	height:14px;
 
}
 
 
.right .comments-cnt{
 
    float: left;
 
    color: rgb(136, 136, 136); 
 
    padding-right: 2px; 
 
}
 
 
.right .changes{
 
	clear: both;
 
}
 
 
.right .changes .changed_total {
 
	display: block;
 
	float: right;
 
	text-align: center;
 
	min-width: 45px;
 
	cursor: pointer;
 
	color: #444444;
 
	background: #FEA;
 
	-webkit-border-radius: 0px 0px 0px 6px;
 
	-moz-border-radius: 0px 0px 0px 6px;
 
	border-radius: 0px 0px 0px 6px;
 
	padding: 1px;
 
}
 
 
.right .changes .added,.changed,.removed {
 
	display: block;
 
	padding: 1px;
 
	color: #444444;
 
	float: right;
 
	text-align: center;
 
	min-width: 15px;
 
}
 
 
.right .changes .added {
 
	background: #CFC;
 
}
 
 
.right .changes .changed {
 
	background: #FEA;
 
}
 
 
.right .changes .removed {
 
	background: #FAA;
 
}
 
 
.right .merge {
 
  padding: 1px 3px 1px 3px;
 
  background-color: #fca062;
 
  font-size: 10px;
 
  font-weight: bold;
 
  color: #ffffff;
 
  text-transform: uppercase;
 
  white-space: nowrap;
 
  -webkit-border-radius: 3px;
 
  -moz-border-radius: 3px;
 
  border-radius: 3px;
 
  margin-right: 2px;
 
}
 
 
.right .parent {
 
	color: #666666;
 
	clear:both;
 
}
 
.right .logtags{
 
	padding: 2px 2px 2px 2px;
 
}
 
.right .logtags .branchtag,.logtags .branchtag {
 
  padding: 1px 3px 1px 3px;
 
  background-color: #bfbfbf;
 
  font-size: 10px;
 
  font-weight: bold;
 
  color: #ffffff;
 
  text-transform: uppercase;
 
  white-space: nowrap;
 
  -webkit-border-radius: 3px;
 
  -moz-border-radius: 3px;
 
  border-radius: 3px;
 
}
 
.right .logtags .branchtag a:hover,.logtags .branchtag a{
 
	color: #ffffff;
 
}
 
.right .logtags .branchtag a:hover,.logtags .branchtag a:hover{
 
	text-decoration: none;
 
	color: #ffffff;
 
}
 
.right .logtags .tagtag,.logtags .tagtag {
 
  padding: 1px 3px 1px 3px;
 
  background-color: #62cffc;
 
  font-size: 10px;
 
  font-weight: bold;
 
  color: #ffffff;
 
  text-transform: uppercase;
 
  white-space: nowrap;
 
  -webkit-border-radius: 3px;
 
  -moz-border-radius: 3px;
 
  border-radius: 3px;
 
}
 
.right .logtags .tagtag a:hover,.logtags .tagtag a{
 
	color: #ffffff;
 
}
 
.right .logtags .tagtag a:hover,.logtags .tagtag a:hover{
 
    text-decoration: none;
 
    color: #ffffff;
 
}
 
.right .logbooks .bookbook,.logbooks .bookbook {
 
  padding: 1px 3px 2px;
 
  background-color: #46A546;
 
  font-size: 9.75px;
 
  font-weight: bold;
 
  color: #ffffff;
 
  text-transform: uppercase;
 
  white-space: nowrap;
 
  -webkit-border-radius: 3px;
 
  -moz-border-radius: 3px;
 
  border-radius: 3px;
 
}
 
.right .logbooks .bookbook,.logbooks .bookbook a{
 
	color: #ffffff;
 
}
 
.right .logbooks .bookbook,.logbooks .bookbook a:hover{
 
    text-decoration: none;
 
    color: #ffffff;
 
}
 
div.browserblock {
 
	overflow: hidden;
 
	border: 1px solid #ccc;
 
	background: #f8f8f8;
 
	font-size: 100%;
 
	line-height: 125%;
 
	padding: 0;
 
    -webkit-border-radius: 6px 6px 0px 0px;
 
    -moz-border-radius: 6px 6px 0px 0px;
 
    border-radius: 6px 6px 0px 0px;	
 
}
 
 
div.browserblock .browser-header {
 
	background: #FFF;
 
	padding: 10px 0px 15px 0px;
 
	width: 100%;
 
}
 
 
div.browserblock .browser-nav {
 
	float: left
 
}
 
 
div.browserblock .browser-branch {
 
	float: left;
 
}
 
 
div.browserblock .browser-branch label {
 
	color: #4A4A4A;
 
	vertical-align: text-top;
 
}
 
 
div.browserblock .browser-header span {
 
	margin-left: 5px;
 
	font-weight: 700;
 
}
 
 
div.browserblock .browser-search {
 
	clear: both;
 
	padding: 8px 8px 0px 5px;
 
	height: 20px;
 
}
 
 
div.browserblock #node_filter_box {
 
	
 
}
 
 
div.browserblock .search_activate {
 
	float: left
 
}
 
 
div.browserblock .add_node {
 
	float: left;
 
	padding-left: 5px;
 
}
 
 
div.browserblock .search_activate a:hover,div.browserblock .add_node a:hover
 
	{
 
	text-decoration: none !important;
 
}
 
 
div.browserblock .browser-body {
 
	background: #EEE;
 
	border-top: 1px solid #CCC;
 
}
 
 
table.code-browser {
 
	border-collapse: collapse;
 
	width: 100%;
 
}
 
 
table.code-browser tr {
 
	margin: 3px;
 
}
 
 
table.code-browser thead th {
 
	background-color: #EEE;
 
	height: 20px;
 
	font-size: 1.1em;
 
	font-weight: 700;
 
	text-align: left;
 
	padding-left: 10px;
 
}
 
 
table.code-browser tbody td {
 
	padding-left: 10px;
 
	height: 20px;
 
}
 
 
table.code-browser .browser-file {
 
	background: url("../images/icons/document_16.png") no-repeat scroll 3px;
 
	height: 16px;
 
	padding-left: 20px;
 
	text-align: left;
 
}
 
.diffblock .changeset_header {
 
    height: 16px;
 
}
 
.diffblock .changeset_file {
 
	background: url("../images/icons/file.png") no-repeat scroll 3px;
 
	text-align: left;
 
	float: left;
 
	padding: 2px 0px 2px 22px;
 
}
 
.diffblock .diff-menu-wrapper{
 
	float: left;
 
}
 
 
.diffblock .diff-menu{
 
    position: absolute;
 
    background: none repeat scroll 0 0 #FFFFFF;
 
    border-color: #003367 #666666 #666666;
 
    border-right: 1px solid #666666;
 
    border-style: solid solid solid;
 
    border-width: 1px;
 
    box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
 
    margin-top:5px;
 
    margin-left:1px;
 
    
 
}
 
.diffblock .diff-actions {
 
    padding: 2px 0px 0px 2px;
 
    float: left;
 
}
 
.diffblock  .diff-menu ul li {
 
	padding: 0px 0px 0px 0px !important;
 
}
 
.diffblock  .diff-menu ul li a{
 
	display: block;
 
	padding: 3px 8px 3px 8px !important;
 
}
 
.diffblock  .diff-menu ul li a:hover{
 
    text-decoration: none;
 
    background-color: #EEEEEE;
 
}
 
table.code-browser .browser-dir {
 
	background: url("../images/icons/folder_16.png") no-repeat scroll 3px;
 
	height: 16px;
 
	padding-left: 20px;
 
	text-align: left;
 
}
 
 
table.code-browser .submodule-dir {
 
    background: url("../images/icons/disconnect.png") no-repeat scroll 3px;
 
    height: 16px;
 
    padding-left: 20px;
 
    text-align: left;
 
}
 
 
 
.box .search {
 
	clear: both;
 
	overflow: hidden;
 
	margin: 0;
 
	padding: 0 20px 10px;
 
}
 
 
.box .search div.search_path {
 
	background: none repeat scroll 0 0 #EEE;
 
	border: 1px solid #CCC;
 
	color: blue;
 
	margin-bottom: 10px;
 
	padding: 10px 0;
 
}
 
 
.box .search div.search_path div.link {
 
	font-weight: 700;
 
	margin-left: 25px;
 
}
 
 
.box .search div.search_path div.link a {
 
	color: #003367;
 
	cursor: pointer;
 
	text-decoration: none;
 
}
 
 
#path_unlock {
 
	color: red;
 
	font-size: 1.2em;
 
	padding-left: 4px;
 
}
 
 
.info_box span {
 
	margin-left: 3px;
 
	margin-right: 3px;
 
}
 
 
.info_box .rev {
 
	color: #003367;
 
	font-size: 1.6em;
 
	font-weight: bold;
 
	vertical-align: sub;
 
}
 
 
.info_box input#at_rev,.info_box input#size {
 
	background: #FFF;
 
	border-top: 1px solid #b3b3b3;
 
	border-left: 1px solid #b3b3b3;
 
	border-right: 1px solid #eaeaea;
 
	border-bottom: 1px solid #eaeaea;
 
	color: #000;
 
	font-size: 12px;
 
	margin: 0;
 
	padding: 1px 5px 1px;
 
}
 
 
.info_box input#view {
 
	text-align: center;
 
	padding: 4px 3px 2px 2px;
 
}
 
 
.yui-overlay,.yui-panel-container {
 
	visibility: hidden;
 
	position: absolute;
 
	z-index: 2;
 
}
 
 
.yui-tt {
 
	visibility: hidden;
 
	position: absolute;
 
	color: #666;
 
	background-color: #FFF;
 
	border: 2px solid #003367;
 
	font: 100% sans-serif;
 
	width: auto;
 
	opacity: 1px;
 
	padding: 8px;
 
	white-space: pre-wrap;
 
	-webkit-border-radius: 8px 8px 8px 8px;
 
	-khtml-border-radius: 8px 8px 8px 8px;
 
	-moz-border-radius: 8px 8px 8px 8px;
 
	border-radius: 8px 8px 8px 8px;
 
	box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
 
}
 
 
.ac {
 
	vertical-align: top;
 
}
 
 
.ac .yui-ac {
 
	position: inherit;
 
	font-size: 100%;
 
}
 
 
.ac .perm_ac {
 
	width: 20em;
 
}
 
 
.ac .yui-ac-input {
 
	width: 100%;
 
}
 
 
.ac .yui-ac-container {
 
	position: absolute;
 
	top: 1.6em;
 
	width: auto;
 
}
 
 
.ac .yui-ac-content {
 
	position: absolute;
 
	border: 1px solid gray;
 
	background: #fff;
 
	z-index: 9050;
 
	
 
}
 
 
.ac .yui-ac-shadow {
 
	position: absolute;
 
	width: 100%;
 
	background: #000;
 
	-moz-opacity: 0.1px;
 
	opacity: .10;
 
	filter: alpha(opacity = 10);
 
	z-index: 9049;
 
	margin: .3em;
 
}
 
 
.ac .yui-ac-content ul {
 
	width: 100%;
 
	margin: 0;
 
	padding: 0;
 
	z-index: 9050;
 
}
 
 
.ac .yui-ac-content li {
 
	cursor: default;
 
	white-space: nowrap;
 
	margin: 0;
 
	padding: 2px 5px;
 
	height: 18px;
 
	z-index: 9050;
 
	display: block;
 
	width: auto !important;
 
}
 
 
.ac .yui-ac-content li .ac-container-wrap{
 
    width: auto;
 
}
 
 
.ac .yui-ac-content li.yui-ac-prehighlight {
 
	background: #B3D4FF;
 
	z-index: 9050;
 
}
 
 
.ac .yui-ac-content li.yui-ac-highlight {
 
	background: #556CB5;
 
	color: #FFF;
 
	z-index: 9050;
 
}
 
.ac .yui-ac-bd{
 
	z-index: 9050;
 
}
 
 
.follow {
 
	background: url("../images/icons/heart_add.png") no-repeat scroll 3px;
 
	height: 16px;
 
	width: 20px;
 
	cursor: pointer;
 
	display: block;
 
	float: right;
 
	margin-top: 2px;
 
}
 
 
.following {
 
	background: url("../images/icons/heart_delete.png") no-repeat scroll 3px;
 
	height: 16px;
 
	width: 20px;
 
	cursor: pointer;
 
	display: block;
 
	float: right;
 
	margin-top: 2px;
 
}
 
 
.currently_following {
 
	padding-left: 10px;
 
	padding-bottom: 5px;
 
}
 
 
.add_icon {
 
	background: url("../images/icons/add.png") no-repeat scroll 3px;
 
	padding-left: 20px;
 
	padding-top: 0px;
 
	text-align: left;
 
}
 
 
.edit_icon {
 
	background: url("../images/icons/folder_edit.png") no-repeat scroll 3px;
 
	padding-left: 20px;
 
	padding-top: 0px;
 
	text-align: left;
 
}
 
 
.delete_icon {
 
	background: url("../images/icons/delete.png") no-repeat scroll 3px;
 
	padding-left: 20px;
 
	padding-top: 0px;
 
	text-align: left;
 
}
 
 
.refresh_icon {
 
	background: url("../images/icons/arrow_refresh.png") no-repeat scroll
 
		3px;
 
	padding-left: 20px;
 
	padding-top: 0px;
 
	text-align: left;
 
}
 
 
.pull_icon {
 
	background: url("../images/icons/connect.png") no-repeat scroll 3px;
 
	padding-left: 20px;
 
	padding-top: 0px;
 
	text-align: left;
 
}
 
 
.rss_icon {
 
	background: url("../images/icons/rss_16.png") no-repeat scroll 3px;
 
	padding-left: 20px;
 
	padding-top: 4px;
 
	text-align: left;
 
	font-size: 8px
 
}
 
 
.atom_icon {
 
	background: url("../images/icons/atom.png") no-repeat scroll 3px;
 
	padding-left: 20px;
 
	padding-top: 4px;
 
	text-align: left;
 
	font-size: 8px
 
}
 
 
.archive_icon {
 
	background: url("../images/icons/compress.png") no-repeat scroll 3px;
 
	padding-left: 20px;
 
	text-align: left;
 
	padding-top: 1px;
 
}
 
 
.start_following_icon {
 
	background: url("../images/icons/heart_add.png") no-repeat scroll 3px;
 
	padding-left: 20px;
 
	text-align: left;
 
	padding-top: 0px;
 
}
 
 
.stop_following_icon {
 
	background: url("../images/icons/heart_delete.png") no-repeat scroll 3px;
 
	padding-left: 20px;
 
	text-align: left;
 
	padding-top: 0px;
 
}
 
 
.action_button {
 
	border: 0;
 
	display: inline;
 
}
 
 
.action_button:hover {
 
	border: 0;
 
	text-decoration: underline;
 
	cursor: pointer;
 
}
 
 
#switch_repos {
 
	position: absolute;
 
	height: 25px;
 
	z-index: 1;
 
}
 
 
#switch_repos select {
 
	min-width: 150px;
 
	max-height: 250px;
 
	z-index: 1;
 
}
 
 
.breadcrumbs {
 
	border: medium none;
 
	color: #FFF;
 
	float: left;
 
	text-transform: uppercase;
 
	font-weight: 700;
 
	font-size: 14px;
 
	margin: 0;
 
	padding: 11px 0 11px 10px;
 
}
 
 
.breadcrumbs .hash {
 
	text-transform: none;
 
	color: #fff;
 
}
 
 
.breadcrumbs a {
 
	color: #FFF;
 
}
 
 
.flash_msg {
 
	
 
}
 
 
.flash_msg ul {
 
	
 
}
 
 
.error_msg {
 
	background-color: #c43c35;
 
	background-repeat: repeat-x;
 
	background-image: -khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35) );
 
	background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35);
 
	background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35);
 
	background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35) );
 
	background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35);
 
	background-image: -o-linear-gradient(top, #ee5f5b, #c43c35);
 
	background-image: linear-gradient(top, #ee5f5b, #c43c35);
 
	filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b',endColorstr='#c43c35', GradientType=0 );
 
	border-color: #c43c35 #c43c35 #882a25;
 
}
 
 
.warning_msg {
 
	color: #404040 !important;
 
	background-color: #eedc94;
 
	background-repeat: repeat-x;
 
	background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), to(#eedc94) );
 
	background-image: -moz-linear-gradient(top, #fceec1, #eedc94);
 
	background-image: -ms-linear-gradient(top, #fceec1, #eedc94);
 
	background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fceec1), color-stop(100%, #eedc94) );
 
	background-image: -webkit-linear-gradient(top, #fceec1, #eedc94);
 
	background-image: -o-linear-gradient(top, #fceec1, #eedc94);
 
	background-image: linear-gradient(top, #fceec1, #eedc94);
 
	filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fceec1', endColorstr='#eedc94', GradientType=0 );
 
	border-color: #eedc94 #eedc94 #e4c652;
 
}
 
 
.success_msg {
 
	background-color: #57a957;
 
	background-repeat: repeat-x !important;
 
	background-image: -khtml-gradient(linear, left top, left bottom, from(#62c462), to(#57a957) );
 
	background-image: -moz-linear-gradient(top, #62c462, #57a957);
 
	background-image: -ms-linear-gradient(top, #62c462, #57a957);
 
	background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #62c462), color-stop(100%, #57a957) );
 
	background-image: -webkit-linear-gradient(top, #62c462, #57a957);
 
	background-image: -o-linear-gradient(top, #62c462, #57a957);
 
	background-image: linear-gradient(top, #62c462, #57a957);
 
	filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0 );
 
	border-color: #57a957 #57a957 #3d773d;
 
}
 
 
.notice_msg {
 
	background-color: #339bb9;
 
	background-repeat: repeat-x;
 
	background-image: -khtml-gradient(linear, left top, left bottom, from(#5bc0de), to(#339bb9) );
 
	background-image: -moz-linear-gradient(top, #5bc0de, #339bb9);
 
	background-image: -ms-linear-gradient(top, #5bc0de, #339bb9);
 
	background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bc0de), color-stop(100%, #339bb9) );
 
	background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9);
 
	background-image: -o-linear-gradient(top, #5bc0de, #339bb9);
 
	background-image: linear-gradient(top, #5bc0de, #339bb9);
 
	filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0 );
 
	border-color: #339bb9 #339bb9 #22697d;
 
}
 
 
.success_msg,.error_msg,.notice_msg,.warning_msg {
 
	font-size: 12px;
 
	font-weight: 700;
 
	min-height: 14px;
 
	line-height: 14px;
 
	margin-bottom: 10px;
 
	margin-top: 0;
 
	display: block;
 
	overflow: auto;
 
	padding: 6px 10px 6px 10px;
 
	border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
 
	position: relative;
 
	color: #FFF;
 
	border-width: 1px;
 
	border-style: solid;
 
	-webkit-border-radius: 4px;
 
	-moz-border-radius: 4px;
 
	border-radius: 4px;
 
	-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
 
	-moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
 
	box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
 
}
 
 
#msg_close {
 
	background: transparent url("../icons/cross_grey_small.png") no-repeat scroll 0 0;
 
	cursor: pointer;
 
	height: 16px;
 
	position: absolute;
 
	right: 5px;
 
	top: 5px;
 
	width: 16px;
 
}
 
div#legend_data{
 
	padding-left:10px;
 
}
 
div#legend_container table{
 
	border: none !important;
 
}
 
div#legend_container table,div#legend_choices table {
 
	width: auto !important;
 
}
 
 
table#permissions_manage {
 
	width: 0 !important;
 
}
 
 
table#permissions_manage span.private_repo_msg {
 
	font-size: 0.8em;
 
	opacity: 0.6px;
 
}
 
 
table#permissions_manage td.private_repo_msg {
 
	font-size: 0.8em;
 
}
 
 
table#permissions_manage tr#add_perm_input td {
 
	vertical-align: middle;
 
}
 
 
div.gravatar {
 
	background-color: #FFF;
 
	float: left;
 
	margin-right: 0.7em;
 
	padding: 1px 1px 1px 1px;
 
    line-height:0;
 
	-webkit-border-radius: 3px;
 
	-khtml-border-radius: 3px;
 
	-moz-border-radius: 3px;
 
	border-radius: 3px;
 
}
 
 
div.gravatar img {
 
	-webkit-border-radius: 2px;
 
	-khtml-border-radius: 2px;
 
	-moz-border-radius: 2px;
 
	border-radius: 2px;
 
}
 
 
#header,#content,#footer {
 
	min-width: 978px;
 
}
 
 
#content {
 
	clear: both;
 
	overflow: hidden;
 
	padding: 54px 10px 14px 10px;
 
}
 
 
#content div.box div.title div.search {
 
	
 
	border-left: 1px solid #316293;
 
}
 
 
#content div.box div.title div.search div.input input {
 
	border: 1px solid #316293;
 
}
 
 
.ui-btn{
 
    color: #515151;
 
    background-color: #DADADA;
 
    background-repeat: repeat-x;
 
    background-image: -khtml-gradient(linear, left top, left bottom, from(#F4F4F4),to(#DADADA) );
 
    background-image: -moz-linear-gradient(top, #F4F4F4, #DADADA);
 
    background-image: -ms-linear-gradient(top, #F4F4F4, #DADADA);
 
    background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #F4F4F4),color-stop(100%, #DADADA) );
 
    background-image: -webkit-linear-gradient(top, #F4F4F4, #DADADA) );
 
    background-image: -o-linear-gradient(top, #F4F4F4, #DADADA) );
 
    background-image: linear-gradient(top, #F4F4F4, #DADADA);
 
    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#F4F4F4', endColorstr='#DADADA', GradientType=0);
 
    
 
    border-top: 1px solid #DDD;
 
    border-left: 1px solid #c6c6c6;
 
    border-right: 1px solid #DDD;
 
    border-bottom: 1px solid #c6c6c6;
 
    color: #515151;
 
    outline: none;
 
    margin: 0px 3px 3px 0px;
 
    -webkit-border-radius: 4px 4px 4px 4px !important;
 
    -khtml-border-radius: 4px 4px 4px 4px !important;
 
    -moz-border-radius: 4px 4px 4px 4px !important;
 
    border-radius: 4px 4px 4px 4px !important;
 
    cursor: pointer !important;
 
	padding: 3px 3px 3px 3px;	
 
	background-position: 0 -15px;
 
 
}
 
.ui-btn.xsmall{
 
    padding: 1px 2px 1px 1px;
 
}
 
.ui-btn.clone{
 
	padding: 5px 2px 6px 1px;
 
	margin: 0px -4px 3px 0px;
 
    -webkit-border-radius: 4px 0px 0px 4px !important;
 
    -khtml-border-radius: 4px 0px 0px 4px !important;
 
    -moz-border-radius: 4px 0px 0px 4px !important;
 
    border-radius: 4px 0px 0px 4px !important;
 
    width: 100px;
 
    text-align: center;
 
    float: left;
 
    position: absolute;
 
}
 
.ui-btn:focus {
 
  outline: none;
 
}
 
.ui-btn:hover{
 
    background-position: 0 0px;
 
    text-decoration: none;
 
    color: #515151;
 
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25), 0 0 3px #FFFFFF !important;
 
}
 
 
.ui-btn.red{
 
  color:#fff;
 
  background-color: #c43c35;
 
  background-repeat: repeat-x;
 
  background-image: -khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35));
 
  background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35);
 
  background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35);
 
  background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35));
 
  background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35);
 
  background-image: -o-linear-gradient(top, #ee5f5b, #c43c35);
 
  background-image: linear-gradient(top, #ee5f5b, #c43c35);
 
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);
 
  border-color: #c43c35 #c43c35 #882a25;
 
  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
 
}
 
 
 
.ui-btn.blue{
 
  background-color: #339bb9;
 
  background-repeat: repeat-x;
 
  background-image: -khtml-gradient(linear, left top, left bottom, from(#5bc0de), to(#339bb9));
 
  background-image: -moz-linear-gradient(top, #5bc0de, #339bb9);
 
  background-image: -ms-linear-gradient(top, #5bc0de, #339bb9);
 
  background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bc0de), color-stop(100%, #339bb9));
 
  background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9);
 
  background-image: -o-linear-gradient(top, #5bc0de, #339bb9);
 
  background-image: linear-gradient(top, #5bc0de, #339bb9);
 
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0);
 
  border-color: #339bb9 #339bb9 #22697d;
 
  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);    
 
}
 
 
.ui-btn.green{
 
  background-color: #57a957;
 
  background-repeat: repeat-x;
 
  background-image: -khtml-gradient(linear, left top, left bottom, from(#62c462), to(#57a957));
 
  background-image: -moz-linear-gradient(top, #62c462, #57a957);
 
  background-image: -ms-linear-gradient(top, #62c462, #57a957);
 
  background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #62c462), color-stop(100%, #57a957));
 
  background-image: -webkit-linear-gradient(top, #62c462, #57a957);
 
  background-image: -o-linear-gradient(top, #62c462, #57a957);
 
  background-image: linear-gradient(top, #62c462, #57a957);
 
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0);
 
  border-color: #57a957 #57a957 #3d773d;
 
  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);	
 
}
 
 
ins,div.options a:hover {
 
	text-decoration: none;
 
}
 
 
img,
 
#header #header-inner #quick li a:hover span.normal,
 
#header #header-inner #quick li ul li.last,
 
#content div.box div.form div.fields div.field div.textarea table td table td a,
 
#clone_url,
 
#clone_url_id
 
{
 
	border: none;
 
}
 
 
img.icon,.right .merge img {
 
	vertical-align: bottom;
 
}
 
 
#header ul#logged-user,#content div.box div.title ul.links,
 
#content div.box div.message div.dismiss,
 
#content div.box div.traffic div.legend ul
 
	{
 
	float: right;
 
	margin: 0;
 
	padding: 0;
 
}
 
 
#header #header-inner #home,#header #header-inner #logo,
 
#content div.box ul.left,#content div.box ol.left,
 
#content div.box div.pagination-left,div#commit_history,
 
div#legend_data,div#legend_container,div#legend_choices
 
	{
 
	float: left;
 
}
 
 
#header #header-inner #quick li:hover ul ul,
 
#header #header-inner #quick li:hover ul ul ul,
 
#header #header-inner #quick li:hover ul ul ul ul,
 
#content #left #menu ul.closed,#content #left #menu li ul.collapsed,.yui-tt-shadow
 
	{
 
	display: none;
 
}
 
 
#header #header-inner #quick li:hover ul,#header #header-inner #quick li li:hover ul,#header #header-inner #quick li li li:hover ul,#header #header-inner #quick li li li li:hover ul,#content #left #menu ul.opened,#content #left #menu li ul.expanded
 
	{
 
	display: block;
 
}
 
 
#content div.graph {
 
	padding: 0 10px 10px;
 
}
 
 
#content div.box div.title ul.links li a:hover,#content div.box div.title ul.links li.ui-tabs-selected a
 
	{
 
	color: #bfe3ff;
 
}
 
 
#content div.box ol.lower-roman,#content div.box ol.upper-roman,#content div.box ol.lower-alpha,#content div.box ol.upper-alpha,#content div.box ol.decimal
 
	{
 
	margin: 10px 24px 10px 44px;
 
}
 
 
#content div.box div.form,#content div.box div.table,#content div.box div.traffic
 
	{
 
	clear: both;
 
	overflow: hidden;
 
	margin: 0;
 
	padding: 0 20px 10px;
 
}
 
 
#content div.box div.form div.fields,#login div.form,#login div.form div.fields,#register div.form,#register div.form div.fields
 
	{
 
	clear: both;
 
	overflow: hidden;
 
	margin: 0;
 
	padding: 0;
 
}
 
 
#content div.box div.form div.fields div.field div.label span,#login div.form div.fields div.field div.label span,#register div.form div.fields div.field div.label span
 
	{
 
	height: 1%;
 
	display: block;
 
	color: #363636;
 
	margin: 0;
 
	padding: 2px 0 0;
 
}
 
 
#content div.box div.form div.fields div.field div.input input.error,#login div.form div.fields div.field div.input input.error,#register div.form div.fields div.field div.input input.error
 
	{
 
	background: #FBE3E4;
 
	border-top: 1px solid #e1b2b3;
 
	border-left: 1px solid #e1b2b3;
 
	border-right: 1px solid #FBC2C4;
 
	border-bottom: 1px solid #FBC2C4;
 
}
 
 
#content div.box div.form div.fields div.field div.input input.success,#login div.form div.fields div.field div.input input.success,#register div.form div.fields div.field div.input input.success
 
	{
 
	background: #E6EFC2;
 
	border-top: 1px solid #cebb98;
 
	border-left: 1px solid #cebb98;
 
	border-right: 1px solid #c6d880;
 
	border-bottom: 1px solid #c6d880;
 
}
 
 
#content div.box-left div.form div.fields div.field div.textarea,#content div.box-right div.form div.fields div.field div.textarea,#content div.box div.form div.fields div.field div.select select,#content div.box table th.selected input,#content div.box table td.selected input
 
	{
 
	margin: 0;
 
}
 
 
#content div.box-left div.form div.fields div.field div.select,#content div.box-left div.form div.fields div.field div.checkboxes,#content div.box-left div.form div.fields div.field div.radios,#content div.box-right div.form div.fields div.field div.select,#content div.box-right div.form div.fields div.field div.checkboxes,#content div.box-right div.form div.fields div.field div.radios
 
	{
 
	margin: 0 0 0 0px !important;
 
	padding: 0;
 
}
 
 
#content div.box div.form div.fields div.field div.select,#content div.box div.form div.fields div.field div.checkboxes,#content div.box div.form div.fields div.field div.radios
 
	{
 
	margin: 0 0 0 200px;
 
	padding: 0;
 
}
 
 
#content div.box div.form div.fields div.field div.select a:hover,#content div.box div.form div.fields div.field div.select a.ui-selectmenu:hover,#content div.box div.action a:hover
 
	{
 
	color: #000;
 
	text-decoration: none;
 
}
 
 
#content div.box div.form div.fields div.field div.select a.ui-selectmenu-focus,#content div.box div.action a.ui-selectmenu-focus
 
	{
 
	border: 1px solid #666;
 
}
 
 
#content div.box div.form div.fields div.field div.checkboxes div.checkbox,#content div.box div.form div.fields div.field div.radios div.radio
 
	{
 
	clear: both;
 
	overflow: hidden;
 
	margin: 0;
 
	padding: 8px 0 2px;
 
}
 
 
#content div.box div.form div.fields div.field div.checkboxes div.checkbox input,#content div.box div.form div.fields div.field div.radios div.radio input
 
	{
 
	float: left;
 
	margin: 0;
 
}
 
 
#content div.box div.form div.fields div.field div.checkboxes div.checkbox label,#content div.box div.form div.fields div.field div.radios div.radio label
 
	{
 
	height: 1%;
 
	display: block;
 
	float: left;
 
	margin: 2px 0 0 4px;
 
}
 
 
div.form div.fields div.field div.button input,#content div.box div.form div.fields div.buttons input,div.form div.fields div.buttons input,#content div.box div.action div.button input
 
	{
 
	color: #000;
 
	font-size: 11px;
 
	font-weight: 700;
 
	margin: 0;
 
}
 
 
input.ui-button {
 
	background: #e5e3e3 url("../images/button.png") repeat-x;
 
	border-top: 1px solid #DDD;
 
	border-left: 1px solid #c6c6c6;
 
	border-right: 1px solid #DDD;
 
	border-bottom: 1px solid #c6c6c6;
 
	color: #515151 !important;
 
	outline: none;
 
	margin: 0;
 
	padding: 6px 12px;
 
	-webkit-border-radius: 4px 4px 4px 4px;
 
	-khtml-border-radius: 4px 4px 4px 4px;
 
	-moz-border-radius: 4px 4px 4px 4px;
 
	border-radius: 4px 4px 4px 4px;
 
	box-shadow: 0 1px 0 #ececec;
 
	cursor: pointer;
 
}
 
 
input.ui-button:hover {
 
	background: #b4b4b4 url("../images/button_selected.png") repeat-x;
 
	border-top: 1px solid #ccc;
 
	border-left: 1px solid #bebebe;
 
	border-right: 1px solid #b1b1b1;
 
	border-bottom: 1px solid #afafaf;
 
}
 
 
div.form div.fields div.field div.highlight,#content div.box div.form div.fields div.buttons div.highlight
 
	{
 
	display: inline;
 
}
 
 
#content div.box div.form div.fields div.buttons,div.form div.fields div.buttons
 
	{
 
	margin: 10px 0 0 200px;
 
	padding: 0;
 
}
 
 
#content div.box-left div.form div.fields div.buttons,#content div.box-right div.form div.fields div.buttons,div.box-left div.form div.fields div.buttons,div.box-right div.form div.fields div.buttons
rhodecode/templates/changeset/changeset.html
Show inline comments
 
## -*- coding: utf-8 -*-
 

	
 
<%inherit file="/base/base.html"/>
 

	
 
<%def name="title()">
 
    ${c.repo_name} ${_('Changeset')} - r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)} - ${c.rhodecode_name}
 
</%def>
 

	
 
<%def name="breadcrumbs_links()">
 
    ${h.link_to(u'Home',h.url('/'))}
 
    &raquo;
 
    ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
 
    &raquo;
 
    ${_('Changeset')} - <span class='hash'>r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)}</span>
 
</%def>
 

	
 
<%def name="page_nav()">
 
    ${self.menu('changelog')}
 
</%def>
 

	
 
<%def name="main()">
 
<div class="box">
 
    <!-- box / title -->
 
    <div class="title">
 
        ${self.breadcrumbs()}
 
    </div>
 
    <div class="table">
 
		<div class="diffblock">
 
			<div class="code-header">
 
                <div class="hash">
 
                 r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)}
 
                </div>
 
                <div class="date">
 
                  ${c.changeset.date}
 
                </div>
 
                <div class="diff-actions">
 
                  <a href="${h.url('raw_changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id,diff='show')}" title="${_('raw diff')}" class="tooltip"><img class="icon" src="${h.url('/images/icons/page_white.png')}"/></a>
 
                  <a href="${h.url('raw_changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id,diff='download')}" title="${_('download diff')}" class="tooltip"><img class="icon" src="${h.url('/images/icons/page_white_get.png')}"/></a>
 
                  ${c.ignorews_url(request.GET)}
 
                  ${c.context_url(request.GET)}
 
                </div>
 
                <div class="comments-number" style="float:right;padding-right:5px">${len(c.comments)} comment(s) (${c.inline_cnt} ${_('inline')})</div>
 
            </div>       
 
		</div>
 
	    <div id="changeset_content">
 
			<div class="container">
 
	             <div class="left">
 
	                 <div class="author">
 
	                     <div class="gravatar">
 
	                         <img alt="gravatar" src="${h.gravatar_url(h.email(c.changeset.author),20)}"/>
 
	                     </div>
 
	                     <span>${h.person(c.changeset.author)}</span><br/>
 
	                     <span><a href="mailto:${h.email_or_none(c.changeset.author)}">${h.email_or_none(c.changeset.author)}</a></span><br/>
 
	                 </div>
 
	                 <div class="message">${h.urlify_commit(h.wrap_paragraphs(c.changeset.message),c.repo_name)}</div>
 
	             </div>
 
	             <div class="right">
 
		             <div class="changes">
 
                        % if len(c.changeset.affected_files) <= c.affected_files_cut_off:
 
		                 <span class="removed" title="${_('removed')}">${len(c.changeset.removed)}</span>
 
		                 <span class="changed" title="${_('changed')}">${len(c.changeset.changed)}</span>
 
		                 <span class="added" title="${_('added')}">${len(c.changeset.added)}</span>
 
	                    % else:
 
                         <span class="removed" title="${_('affected %s files') % len(c.changeset.affected_files)}">!</span>
 
                         <span class="changed" title="${_('affected %s files') % len(c.changeset.affected_files)}">!</span>
 
                         <span class="added"   title="${_('affected %s files') % len(c.changeset.affected_files)}">!</span>
 
	                    % endif
 
		             </div>
 

	
 
		            %if c.changeset.parents:
 
		             %for p_cs in reversed(c.changeset.parents):
 
		                 <div class="parent">${_('Parent')}
 
                     <span class="changeset_id">${p_cs.revision}:<span class="changeset_hash">${h.link_to(h.short_id(p_cs.raw_id),
 
		                     h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message)}</span></span>
 
		                 </div>
 
		             %endfor
 
                    %else:
 
                        <div class="parent">${_('No parents')}</div>
 
                    %endif
 
		         <span class="logtags">
 
                 %if len(c.changeset.parents)>1:
 
                 <span class="merge">${_('merge')}</span>
 
                 %endif
 
		             <span class="branchtag" title="${'%s %s' % (_('branch'),c.changeset.branch)}">
 
		             ${h.link_to(c.changeset.branch,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}</span>
 
		             %if c.changeset.branch:
 
                     <span class="branchtag" title="${'%s %s' % (_('branch'),c.changeset.branch)}">
 
		             ${h.link_to(c.changeset.branch,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}
 
                     </span>
 
                     %endif
 
		             %for tag in c.changeset.tags:
 
		                 <span class="tagtag"  title="${'%s %s' % (_('tag'),tag)}">
 
		                 ${h.link_to(tag,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}</span>
 
		             %endfor
 
		         </span>
 
	                </div>
 
	        </div>
 
	        <span>
 
	        ${_('%s files affected with %s insertions and %s deletions:') % (len(c.changeset.affected_files),c.lines_added,c.lines_deleted)}
 
	        </span>
 
	        <div class="cs_files">
 
	                %for change,filenode,diff,cs1,cs2,stat in c.changes:
 
	                    <div class="cs_${change}">
 
                            <div class="node">
 
                            %if change != 'removed':
 
                                ${h.link_to(h.safe_unicode(filenode.path),c.anchor_url(filenode.changeset.raw_id,filenode.path,request.GET)+"_target")}
 
                            %else:
 
                                ${h.link_to(h.safe_unicode(filenode.path),h.url.current(anchor=h.FID('',filenode.path)))}
 
                            %endif
 
                            </div>
 
		                    <div class="changes">${h.fancy_file_stats(stat)}</div>
 
	                    </div>
 
	                %endfor
 
	                % if c.cut_off:
 
	                  ${_('Changeset was too big and was cut off...')}
 
	                % endif
 
	        </div>
 
	    </div>
 

	
 
    </div>
 

	
 
    ## diff block
 
    <%namespace name="diff_block" file="/changeset/diff_block.html"/>
 
    ${diff_block.diff_block(c.changes)}
 

	
 
    ## template for inline comment form
 
    <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
 
    ${comment.comment_inline_form(c.changeset)}
 

	
 
    ## render comments
 
    ${comment.comments(c.changeset)}
 
    <script type="text/javascript">
 
      YUE.onDOMReady(function(){
 
    	  AJAX_COMMENT_URL = "${url('changeset_comment',repo_name=c.repo_name,revision=c.changeset.raw_id)}";
 
    	  AJAX_COMMENT_DELETE_URL = "${url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}"
 
          YUE.on(YUQ('.show-inline-comments'),'change',function(e){
 
              var show = 'none';
 
              var target = e.currentTarget;
 
              if(target.checked){
 
                  var show = ''
 
              }
 
              var boxid = YUD.getAttribute(target,'id_for');
 
              var comments = YUQ('#{0} .inline-comments'.format(boxid));
 
              for(c in comments){
 
                 YUD.setStyle(comments[c],'display',show);
 
              }
 
              var btns = YUQ('#{0} .inline-comments-button'.format(boxid));
 
              for(c in btns){
 
                  YUD.setStyle(btns[c],'display',show);
 
               }
 
          })
 

	
 
          YUE.on(YUQ('.line'),'click',function(e){
 
              var tr = e.currentTarget;
 
              injectInlineForm(tr);
 
          });
 

	
 
          // inject comments into they proper positions
 
          var file_comments = YUQ('.inline-comment-placeholder');
 
          renderInlineComments(file_comments);
 
          
 
          YUE.on(YUD.get('show_changeset_status_box'),'change',function(e){
 
        	  if(e.currentTarget.checked){
 
        		  YUD.setStyle('status_block_container','display','');  
 
        	  }
 
        	  else{
 
        		  YUD.setStyle('status_block_container','display','none');
 
        	  }
 
          })
 
      })
 

	
 
    </script>
 

	
 
    </div>
 
</%def>
rhodecode/templates/files/files_browser.html
Show inline comments
 
<%def name="file_class(node)">
 
	%if node.is_file():
 
		<%return "browser-file" %>
 
	%else:
 
		<%return "browser-dir"%>
 
	%endif
 
</%def>
 
<div id="body" class="browserblock">
 
    <div class="browser-header">
 
		<div class="browser-nav">
 
			${h.form(h.url.current())}
 
			<div class="info_box">
 
	          <span class="rev">${_('view')}@rev</span>
 
	          <a class="ui-btn" href="${c.url_prev}" title="${_('previous revision')}">&laquo;</a>
 
	          ${h.text('at_rev',value=c.changeset.revision,size=5)}
 
	          <a class="ui-btn" href="${c.url_next}" title="${_('next revision')}">&raquo;</a>
 
	          ## ${h.submit('view',_('view'),class_="ui-btn")}
 
		    </div>
 
			${h.end_form()}
 
		</div>
 
	    <div class="browser-branch">
 
	       ${h.checkbox('stay_at_branch',c.changeset.branch,c.changeset.branch==c.branch)}
 
	       <label>${_('follow current branch')}</label>
 
	    </div>
 
        <div class="browser-search">
 
              <div id="search_activate_id" class="search_activate">
 
                  <a class="ui-btn" id="filter_activate" href="#">${_('search file list')}</a>
 
              </div>
 
              % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
 
                    <div id="add_node_id" class="add_node">
 
                        <a class="ui-btn" href="${h.url('files_add_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path)}">${_('add new file')}</a>
 
                    </div>
 
              % endif
 
        <div>
 
            <div id="node_filter_box_loading" style="display:none">${_('Loading file list...')}</div>
 
            <div id="node_filter_box" style="display:none">
 
            ${h.files_breadcrumbs(c.repo_name,c.changeset.raw_id,c.file.path)}/<input class="init" type="text" value="type to search..." name="filter" size="25" id="node_filter" autocomplete="off">
 
            </div>
 
        </div>
 
        </div>
 
    </div>
 

	
 
	<div class="browser-body">
 
		<table class="code-browser">
 
		         <thead>
 
		             <tr>
 
		                 <th>${_('Name')}</th>
 
		                 <th>${_('Size')}</th>
 
		                 <th>${_('Mimetype')}</th>
 
		                 <th>${_('Last Revision')}</th>
 
		                 <th>${_('Last modified')}</th>
 
		                 <th>${_('Last commiter')}</th>
 
		             </tr>
 
		         </thead>
 

	
 
                <tbody id="tbody">
 
          		%if c.file.parent:
 
         		<tr class="parity0">
 
	          		<td>
 
	          			${h.link_to('..',h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.file.parent.path),class_="browser-dir ypjax-link")}
 
	          		</td>
 
	          		<td></td>
 
	          		<td></td>
 
	          		<td></td>
 
	          		<td></td>
 
	          		<td></td>
 
				</tr>
 
          		%endif
 

	
 
		    %for cnt,node in enumerate(c.file):
 
				<tr class="parity${cnt%2}">
 
		             <td>
 
                        ${h.link_to(node.name,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=h.safe_unicode(node.path)),class_=file_class(node)+" ypjax-link")}
 
                        %if node.is_submodule():
 
                           ${h.link_to(node.name,node.url or '#',class_="submodule-dir ypjax-link")}
 
                        %else:
 
                          ${h.link_to(node.name, h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=h.safe_unicode(node.path)),class_=file_class(node)+" ypjax-link")}
 
                        %endif:
 
		             </td>
 
		             <td>
 
		             %if node.is_file():
 
		             	${h.format_byte_size(node.size,binary=True)}
 
		             %endif
 
		             </td>
 
		             <td>
 
		              %if node.is_file():
 
		                  ${node.mimetype}
 
		              %endif
 
		             </td>
 
		             <td>
 
		             	%if node.is_file():
 
		             		<div class="tooltip" title="${node.last_changeset.message}">
 
		             		<pre>${'r%s:%s' % (node.last_changeset.revision,node.last_changeset.short_id)}</pre>
 
                            </div>
 
		             	%endif
 
		             </td>
 
		             <td>
 
		             	%if node.is_file():
 
		             		<span class="tooltip" title="${node.last_changeset.date}">
 
                            ${h.age(node.last_changeset.date)}</span>
 
		             	%endif
 
		             </td>
 
		             <td>
 
		             	%if node.is_file():
 
		             		<span title="${node.last_changeset.author}">
 
                            ${h.person(node.last_changeset.author)}
 
                            </span>
 
		             	%endif
 
		             </td>
 
				</tr>
 
			%endfor
 
                </tbody>
 
                <tbody id="tbody_filtered" style="display:none">
 
                </tbody>
 
		</table>
 
	</div>
 
</div>
rhodecode/templates/index_base.html
Show inline comments
 
<%page args="parent" />
 
    <div class="box">
 
        <!-- box / title -->
 
        <div class="title">
 
            <h5>
 
            <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" value="${_('quick filter...')}"/> ${parent.breadcrumbs()} <span id="repo_count">0</span> ${_('repositories')}
 
            </h5>
 
            %if c.rhodecode_user.username != 'default':
 
                %if h.HasPermissionAny('hg.admin','hg.create.repository')():
 
                <ul class="links">
 
                  <li>
 
                  %if c.group:
 
                    <span>${h.link_to(_('ADD REPOSITORY'),h.url('admin_settings_create_repository',parent_group=c.group.group_id))}</span>
 
                  %else:
 
                    <span>${h.link_to(_('ADD REPOSITORY'),h.url('admin_settings_create_repository'))}</span>
 
                  %endif
 
                  </li>
 
                </ul>
 
                %endif
 
            %endif
 
        </div>
 
        <!-- end box / title -->
 
        <div class="table">
 
           % if c.groups:
 
            <div id='groups_list_wrap' class="yui-skin-sam">
 
              <table id="groups_list">
 
                  <thead>
 
                      <tr>
 
                          <th class="left"><a href="#">${_('Group name')}</a></th>
 
                          <th class="left"><a href="#">${_('Description')}</a></th>
 
                          ##<th class="left"><a href="#">${_('Number of repositories')}</a></th>
 
                      </tr>
 
                  </thead>
 

	
 
                  ## REPO GROUPS
 
                  % for gr in c.groups:
 
                    <tr>
 
                        <td>
 
                            <div style="white-space: nowrap">
 
                            <img class="icon" alt="${_('Repositories group')}" src="${h.url('/images/icons/database_link.png')}"/>
 
                            ${h.link_to(gr.name,url('repos_group_home',group_name=gr.group_name))}
 
                            </div>
 
                        </td>
 
                        <td>${gr.group_description}</td>
 
                        ## this is commented out since for multi nested repos can be HEAVY!
 
                        ## in number of executed queries during traversing uncomment at will
 
                        ##<td><b>${gr.repositories_recursive_count}</b></td>
 
                    </tr>
 
                  % endfor
 

	
 
              </table>
 
            </div>
 
            <div style="height: 20px"></div>
 
            % endif
 
            <div id="welcome" style="display:none;text-align:center">
 
                <h1><a href="${h.url('home')}">${c.rhodecode_name} ${c.rhodecode_version}</a></h1>
 
            </div>
 
            <div id='repos_list_wrap' class="yui-skin-sam">
 
            <%cnt=0%>
 
            <%namespace name="dt" file="/_data_table/_dt_elements.html"/>
 

	
 
            <table id="repos_list">
 
            <thead>
 
                <tr>
 
                    <th class="left"></th>
 
                    <th class="left">${_('Name')}</th>
 
                    <th class="left">${_('Description')}</th>
 
                    <th class="left">${_('Last change')}</th>
 
                    <th class="left">${_('Tip')}</th>
 
                    <th class="left">${_('Owner')}</th>
 
                    <th class="left">${_('RSS')}</th>
 
                    <th class="left">${_('Atom')}</th>
 
                </tr>
 
            </thead>
 
            <tbody>
 
            %for cnt,repo in enumerate(c.repos_list):
 
                <tr class="parity${(cnt+1)%2}">
 
                    ##QUICK MENU
 
                    <td class="quick_repo_menu">
 
                      ${dt.quick_menu(repo['name'])}
 
                    </td>
 
                    ##REPO NAME AND ICONS
 
                    <td class="reponame">
 
                      ${dt.repo_name(repo['name'],repo['dbrepo']['repo_type'],repo['dbrepo']['private'],repo['dbrepo_fork'].get('repo_name'),pageargs.get('short_repo_names'))}
 
                    </td>
 
                    ##DESCRIPTION
 
                    <td><span class="tooltip" title="${h.tooltip(repo['description'])}">
 
                       ${h.truncate(repo['description'],60)}</span>
 
                    </td>
 
                    ##LAST CHANGE DATE
 
                    <td>
 
                      <span class="tooltip" title="${repo['last_change']}">${h.age(repo['last_change'])}</span>
 
                    </td>
 
                    ##LAST REVISION
 
                    <td>
 
                        ${dt.revision(repo['name'],repo['rev'],repo['tip'],repo['author'],repo['last_msg'])}
 
                    </td>
 
                    ##
 
                    <td title="${repo['contact']}">${h.person(repo['contact'])}</td>
 
                    <td>
 
                      %if c.rhodecode_user.username != 'default':
 
                        <a title="${_('Subscribe to %s rss feed')%repo['name']}" class="rss_icon"  href="${h.url('rss_feed_home',repo_name=repo['name'],api_key=c.rhodecode_user.api_key)}"></a>
 
                      %else:
 
                        <a title="${_('Subscribe to %s rss feed')%repo['name']}" class="rss_icon"  href="${h.url('rss_feed_home',repo_name=repo['name'])}"></a>
 
                      %endif:
 
                    </td>
 
                    <td>
 
                      %if c.rhodecode_user.username != 'default':
 
                        <a title="${_('Subscribe to %s atom feed')%repo['name']}"  class="atom_icon" href="${h.url('atom_feed_home',repo_name=repo['name'],api_key=c.rhodecode_user.api_key)}"></a>
 
                      %else:
 
                        <a title="${_('Subscribe to %s atom feed')%repo['name']}"  class="atom_icon" href="${h.url('atom_feed_home',repo_name=repo['name'])}"></a>
 
                      %endif:
 
                    </td>
 
                </tr>
 
            %endfor
 
            </tbody>
 
            </table>
 
            </div>
 
        </div>
 
    </div>
 
    <script>
 
      YUD.get('repo_count').innerHTML = ${cnt};
 
      YUD.get('repo_count').innerHTML = ${cnt+1};
 
      var func = function(node){
 
          return node.parentNode.parentNode.parentNode.parentNode;
 
      }
 

	
 

	
 
      // groups table sorting
 
      var myColumnDefs = [
 
          {key:"name",label:"${_('Group Name')}",sortable:true,
 
              sortOptions: { sortFunction: groupNameSort }},
 
          {key:"desc",label:"${_('Description')}",sortable:true},
 
      ];
 

	
 
      var myDataSource = new YAHOO.util.DataSource(YUD.get("groups_list"));
 

	
 
      myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
 
      myDataSource.responseSchema = {
 
          fields: [
 
              {key:"name"},
 
              {key:"desc"},
 
          ]
 
      };
 

	
 
      var myDataTable = new YAHOO.widget.DataTable("groups_list_wrap", myColumnDefs, myDataSource,
 
              {
 
               sortedBy:{key:"name",dir:"asc"},
 
               MSG_SORTASC:"${_('Click to sort ascending')}",
 
               MSG_SORTDESC:"${_('Click to sort descending')}"
 
              }
 
      );
 

	
 
      // main table sorting
 
      var myColumnDefs = [
 
          {key:"menu",label:"",sortable:false,className:"quick_repo_menu hidden"},
 
          {key:"name",label:"${_('Name')}",sortable:true,
 
              sortOptions: { sortFunction: nameSort }},
 
          {key:"desc",label:"${_('Description')}",sortable:true},
 
          {key:"last_change",label:"${_('Last Change')}",sortable:true,
 
              sortOptions: { sortFunction: ageSort }},
 
          {key:"tip",label:"${_('Tip')}",sortable:true,
 
        	  sortOptions: { sortFunction: revisionSort }},
 
          {key:"owner",label:"${_('Owner')}",sortable:true},
 
          {key:"rss",label:"",sortable:false},
 
          {key:"atom",label:"",sortable:false},
 
      ];
 

	
 
      var myDataSource = new YAHOO.util.DataSource(YUD.get("repos_list"));
 

	
 
      myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
 

	
 
      myDataSource.responseSchema = {
 
          fields: [
 
              {key:"menu"},
 
              {key:"name"},
 
              {key:"desc"},
 
              {key:"last_change"},
 
              {key:"tip"},
 
              {key:"owner"},
 
              {key:"rss"},
 
              {key:"atom"},
 
          ]
 
      };
 

	
 
      var myDataTable = new YAHOO.widget.DataTable("repos_list_wrap", myColumnDefs, myDataSource,
 
              {
 
               sortedBy:{key:"name",dir:"asc"},
 
               MSG_SORTASC:"${_('Click to sort ascending')}",
 
               MSG_SORTDESC:"${_('Click to sort descending')}",
 
               MSG_EMPTY:"${_('No records found.')}",
 
               MSG_ERROR:"${_('Data error.')}",
 
               MSG_LOADING:"${_('Loading...')}",
 
              }
 
      );
 
      myDataTable.subscribe('postRenderEvent',function(oArgs) {
 
          tooltip_activate();
 
          quick_repo_menu();
 
          q_filter('q_filter',YUQ('div.table tr td a.repo_name'),func);
 
      });
 

	
 
    </script>
rhodecode/templates/shortlog/shortlog_data.html
Show inline comments
 
## -*- coding: utf-8 -*-
 
%if c.repo_changesets:
 
<table class="table_disp">
 
	<tr>
 
	    <th class="left">${_('revision')}</th>
 
        <th class="left">${_('commit message')}</th>
 
		<th class="left">${_('age')}</th>
 
		<th class="left">${_('author')}</th>
 
		<th class="left">${_('branch')}</th>
 
		<th class="left">${_('tags')}</th>
 
	</tr>
 
%for cnt,cs in enumerate(c.repo_changesets):
 
	<tr class="parity${cnt%2}">
 
        <td>
 
            <div><pre><a href="${h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id)}">r${cs.revision}:${h.short_id(cs.raw_id)}</a></pre></div>
 
        </td>
 
        <td>
 
            ${h.link_to(h.truncate(cs.message,50),
 
            ${h.link_to(h.truncate(cs.message,50) or _('No commit message'),
 
            h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id),
 
            title=cs.message)}
 
        </td>
 
        <td><span class="tooltip" title="${cs.date}">
 
                      ${h.age(cs.date)}</span>
 
        </td>
 
		<td title="${cs.author}">${h.person(cs.author)}</td>
 
		<td>
 
			<span class="logtags">
 
                %if cs.branch:
 
				<span class="branchtag">
 
                    ${cs.branch}
 
                </span>
 
                %endif
 
			</span>
 
		</td>
 
		<td>
 
			<span class="logtags">
 
				%for tag in cs.tags:
 
					<span class="tagtag">${tag}</span>
 
				%endfor
 
			</span>
 
		</td>
 
	</tr>
 
%endfor
 

	
 
</table>
 

	
 
<script type="text/javascript">
 
  YUE.onDOMReady(function(){
 
    YUE.delegate("shortlog_data","click",function(e, matchedEl, container){
 
        ypjax(e.target.href,"shortlog_data",function(){tooltip_activate();});
 
        YUE.preventDefault(e);
 
    },'.pager_link');
 
  });
 
</script>
 

	
 
<div class="pagination-wh pagination-left">
 
${c.repo_changesets.pager('$link_previous ~2~ $link_next')}
 
</div>
 
%else:
 

	
 
%if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
 
<h4>${_('Add or upload files directly via RhodeCode')}</h4>
 
<div style="margin: 20px 30px;">
 
  <div id="add_node_id" class="add_node">
 
      <a class="ui-btn" href="${h.url('files_add_home',repo_name=c.repo_name,revision=0,f_path='')}">${_('add new file')}</a>
 
  </div>
 
</div>
 
%endif
 

	
 

	
 
<h4>${_('Push new repo')}</h4>
 
<pre>
 
    ${c.rhodecode_repo.alias} clone ${c.clone_repo_url}
 
    ${c.rhodecode_repo.alias} add README # add first file
 
    ${c.rhodecode_repo.alias} commit -m "Initial" # commit with message
 
    ${c.rhodecode_repo.alias} push ${'origin master' if h.is_git(c.rhodecode_repo) else ''} # push changes back
 
</pre>
 

	
 
<h4>${_('Existing repository?')}</h4>
 
<pre>
 
    ${c.rhodecode_repo.alias} push ${c.clone_repo_url}
 
</pre>
 
%endif
0 comments (0 inline, 0 general)